From b013e8be2878c70f3ee660a7603d165090ea885c Mon Sep 17 00:00:00 2001 From: Truong Trung Nghia <149591043+ttncode@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:48:23 +0700 Subject: [PATCH] feat: about me page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary About Me page (menu → Settings → About Me, or Alt+P): name, bio and location. The save bar appears only with unsaved changes; Cmd/Ctrl+Enter saves. ## Testing - Lint, typecheck, unit tests and build pass. - In the browser: first save inserts, later saves update, save bar hides after saving. --- src/components/pages/AboutMePage.tsx | 152 ++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/src/components/pages/AboutMePage.tsx b/src/components/pages/AboutMePage.tsx index f5fcbbe..47270ee 100644 --- a/src/components/pages/AboutMePage.tsx +++ b/src/components/pages/AboutMePage.tsx @@ -1,6 +1,154 @@ "use client"; +import { useEffect, useState } from "react"; +import { Save } from "../icons"; +import { localDb, type Row } from "../lib/local-db"; + +type AboutMeRow = Row & { + user_id: string; + name: string | null; + bio: string | null; + location: string | null; +}; + +interface Profile { + name: string; + bio: string; + location: string; +} + +const EMPTY: Profile = { name: "", bio: "", location: "" }; + +const fieldClass = + "w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"; + export function AboutMePage({ userId }: { userId: string }) { - void userId; - return null; + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [rowId, setRowId] = useState(null); + const [form, setForm] = useState(EMPTY); + const [saved, setSaved] = useState(EMPTY); + + useEffect(() => { + let cancelled = false; + localDb + .from("about_me") + .select("*") + .eq("user_id", userId) + .maybeSingle() + .then(({ data, error }) => { + if (cancelled) return; + if (error) console.error("Error loading about me:", error); + if (data) { + const profile = { name: data.name ?? "", bio: data.bio ?? "", location: data.location ?? "" }; + setRowId(data.id); + setForm(profile); + setSaved(profile); + } + setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [userId]); + + const dirty = form.name !== saved.name || form.bio !== saved.bio || form.location !== saved.location; + + const save = async () => { + setSaving(true); + const values = { + name: form.name.trim() || null, + bio: form.bio.trim() || null, + location: form.location.trim() || null, + }; + const { data, error } = rowId + ? await localDb + .from("about_me") + .update({ ...values, updated_at: new Date().toISOString() }) + .eq("id", rowId) + .select() + : await localDb.from("about_me").insert({ user_id: userId, ...values }).select(); + if (error) { + console.error("Error saving about me:", error); + } else { + if (!rowId && data?.[0]) setRowId(data[0].id); + setSaved(form); + } + setSaving(false); + }; + + // Cmd/Ctrl+Enter saves from anywhere on the page. + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && dirty && !saving) { + e.preventDefault(); + void save(); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + const set = (key: keyof Profile) => (e: { target: { value: string } }) => + setForm((prev) => ({ ...prev, [key]: e.target.value })); + + return ( +
+
+

About Me

+
+ +
+
+
+ + +
+ +
+ +