"use client"; /** * /settings — the top-level user-settings screen. * * Lives outside the /u/me profile layout so it doesn't carry the * ProfileHeader + tabs that frame the profile views. It sits directly * under (app)/, which gives it the default site chrome (sidebar, header, * etc.) without the profile-specific frame on top. * * Sections are stacked plain — no per-section card chrome — so the page * reads as a single ordered list of preferences rather than a dashboard * of widgets. To add a new section, drop another
in the * `
` below. */ import { Suspense, useEffect, useState } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { AlertCircle, Check, ChevronLeft, ChevronRight, LogOut, } from "lucide-react"; import { auth, AuthError } from "@/lib/auth"; import { useAuth } from "@/components/auth/auth-provider"; import { AddEmailModal } from "@/components/auth/add-email-modal"; import { ChangePasswordModal } from "@/components/auth/change-password-modal"; import { ReadingSettings } from "@/components/reading-settings"; const BFF_BASE = process.env.NEXT_PUBLIC_BFF_URL || "http://localhost:8080"; export default function SettingsPage() { return (
Back to profile

Settings

{/* Suspense boundary required because LinkResult reads useSearchParams — Next.js bails out of SSR otherwise. */}
); } function SignOutSection() { const { logOut } = useAuth(); const router = useRouter(); const [busy, setBusy] = useState(false); async function onSignOut() { setBusy(true); try { await logOut(); router.push("/"); router.refresh(); } catch { // Logout errors are silent — the cookie is cleared on the server // either way and the next protected request will redirect anyway. setBusy(false); } } // Renders as a full-width clickable row matching LegalLink — same // padding, same hover, with LogOut taking the slot ChevronRight uses // on link rows. The whole row is the click target. return ( ); } function Section({ title, description, children, }: { title: string; description?: string; children: React.ReactNode; }) { return (

{title}

{description ? (

{description}

) : null}
{children}
); } // ─── Authentication ──────────────────────────────────────────────────── function AuthenticationSection() { const { user } = useAuth(); const [pwModalOpen, setPwModalOpen] = useState(false); const [emailModalOpen, setEmailModalOpen] = useState(false); const [linkedProviders, setLinkedProviders] = useState | null>( null, ); const next = "/settings"; // Fetch linked providers on mount. A null set means "not yet loaded" — we // render the rows as if nothing is connected until the answer comes back, // which is fine because the only thing that changes after load is the // "Connected" pill swapping in for "Connect". useEffect(() => { let cancelled = false; auth .linkedProviders() .then(({ providers }) => { if (!cancelled) setLinkedProviders(new Set(providers)); }) .catch(() => { if (!cancelled) setLinkedProviders(new Set()); }); return () => { cancelled = true; }; }, []); function startOAuth(provider: "facebook" | "google") { // /link/start — links the provider to the currently-authenticated // user (auth cookie travels on the top-level navigation). The login // page still uses /start for sign-in / sign-up. const u = new URL( `${BFF_BASE}/api/v1/auth/oauth/${provider}/link/start`, ); u.searchParams.set("next", next); return u.toString(); } function providerAction(provider: "google" | "facebook") { if (linkedProviders?.has(provider)) { return ; } return ( Connect ); } // Three contextual states for the email-related rows: // • user has no email → "Add your email" row only // • user has email, NOT verified → "Verify your email" row + // "Change your password" row // • user has email, verified → "Change your password" row const needsEmail = !user?.email; const needsVerify = !!user?.email && !user.email_verified_at; return ( <>
    } label="Connect your Google account" action={providerAction("google")} /> } label="Connect your Facebook account" action={providerAction("facebook")} /> {needsEmail ? ( setEmailModalOpen(true)} className="text-base font-medium text-primary hover:underline" > Add email } /> ) : ( <> {needsVerify ? : null} setPwModalOpen(true)} className="text-base font-medium text-primary hover:underline" > Change } /> )}
setPwModalOpen(false)} /> setEmailModalOpen(false)} /> ); } function VerifyEmailRow({ email }: { email: string }) { const [busy, setBusy] = useState(false); const [sent, setSent] = useState(false); const [error, setError] = useState(null); async function onResend() { setError(null); setBusy(true); try { await auth.resendVerification(); setSent(true); } catch (err) { setError( err instanceof AuthError ? err.message : "Could not send verification email.", ); } finally { setBusy(false); } } return ( Sent ) : ( ) } /> ); } function LinkResult() { const params = useSearchParams(); const router = useRouter(); const raw = params.get("link"); // Strip the param from the URL so a refresh doesn't re-show the banner. useEffect(() => { if (!raw) return; const next = new URLSearchParams(params.toString()); next.delete("link"); const q = next.toString(); router.replace(q ? `/settings?${q}` : "/settings"); }, [raw, params, router]); if (!raw) return null; // Format: ":" — `google:ok`, `facebook:claimed`, etc. const [provider, status] = raw.split(":", 2); const name = provider.charAt(0).toUpperCase() + provider.slice(1); if (status === "ok") { return (

Connected your {name} account.

); } if (status === "claimed") { return (

That {name} account is already linked to another Prabhupāda Says account.

); } return (

Couldn't connect your {name} account. Please try again.

); } function ConnectedPill() { return ( Connected ); } // ─── Legal ───────────────────────────────────────────────────────────── function LegalSection() { return (
); } function LegalLink({ href, label }: { href: string; label: string }) { return (
  • {label}
  • ); } // ─── Row helper ──────────────────────────────────────────────────────── function GoogleGlyph() { return ( ); } function FacebookGlyph() { return ( ); } function Row({ icon, label, description, action, }: { icon?: React.ReactNode; label: string; description?: string; action: React.ReactNode; }) { return (
  • {icon ? {icon} : null} {label}
    {description ? (

    {description}

    ) : null}
    {action}
  • ); }