From 197e13c0efb614b7c7b1c815442e8c8013ba4bd0 Mon Sep 17 00:00:00 2001
From: gosangam
Date: Mon, 15 Jun 2026 23:54:34 +0530
Subject: [PATCH] feat: add edit profile page
---
app/(app)/settings/profile/page.tsx | 64 +++++++++++
app/(app)/u/me/layout.tsx | 30 ++---
app/(app)/u/me/page.tsx | 31 +----
components/profile/profile-edit-form.tsx | 140 ++++++++++++-----------
4 files changed, 146 insertions(+), 119 deletions(-)
create mode 100644 app/(app)/settings/profile/page.tsx
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 (
-