diff --git a/app/(app)/settings/profile/page.tsx b/app/(app)/settings/profile/page.tsx new file mode 100644 index 00000000..84599d96 --- /dev/null +++ b/app/(app)/settings/profile/page.tsx @@ -0,0 +1,64 @@ +"use client"; + +/** + * /settings/profile — dedicated profile editor. + * + * Lives outside the /u/me profile layout (same reasoning as /settings) so it + * carries the default site chrome instead of the ProfileHeader + tabs frame. + * The "Edit profile" button on the profile header links here. The page owns + * the title + back button; ProfileEditForm renders the sections. Saving stays + * on the page (toast); the back button / Cancel return to /u/me. + */ + +import { useEffect } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { ChevronLeft } from "lucide-react"; +import { useAuth } from "@/components/auth/auth-provider"; +import { useAuthGate } from "@/components/auth/auth-gate"; +import { ProfileEditForm } from "@/components/profile/profile-edit-form"; + +export default function EditProfilePage() { + const { user, loading } = useAuth(); + const { requireAuth } = useAuthGate(); + const router = useRouter(); + + // Same auth gate as the /u/me layout: prompt anonymous visitors and bounce + // them home if they dismiss it. + useEffect(() => { + if (loading || user) return; + let cancelled = false; + (async () => { + const ok = await requireAuth(); + if (!ok && !cancelled) router.replace("/"); + })(); + return () => { + cancelled = true; + }; + }, [loading, user, requireAuth, router]); + + return ( +
+
+ + + Back to profile + +

+ Edit profile +

+
+ + {loading || !user ? ( +

+ {loading ? "Loading…" : "Sign in to edit your profile."} +

+ ) : ( + router.push("/u/me")} /> + )} +
+ ); +} diff --git a/app/(app)/u/me/layout.tsx b/app/(app)/u/me/layout.tsx index 5fd1bc13..9e99286b 100644 --- a/app/(app)/u/me/layout.tsx +++ b/app/(app)/u/me/layout.tsx @@ -8,13 +8,13 @@ * • Renders the ProfileHeader + ProfileTabs above every /u/me/* page. * Tabs: Posts / Collections / Bookmarks / Mentions. * - * The old left ProfileSidebar is gone. The "Edit profile" affordance - * moved onto the header as a button that toggles edit mode on /u/me - * via a query param (kept simple — no new route for the form). + * The old left ProfileSidebar is gone. The "Edit profile" affordance on + * the header links out to the dedicated /settings/profile editor (which + * lives outside this layout so it isn't framed by the header + tabs). */ -import { Suspense, useEffect, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; import { AtSign, Bookmark, @@ -130,29 +130,13 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { name={user.name} handle={user.handle} joinedAt={user.created_at} - editHref="/u/me?edit=1" + editHref="/settings/profile" postCount={counts?.posts} followerCount={counts?.followers} followingCount={counts?.following} /> - {/* Tab strip hides on /u/me?edit=1 (see ConditionalTabs). The - Suspense boundary is required by Next.js because useSearchParams - forces a CSR bailout during prerender. Without it, every - /u/me/* route fails to build. Fallback renders the tabs as-is - so the visual default (tabs visible) is what users see while - the search params resolve. */} - }> - - +
{children}
); } - -function ConditionalTabs({ tabs }: { tabs: ProfileTab[] }) { - const searchParams = useSearchParams(); - // Hide tabs when the user is editing their profile inline — the strip - // is visual noise that distracts from the task they navigated in to do. - const editMode = searchParams.get("edit") === "1"; - return editMode ? null : ; -} diff --git a/app/(app)/u/me/page.tsx b/app/(app)/u/me/page.tsx index 7b34b446..1c8afdb5 100644 --- a/app/(app)/u/me/page.tsx +++ b/app/(app)/u/me/page.tsx @@ -7,40 +7,19 @@ * /u/me/posts#post- where the Instagram-style scroll feed renders * the full passages. * - * `?edit=1` (set by the header's Edit profile button) swaps the grid - * for the inline edit form. Saving returns to /u/me. Keeps the URL - * shareable while avoiding a separate route + layout. + * Editing the profile lives on its own page (/settings/profile), reached + * from the header's "Edit profile" button. */ -import { Suspense, useEffect, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; import { LayoutGrid } from "lucide-react"; import { useAuth } from "@/components/auth/auth-provider"; -import { ProfileEditForm } from "@/components/profile/profile-edit-form"; import { PostGridCard } from "@/components/social/post/post-grid-card"; import { ConfirmModal } from "@/components/ui/confirm-modal"; import { posts, SocialError, type Post } from "@/lib/social"; export default function MyProfilePage() { - // useSearchParams() forces a CSR bailout during prerender; the - // Suspense boundary is required by Next.js so the build doesn't fail - // (the same wrapping is in app/u/me/layout.tsx for its own check). - // The fallback matches the page's idle loading state so the brief - // moment before search params resolve doesn't flash an empty section. - return ( - Loading…

} - > - -
- ); -} - -function MyProfilePageInner() { const { user } = useAuth(); - const router = useRouter(); - const searchParams = useSearchParams(); - const editMode = searchParams.get("edit") === "1"; const [feed, setFeed] = useState([]); const [loading, setLoading] = useState(true); @@ -87,10 +66,6 @@ function MyProfilePageInner() { if (!user) return null; - if (editMode) { - return router.replace("/u/me")} />; - } - return (
{error ? ( diff --git a/components/profile/profile-edit-form.tsx b/components/profile/profile-edit-form.tsx index 85f2b230..540eeecb 100644 --- a/components/profile/profile-edit-form.tsx +++ b/components/profile/profile-edit-form.tsx @@ -1,23 +1,27 @@ "use client"; /** - * ProfileEditForm — the editor block used in /u/me's edit mode. + * ProfileEditForm — the editor used on the dedicated /settings/profile page. * - * Three editable fields: - * • Display name (PATCH /users/me { name }) - * • Handle (PATCH /users/me { handle }) — 409 if taken - * • Avatar (R2 presign + PATCH /users/me { avatar_url }) + * Three groups of editable fields, rendered as plain page sections (no card + * chrome) so the page reads as a normal settings-style screen rather than a + * widget: + * • Profile photo (R2 presign + PATCH /users/me { avatar_url }) + * • Account (name, handle, country, email) + * • Devotional details (sampradāya, gender, initiation, daily rounds) * - * `onDone` is invoked after a successful save OR when the user clicks - * "Cancel" — the parent flips back to view mode. + * The page owns the title + back button; this component renders only the + * sections and the Save/Cancel footer. `onDone` runs on "Cancel" (the parent + * navigates back to the profile). Saving stays on the page with a toast and + * re-baselines the form against the freshly-saved user. * - * No log-out button here — that's handled by the sidebar footer so it lives - * outside the edit context. + * No log-out button here — that lives in the sidebar/settings footer so it + * stays outside the edit context. */ import { useRef, useState } from "react"; import Image from "next/image"; -import { ImagePlus, Save, X } from "lucide-react"; +import { ImagePlus, Save } from "lucide-react"; import { toast } from "sonner"; import { useAuth } from "@/components/auth/auth-provider"; import { auth, AuthError, type User } from "@/lib/auth"; @@ -135,10 +139,9 @@ export function ProfileEditForm({ } const updated = await auth.updateProfile(patch); - // Update the cached user in place and STAY on the edit form. The - // form's fields re-baseline against the new `user` prop (so it's no - // longer "dirty"), and we keep the ?edit=1 URL rather than bouncing - // back to the profile view. Exit is via Cancel/✕. + // Update the cached user in place and STAY on the page. The form's + // fields re-baseline against the new `user` prop (so it's no longer + // "dirty"); exit is via the page's back button or Cancel. setUser(updated); toast.success("Profile saved"); } catch (err) { @@ -169,60 +172,44 @@ export function ProfileEditForm({ } return ( -
-
-

- Edit profile -

- -
- - {/* Avatar — same widget pattern as the old /profile form */} -
- -
- - - {avatarUrl ? ( + +
+
+ +
+ - ) : null} + {avatarUrl ? ( + + ) : null} +
-
+
-
-

- Account -

+
)}
-
- -
-

- Devotional details -

+
+
- +
{error ?

{error}

: null} @@ -401,6 +384,27 @@ export function ProfileEditForm({ ); } +/** A plain page section: a bottom-bordered title over its fields. Matches the + * /settings page section style so the editor reads as a normal page. */ +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+
+

+ {title} +

+
+
{children}
+
+ ); +} + function AddEmailInline() { const { setUser } = useAuth(); const [email, setEmail] = useState("");