319 lines
9 KiB
TypeScript
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 };
|
|
}
|