xtablo-source/apps/main/src/hooks/notes.ts
Arthur Belleville 78d5c3e45c
Fix read notes
2025-10-26 15:40:29 +01:00

319 lines
9 KiB
TypeScript

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<Database["public"]["Tables"]["notes"]["Insert"], "title" | "content">;
type UpdateNoteInput = Pick<
Database["public"]["Tables"]["notes"]["Update"],
"id" | "title" | "content"
>;
export function useNotes() {
const user = useUser();
const { data: notes, isLoading } = useQuery<Note[]>({
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<Note | null>({
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<Note, Error, CreateNoteInput>({
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<void, Error, UpdateNoteInput>({
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<void, Error, string>({
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<Note | null>({
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 };
}