feat: create SettingsContainer component with shared auth-methods state

- Add SettingsContainer component with vertical sidebar navigation
- Fetch auth-methods from /api/user/me/auth-methods endpoint
- Pass authMethods props to Security and Accounts tabs
- Update profile page to use SettingsContainer instead of inline sub-tabs
- Use LinkIcon alias to avoid conflict with Next.js Link component
This commit is contained in:
2026-04-26 12:17:47 -05:00
parent beed7f02a8
commit 45c071692e
2 changed files with 122 additions and 55 deletions
+114
View File
@@ -0,0 +1,114 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { User, Shield, Link as LinkIcon } from "lucide-react"
import { SettingsProfileTab } from "@/components/profile/settings-profile-tab"
import { SettingsSecurityTab } from "@/components/profile/settings-security-tab"
import { SettingsAccountsTab } from "@/components/profile/settings-accounts-tab"
interface AuthMethods {
hasPassword: boolean
passkeyCount: number
oauthProviders: Array<{ providerId: string; id: string }>
totalAuthMethods: number
}
type SettingsSubTab = "profile" | "security" | "accounts"
const subTabs: { id: SettingsSubTab; label: string; icon: typeof User }[] = [
{ id: "profile", label: "Profile", icon: User },
{ id: "security", label: "Security", icon: Shield },
{ id: "accounts", label: "Linked Accounts", icon: LinkIcon },
]
interface SettingsContainerProps {
name: string
email: string
role: string | null
createdAt: string
}
export function SettingsContainer({ name, email, role, createdAt }: SettingsContainerProps) {
const [activeSubTab, setActiveSubTab] = useState<SettingsSubTab>("profile")
const [authMethods, setAuthMethods] = useState<AuthMethods | null>(null)
const [isLoadingAuthMethods, setIsLoadingAuthMethods] = useState(true)
const fetchRef = useRef(false)
useEffect(() => {
if (fetchRef.current) return
fetchRef.current = true
let cancelled = false
fetch("/api/user/me/auth-methods", { credentials: "include" })
.then(r => r.ok ? r.json() : null)
.then(data => {
if (cancelled) return
if (data) setAuthMethods(data)
setIsLoadingAuthMethods(false)
})
.catch(() => {
setIsLoadingAuthMethods(false)
})
return () => { cancelled = true }
}, [])
const refreshAuthMethods = async () => {
try {
const res = await fetch("/api/user/me/auth-methods", { credentials: "include" })
if (res.ok) setAuthMethods(await res.json())
} catch {
// silently fail
}
}
return (
<div className="flex flex-col md:flex-row gap-6">
{/* Sidebar Navigation */}
<nav className="md:w-48 shrink-0">
<div className="flex md:flex-col gap-1 overflow-x-auto md:overflow-visible pb-2 md:pb-0 md:border-r md:border-border">
{subTabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveSubTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors whitespace-nowrap rounded-lg md:rounded-none md:border-l-2 md:border-r-0 md:border-transparent cursor-pointer ${
activeSubTab === tab.id
? "bg-primary/10 text-primary md:border-l-primary md:bg-primary/10"
: "text-text/50 hover:text-text/70 hover:bg-text/5"
}`}
>
<tab.icon className="h-4 w-4 shrink-0" />
{tab.label}
</button>
))}
</div>
</nav>
{/* Content Area */}
<div className="flex-1 min-w-0">
{activeSubTab === "profile" && (
<SettingsProfileTab
name={name}
email={email}
role={role}
createdAt={createdAt}
/>
)}
{activeSubTab === "security" && (
<SettingsSecurityTab
authMethods={authMethods}
isLoadingAuthMethods={isLoadingAuthMethods}
onRefreshAuthMethods={refreshAuthMethods}
/>
)}
{activeSubTab === "accounts" && (
<SettingsAccountsTab
authMethods={authMethods}
isLoadingAuthMethods={isLoadingAuthMethods}
onRefreshAuthMethods={refreshAuthMethods}
/>
)}
</div>
</div>
)
}