import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Database } from "@xtablo/shared/types/database.types"; import { useNavigate } from "react-router-dom"; import { supabase } from "../lib/supabase"; import { useUser } from "../providers/UserStoreProvider"; import { useAuthedApi } from "./auth"; type Note = Database["public"]["Tables"]["notes"]["Row"]; type CreateNoteInput = Pick; type UpdateNoteInput = Pick< Database["public"]["Tables"]["notes"]["Update"], "id" | "title" | "content" >; export function useNotes() { const user = useUser(); const { data: notes, isLoading } = useQuery({ queryKey: ["notes"], queryFn: async () => { const { data, error } = await supabase .from("notes") .select("*") .eq("user_id", user.id) .is("deleted_at", null) .order("created_at", { ascending: false }); if (error) throw error; return data as Note[]; }, enabled: !!user.id, }); return { notes, isLoading }; } export function useNote(id: string | undefined) { const user = useUser(); const { data: note, isLoading } = useQuery({ queryKey: ["notes", id], queryFn: async () => { const { data, error } = await supabase .from("notes") .select("*") .eq("id", id) .eq("user_id", user.id) .is("deleted_at", null); if (error) throw error; if (data.length === 0) return null; return data[0] as Note; }, enabled: !!id, }); return { note, isLoading }; } export function useCreateNote() { const user = useUser(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { mutate, isPending } = useMutation({ mutationFn: async (input: CreateNoteInput) => { const { data, error } = await supabase .from("notes") .insert({ title: input.title, content: input.content, user_id: user.id, }) .select() .single(); if (error) throw error; return data as Note; }, onSuccess: (note) => { // Invalidate notes list to refetch queryClient.invalidateQueries({ queryKey: ["notes"] }); navigate(`/notes/${note.id}`); }, }); return { mutate, isPending }; } /** * Update an existing note */ export function useUpdateNote() { const user = useUser(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (input: UpdateNoteInput) => { const { error } = await supabase .from("notes") .update({ title: input.title, content: input.content, updated_at: new Date().toISOString(), }) .eq("id", input.id) .eq("user_id", user.id) .is("deleted_at", null); if (error) throw error; return; }, onSuccess: () => { // Invalidate both the specific note and notes list queryClient.invalidateQueries({ queryKey: ["notes"] }); queryClient.invalidateQueries({ queryKey: ["tablo-notes"] }); }, }); } export function useDeleteNote() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { mutate, isPending } = useMutation({ mutationFn: async (id: string) => { const { error } = await supabase .from("notes") .update({ deleted_at: new Date().toISOString() }) .eq("id", id); if (error) throw error; return; }, onSuccess: () => { // Invalidate notes list to refetch queryClient.invalidateQueries({ queryKey: ["notes"] }); navigate("/notes/create", { replace: true }); }, }); return { mutate, isPending }; } /** * Hook to fetch sharing settings for a specific note */ export function useNoteSharing(noteId: string | undefined) { const user = useUser(); const { data, isLoading } = useQuery({ queryKey: ["note-sharing", noteId], queryFn: async () => { if (!noteId) return { isPublic: false, isSharedWithAllTablos: false }; // Fetch public sharing status const { data: sharedNote, error: sharedError } = await supabase .from("shared_notes") .select("is_public") .eq("note_id", noteId) .eq("user_id", user.id) .maybeSingle(); if (sharedError) throw sharedError; // Fetch tablo sharing status (tablo_id IS NULL means shared with all tablos) const { data: noteAccess, error: accessError } = await supabase .from("note_access") .select("is_active, tablo_id") .eq("note_id", noteId) .eq("user_id", user.id) .is("tablo_id", null) .maybeSingle(); if (accessError) throw accessError; return { isPublic: sharedNote?.is_public ?? false, isSharedWithAllTablos: noteAccess?.is_active ?? false, }; }, enabled: !!noteId && !!user.id, }); return { isPublic: data?.isPublic, isSharedWithAllTablos: data?.isSharedWithAllTablos, isLoading, }; } /** * Hook to update sharing settings for a note */ export function useUpdateNoteSharing() { const user = useUser(); const queryClient = useQueryClient(); return useMutation< void, Error, { noteId: string; isPublic: boolean; isSharedWithAllTablos: boolean } >({ mutationFn: async ({ noteId, isPublic, isSharedWithAllTablos }) => { // Upsert shared_notes for public sharing const { error: sharedError } = await supabase.from("shared_notes").upsert( { note_id: noteId, user_id: user.id, is_public: isPublic, updated_at: new Date().toISOString(), }, { onConflict: "note_id", } ); if (sharedError) throw sharedError; // Handle tablo sharing if (isSharedWithAllTablos) { // First, try to update existing "all tablos" sharing record const { data: existingRecord, error: selectError } = await supabase .from("note_access") .select("id") .eq("note_id", noteId) .eq("user_id", user.id) .is("tablo_id", null) .maybeSingle(); if (selectError) throw selectError; if (existingRecord) { // Update existing record const { error: updateError } = await supabase .from("note_access") .update({ is_active: true, updated_at: new Date().toISOString(), }) .eq("id", existingRecord.id); if (updateError) throw updateError; } else { // Insert new record const { error: insertError } = await supabase.from("note_access").insert({ note_id: noteId, user_id: user.id, tablo_id: null, is_active: true, updated_at: new Date().toISOString(), }); if (insertError) throw insertError; } } else { // Remove tablo sharing by deleting const { error: deleteError } = await supabase .from("note_access") .delete() .eq("note_id", noteId) .eq("user_id", user.id) .is("tablo_id", null); if (deleteError) throw deleteError; } }, onSuccess: (_, variables) => { // Invalidate sharing queries queryClient.invalidateQueries({ queryKey: ["note-sharing", variables.noteId] }); queryClient.invalidateQueries({ queryKey: ["tablo-notes"] }); }, }); } /** * Hook to fetch a public note (unauthenticated access allowed) */ export function usePublicNote(noteId: string | undefined) { const { data: note, isLoading } = useQuery({ queryKey: ["public-note", noteId], queryFn: async () => { if (!noteId) return null; // First check if the note is public const { data: sharedNote, error: sharedError } = await supabase .from("shared_notes") .select("note_id, is_public") .eq("note_id", noteId) .eq("is_public", true) .maybeSingle(); if (sharedError) throw sharedError; if (!sharedNote) return null; // Fetch the actual note const { data: noteData, error: noteError } = await supabase .from("notes") .select("*") .eq("id", noteId) .is("deleted_at", null) .maybeSingle(); if (noteError) throw noteError; return noteData as Note | null; }, enabled: !!noteId, }); return { note, isLoading }; } /** * Hook to fetch notes shared with a specific tablo */ export function useTabloNotes(tabloId: string | undefined) { const user = useUser(); const api = useAuthedApi(); const { data, isLoading } = useQuery<{ notes: Note[] }>({ queryKey: ["tablo-notes", tabloId], queryFn: async () => { if (!tabloId) return { notes: [] }; const { data } = await api.get<{ notes: Note[] }>(`/api/v1/notes/${tabloId}`); return data; }, enabled: !!tabloId && !!user.id, }); return { notes: data?.notes, isLoading }; }