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:
+8
-55
@@ -8,20 +8,16 @@ import { StatsRow } from "@/components/profile/stats-row"
|
|||||||
import { ContributionList } from "@/components/profile/contribution-list"
|
import { ContributionList } from "@/components/profile/contribution-list"
|
||||||
import { SavedGamesGrid } from "@/components/saved-games/saved-games-grid"
|
import { SavedGamesGrid } from "@/components/saved-games/saved-games-grid"
|
||||||
import { Bookmark, Settings, Loader2, TrendingUp } from "lucide-react"
|
import { Bookmark, Settings, Loader2, TrendingUp } from "lucide-react"
|
||||||
import { SettingsProfileTab } from "@/components/profile/settings-profile-tab"
|
import { SettingsContainer } from "@/components/profile/settings-container"
|
||||||
import { SettingsSecurityTab } from "@/components/profile/settings-security-tab"
|
|
||||||
import { SettingsAccountsTab } from "@/components/profile/settings-accounts-tab"
|
|
||||||
import { motion } from "motion/react"
|
import { motion } from "motion/react"
|
||||||
import type { ContributionEntry } from "@/types/api"
|
import type { ContributionEntry } from "@/types/api"
|
||||||
|
|
||||||
type Tab = "overview" | "saved" | "settings"
|
type Tab = "overview" | "saved" | "settings"
|
||||||
type SettingsTab = "profile" | "security" | "accounts"
|
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { data: session, isPending: isSessionLoading } = useSession()
|
const { data: session, isPending: isSessionLoading } = useSession()
|
||||||
const [activeTab, setActiveTab] = useState<Tab>("overview")
|
const [activeTab, setActiveTab] = useState<Tab>("overview")
|
||||||
const [settingsSubTab, setSettingsSubTab] = useState<SettingsTab>("profile")
|
|
||||||
const [profile, setProfile] = useState<{
|
const [profile, setProfile] = useState<{
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -173,56 +169,13 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
{activeTab === "saved" && <SavedGamesGrid />}
|
{activeTab === "saved" && <SavedGamesGrid />}
|
||||||
|
|
||||||
{activeTab === "settings" && (
|
{activeTab === "settings" && profile && (
|
||||||
<div className='space-y-4'>
|
<SettingsContainer
|
||||||
{/* Settings sub-tabs */}
|
name={profile.name}
|
||||||
<div className='flex gap-1 border-b border-border'>
|
email={profile.email}
|
||||||
{[
|
role={profile.role}
|
||||||
{
|
createdAt={profile.createdAt}
|
||||||
id: "profile" as SettingsTab,
|
/>
|
||||||
label: "Profile",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "security" as SettingsTab,
|
|
||||||
label: "Security",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "accounts" as SettingsTab,
|
|
||||||
label: "Linked Accounts",
|
|
||||||
},
|
|
||||||
].map((subTab) => (
|
|
||||||
<button
|
|
||||||
key={subTab.id}
|
|
||||||
onClick={() =>
|
|
||||||
setSettingsSubTab(subTab.id)
|
|
||||||
}
|
|
||||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px cursor-pointer ${
|
|
||||||
settingsSubTab === subTab.id
|
|
||||||
? "border-primary text-primary"
|
|
||||||
: "border-transparent text-text/50 hover:text-text/70"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{subTab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings sub-tab content */}
|
|
||||||
{settingsSubTab === "profile" && (
|
|
||||||
<SettingsProfileTab
|
|
||||||
name={profile.name}
|
|
||||||
email={profile.email}
|
|
||||||
role={profile.role}
|
|
||||||
createdAt={profile.createdAt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{settingsSubTab === "security" && (
|
|
||||||
<SettingsSecurityTab />
|
|
||||||
)}
|
|
||||||
{settingsSubTab === "accounts" && (
|
|
||||||
<SettingsAccountsTab />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user