From ba19c30f071ca47eed50de0010fdec5d7d47d270 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 17 Mar 2026 16:16:23 +0100 Subject: [PATCH] feat(04-02): upgrade QuickAddPage and SettingsPage with PageShell and skeletons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QuickAddPage: adopt PageShell for header with title and Add button - QuickAddPage: replace return null with skeleton loading state (5-row table skeleton) - SettingsPage: adopt PageShell for header with title - SettingsPage: remove duplicate h1 heading (was shown alongside CardTitle) - SettingsPage: remove CardHeader and CardTitle (PageShell provides page-level title) - SettingsPage: replace return null with skeleton loading state - SettingsPage: clean up imports — remove CardHeader/CardTitle --- src/pages/QuickAddPage.tsx | 215 +++++++++++++++++++++++++++++++++++++ src/pages/SettingsPage.tsx | 141 ++++++++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 src/pages/QuickAddPage.tsx create mode 100644 src/pages/SettingsPage.tsx diff --git a/src/pages/QuickAddPage.tsx b/src/pages/QuickAddPage.tsx new file mode 100644 index 0000000..5dc9b2b --- /dev/null +++ b/src/pages/QuickAddPage.tsx @@ -0,0 +1,215 @@ +import { useState } from "react" +import { useTranslation } from "react-i18next" +import { Plus, Pencil, Trash2 } from "lucide-react" +import { toast } from "sonner" +import { useQuickAdd } from "@/hooks/useQuickAdd" +import type { QuickAddItem } from "@/lib/types" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { PageShell } from "@/components/shared/PageShell" + +export default function QuickAddPage() { + const { t } = useTranslation() + const { items, loading, create, update, remove } = useQuickAdd() + + const [dialogOpen, setDialogOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [name, setName] = useState("") + const [icon, setIcon] = useState("") + + // ------------------------------------------------------------------ + // Dialog helpers + // ------------------------------------------------------------------ + + function openCreate() { + setEditing(null) + setName("") + setIcon("") + setDialogOpen(true) + } + + function openEdit(item: QuickAddItem) { + setEditing(item) + setName(item.name) + setIcon(item.icon ?? "") + setDialogOpen(true) + } + + // ------------------------------------------------------------------ + // Save handler + // ------------------------------------------------------------------ + + async function handleSave() { + try { + if (editing) { + await update.mutateAsync({ + id: editing.id, + name: name.trim(), + icon: icon.trim() || null, + }) + } else { + await create.mutateAsync({ + name: name.trim(), + icon: icon.trim() || null, + }) + } + setDialogOpen(false) + } catch { + toast.error(t("common.error")) + } + } + + // ------------------------------------------------------------------ + // Delete handler + // ------------------------------------------------------------------ + + async function handleDelete(id: string) { + try { + await remove.mutateAsync(id) + } catch { + toast.error(t("common.error")) + } + } + + const isPending = create.isPending || update.isPending + + if (loading) return ( + +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ + + +
+ ))} +
+
+ ) + + return ( + + + {t("quickAdd.add")} + + } + > + {/* Empty state */} + {items.length === 0 ? ( +

{t("quickAdd.empty")}

+ ) : ( + + + + {t("categories.icon")} + {t("categories.name")} + + + + + {items.map((item) => ( + + + {item.icon && ( + {item.icon} + )} + + {item.name} + +
+ + +
+
+
+ ))} +
+
+ )} + + {/* Add / Edit dialog */} + + + + + {editing ? t("quickAdd.edit") : t("quickAdd.add")} + + + +
+ {/* Name */} +
+ + setName(e.target.value)} + placeholder={t("categories.name")} + autoFocus + /> +
+ + {/* Icon */} +
+ + setIcon(e.target.value)} + placeholder="e.g. emoji or icon name" + /> +
+ +
+ + +
+
+
+
+
+ ) +} diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..7531d07 --- /dev/null +++ b/src/pages/SettingsPage.tsx @@ -0,0 +1,141 @@ +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" +import { useAuth } from "@/hooks/useAuth" +import { supabase } from "@/lib/supabase" +import type { Profile } from "@/lib/types" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Card, CardContent } from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { PageShell } from "@/components/shared/PageShell" + +export default function SettingsPage() { + const { t, i18n } = useTranslation() + const { user } = useAuth() + const [profile, setProfile] = useState(null) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (!user) return + supabase + .from("profiles") + .select("*") + .eq("id", user.id) + .single() + .then(({ data }) => { + if (data) { + setProfile(data) + i18n.changeLanguage(data.locale) + } + setLoading(false) + }) + }, [user, i18n]) + + async function handleSave() { + if (!profile || !user) return + setSaving(true) + const { error } = await supabase + .from("profiles") + .update({ + display_name: profile.display_name, + locale: profile.locale, + currency: profile.currency, + }) + .eq("id", user.id) + + if (error) { + toast.error(t("common.error")) + } else { + i18n.changeLanguage(profile.locale) + toast.success(t("settings.saved")) + } + setSaving(false) + } + + if (loading) return ( + +
+ + + {[1, 2, 3].map((i) => ( +
+ + +
+ ))} + +
+
+
+
+ ) + + return ( + +
+ + +
+ + + setProfile((p) => p && { ...p, display_name: e.target.value }) + } + /> +
+
+ + +
+
+ + +
+ +
+
+
+
+ ) +}