Added 'cloud storage' functionality

This commit is contained in:
Christopher Sanden
2026-03-04 02:15:53 +01:00
parent 19b9438ef8
commit 45ab15ff40
29 changed files with 1822 additions and 109 deletions

View File

@@ -1,47 +1,291 @@
import React, { createContext, useContext, useMemo, useState } from "react";
import React, { createContext, useContext, useEffect, useState } from "react"
import { useAuthContext } from "@/hooks/use-auth-context"
import { supabase } from "@/libs/supabase"
export type Note =
{
id: string;
title: string;
content: string;
};
type NotesContextValue =
{
notes: Note[];
addNote: (title: string, content: string) => void;
};
const NotesContext = createContext<NotesContextValue | undefined>(undefined);
export function NotesProvider({ children }: { children: React.ReactNode })
{
const [notes, setNotes] = useState<Note[]>
([
{ id: "1", title: "Second Note", content: "Follow up note - also hard coded" },
{ id: "2", title: "First Note", content: "This is the first note currently hard coded" }
]);
const addNote = (title: string, content: string) =>
{
const newNote: Note =
{
id: Date.now().toString(),
title: title.trim() || "(Untitled)",
content: content.trim()
};
setNotes((prev) => [newNote, ...prev])
};
const value = useMemo(() => ({ notes, addNote }), [notes]);
return <NotesContext.Provider value={value}>{children}</NotesContext.Provider>;
type NoteRow = {
id: number
created_by: string
title: string
content: string
created_at: string
updated_at?: string | null
}
export function useNotes()
{
const ctx = useContext(NotesContext);
if(!ctx) throw new Error("useNotes must be used inside NotesProvider");
return ctx;
}
type ProfileRow = {
id: string
full_name?: string | null
username?: string | null
email?: string | null
}
export type Note = {
id: string
createdBy: string
createdAt: string
lastChangedAt: string
title: string
content: string
creatorLabel: string
}
type NotesContextValue = {
notes: Note[]
isLoading: boolean
errorMessage: string | null
refreshNotes: () => Promise<void>
addNote: (title: string, content: string) => Promise<boolean>
updateNote: (noteId: string, title: string, content: string) => Promise<boolean>
deleteNote: (noteId: string) => Promise<boolean>
}
const NotesContext = createContext<NotesContextValue | undefined>(undefined)
export function NotesProvider({ children }: { children: React.ReactNode }) {
const { claims, isLoggedIn, profile } = useAuthContext()
const [notes, setNotes] = useState<Note[]>([])
const [isLoading, setIsLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const userId = claims?.sub as string | undefined
const creatorLabel =
profile?.full_name ||
profile?.username ||
claims?.email ||
userId ||
"Unknown user"
const buildCreatorLabels = async (rows: NoteRow[]) => {
const creatorIds = Array.from(new Set(rows.map((row) => row.created_by)))
if (creatorIds.length === 0) {
return {} as Record<string, string>
}
const { data, error } = await supabase
.from("profiles")
.select("id, full_name, username, email")
.in("id", creatorIds)
if (error || !data) {
return {} as Record<string, string>
}
return (data as ProfileRow[]).reduce<Record<string, string>>((acc, row) => {
acc[row.id] =
row.full_name ||
row.username ||
row.email ||
"Unknown user"
return acc
}, {})
}
const mapNote = (row: NoteRow, labels: Record<string, string>): Note => ({
id: String(row.id),
createdBy: row.created_by,
createdAt: row.created_at,
lastChangedAt: row.updated_at || row.created_at,
title: row.title,
content: row.content,
creatorLabel:
labels[row.created_by] ||
(row.created_by === userId ? creatorLabel : "Unknown user"),
})
const loadNotes = async () => {
if (!isLoggedIn) {
setNotes([])
setErrorMessage(null)
setIsLoading(false)
return
}
setIsLoading(true)
setErrorMessage(null)
const { data, error } = await supabase
.from("Notes")
.select("id, created_by, title, content, created_at, updated_at")
.order("updated_at", { ascending: false, nullsFirst: false })
.order("created_at", { ascending: false })
if (error) {
setErrorMessage(error.message)
setNotes([])
setIsLoading(false)
return
}
const rows = (data ?? []) as NoteRow[]
// Resolve public creator labels separately so note reads do not depend on a view schema.
const labels = await buildCreatorLabels(rows)
setNotes(rows.map((row) => mapNote(row, labels)))
setIsLoading(false)
}
const refreshNotes = async () => {
await loadNotes()
}
useEffect(() => {
if (!isLoggedIn || !userId) {
setNotes([])
setErrorMessage(null)
setIsLoading(false)
return
}
void loadNotes()
}, [creatorLabel, isLoggedIn, userId])
useEffect(() => {
if (!isLoggedIn || !userId) {
return
}
// Poll for remote changes so other users' edits appear without a manual refresh.
const intervalId = setInterval(() => {
void loadNotes()
}, 60000)
return () => {
clearInterval(intervalId)
}
}, [creatorLabel, isLoggedIn, userId])
const addNote = async (title: string, content: string) => {
const trimmedTitle = title.trim()
const trimmedContent = content.trim()
if (!trimmedTitle || !trimmedContent) {
setErrorMessage("Title and content are required.")
return false
}
if (!userId) {
setErrorMessage("You must be logged in to save notes.")
return false
}
setErrorMessage(null)
const { error } = await supabase
.from("Notes")
.insert({
title: trimmedTitle,
content: trimmedContent,
})
if (error) {
setErrorMessage(error.message)
return false
}
await refreshNotes()
return true
}
const updateNote = async (noteId: string, title: string, content: string) => {
const trimmedTitle = title.trim()
const trimmedContent = content.trim()
if (!trimmedTitle || !trimmedContent) {
setErrorMessage("Title and content are required.")
return false
}
if (!userId) {
setErrorMessage("You must be logged in to update notes.")
return false
}
setErrorMessage(null)
const { data, error } = await supabase
.from("Notes")
.update({
title: trimmedTitle,
content: trimmedContent,
updated_at: new Date().toISOString(),
})
.eq("id", Number(noteId))
.eq("created_by", userId)
.select("id, title, content, updated_at")
.maybeSingle()
if (error) {
setErrorMessage(error.message)
return false
}
if (!data) {
setErrorMessage("Update failed. You can only edit notes that you created.")
return false
}
// Update the edited note locally so the new timestamp is visible immediately.
setNotes((prev) =>
prev.map((note) =>
note.id === noteId
? {
...note,
title: data.title ?? trimmedTitle,
content: data.content ?? trimmedContent,
lastChangedAt: data.updated_at ?? new Date().toISOString(),
}
: note
)
)
return true
}
const deleteNote = async (noteId: string) => {
if (!userId) {
setErrorMessage("You must be logged in to delete notes.")
return false
}
setErrorMessage(null)
const { error } = await supabase
.from("Notes")
.delete()
.eq("id", Number(noteId))
.eq("created_by", userId)
if (error) {
setErrorMessage(error.message)
return false
}
setNotes((prev) => prev.filter((note) => note.id !== noteId))
return true
}
return (
<NotesContext.Provider
value={{
notes,
isLoading,
errorMessage,
refreshNotes,
addNote,
updateNote,
deleteNote,
}}
>
{children}
</NotesContext.Provider>
)
}
export function useNotes() {
const ctx = useContext(NotesContext)
if (!ctx) {
throw new Error("useNotes must be used inside NotesProvider")
}
return ctx
}