feat: implement Account page and ThemeProvider; refactor App component and sidebar navigation

This commit is contained in:
JSC
2025-06-29 22:01:13 +02:00
parent d0f8f13c86
commit e484251787
8 changed files with 755 additions and 231 deletions

View File

@@ -1,76 +1,86 @@
import { AppLayout } from '@/components/AppLayout' import { AppLayout } from '@/components/AppLayout'
import { ProtectedRoute } from '@/components/ProtectedRoute' import { ProtectedRoute } from '@/components/ProtectedRoute'
import { AuthProvider } from '@/contexts/AuthContext'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { AuthProvider } from '@/contexts/AuthContext'
import { AccountPage } from '@/pages/AccountPage'
import { ActivityPage } from '@/pages/ActivityPage' import { ActivityPage } from '@/pages/ActivityPage'
import { AdminUsersPage } from '@/pages/AdminUsersPage' import { AdminUsersPage } from '@/pages/AdminUsersPage'
import { DashboardPage } from '@/pages/DashboardPage' import { DashboardPage } from '@/pages/DashboardPage'
import { LoginPage } from '@/pages/LoginPage' import { LoginPage } from '@/pages/LoginPage'
import { RegisterPage } from '@/pages/RegisterPage' import { RegisterPage } from '@/pages/RegisterPage'
import { SettingsPage } from '@/pages/SettingsPage'
import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router' import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router'
import { ThemeProvider } from './components/ThemeProvider'
function App() { function App() {
return ( return (
<AuthProvider> <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<Router> <AuthProvider>
<Routes> <Router>
<Route path="/login" element={<LoginPage />} /> <Routes>
<Route path="/register" element={<RegisterPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Protected routes with layout */} {/* Protected routes with layout */}
<Route <Route
path="/dashboard" path="/dashboard"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AppLayout title="Dashboard" description="Welcome to your dashboard"> <AppLayout
<DashboardPage /> title="Dashboard"
</AppLayout> description="Welcome to your dashboard"
</ProtectedRoute> >
} <DashboardPage />
/> </AppLayout>
<Route </ProtectedRoute>
path="/activity" }
element={ />
<ProtectedRoute> <Route
<AppLayout title="Activity" description="View recent activity and logs"> path="/activity"
<ActivityPage /> element={
</AppLayout> <ProtectedRoute>
</ProtectedRoute> <AppLayout
} title="Activity"
/> description="View recent activity and logs"
<Route >
path="/settings" <ActivityPage />
element={ </AppLayout>
<ProtectedRoute> </ProtectedRoute>
<AppLayout title="Settings" description="Manage your account settings and preferences"> }
<SettingsPage /> />
</AppLayout> <Route
</ProtectedRoute> path="/account"
} element={
/> <ProtectedRoute>
<Route <AppLayout
path="/admin/users" title="Account"
element={ description="Manage your account settings and preferences"
<ProtectedRoute requireAdmin> >
<AppLayout <AccountPage />
title="User Management" </AppLayout>
description="Manage users and their permissions" </ProtectedRoute>
headerActions={ }
<Button>Add User</Button> />
} <Route
> path="/admin/users"
<AdminUsersPage /> element={
</AppLayout> <ProtectedRoute requireAdmin>
</ProtectedRoute> <AppLayout
} title="User Management"
/> description="Manage users and their permissions"
headerActions={<Button>Add User</Button>}
>
<AdminUsersPage />
</AppLayout>
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} /> <Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes> </Routes>
</Router> </Router>
</AuthProvider> </AuthProvider>
</ThemeProvider>
) )
} }

View File

@@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}

View File

@@ -1,4 +1,3 @@
import { Button } from '@/components/ui/button'
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -7,10 +6,10 @@ import {
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar' } from '@/components/ui/sidebar'
import { useSidebar } from '@/components/ui/sidebar'
import { useAuth } from '@/contexts/AuthContext' import { useAuth } from '@/contexts/AuthContext'
import { Activity, Home, LogOut, Settings, Users } from 'lucide-react' import { Activity, Home, Users } from 'lucide-react'
import { Link, useLocation } from 'react-router' import { Link, useLocation } from 'react-router'
import { NavUser } from './NavUser' import { NavUser } from './NavUser'
@@ -25,11 +24,6 @@ const navigationItems = [
href: '/activity', href: '/activity',
icon: Activity, icon: Activity,
}, },
{
title: 'Settings',
href: '/settings',
icon: Settings,
},
] ]
const adminNavigationItems = [ const adminNavigationItems = [

View File

@@ -16,6 +16,7 @@ import {
SidebarMenuItem, SidebarMenuItem,
useSidebar, useSidebar,
} from '../ui/sidebar' } from '../ui/sidebar'
import { Link } from 'react-router'
interface NavUserProps { interface NavUserProps {
user: User user: User
@@ -65,10 +66,10 @@ export function NavUser({ user, logout }: NavUserProps) {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/account"> <Link to="/account">
<UserIcon /> <UserIcon />
Account Account
</a> </Link>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />

View File

@@ -2,7 +2,7 @@
import * as React from "react" import * as React from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { cva, VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react" import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from "@/hooks/use-mobile"

595
src/pages/AccountPage.tsx Normal file
View File

@@ -0,0 +1,595 @@
import { useTheme } from '@/components/ThemeProvider'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useAuth } from '@/contexts/AuthContext'
import { useEffect, useRef, useState } from 'react'
export function AccountPage() {
const { user, refreshUser } = useAuth()
const { theme, setTheme } = useTheme()
const [isLoading, setIsLoading] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(true)
const [name, setName] = useState(user?.name || '')
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const [apiToken, setApiToken] = useState('')
const [showFullToken, setShowFullToken] = useState(false)
const tokenInputRef = useRef<HTMLInputElement>(null)
// Password management state
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [showPasswordForm, setShowPasswordForm] = useState(false)
// Refresh user data when component mounts (run only once)
useEffect(() => {
const refreshOnMount = async () => {
try {
setIsRefreshing(true)
await refreshUser()
} catch (error) {
console.error(
'Failed to refresh user data on account page load:',
error,
)
setError('Failed to load latest user data')
} finally {
setIsRefreshing(false)
}
}
refreshOnMount()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Intentionally empty dependency array to run only once on mount
// Update local name state when user data changes
useEffect(() => {
if (user?.name) {
setName(user.name)
}
}, [user?.name])
const handleUpdateProfile = async () => {
if (!name.trim()) {
setError('Name cannot be empty')
return
}
setIsLoading(true)
setError('')
setMessage('')
try {
const response = await fetch('http://localhost:5000/api/auth/profile', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ name: name.trim() }),
})
const data = await response.json()
if (response.ok) {
setMessage('Profile updated successfully!')
await refreshUser() // Refresh user data in context
} else {
setError(data.error || 'Failed to update profile')
}
} catch (error) {
console.error('Failed to update profile:', error)
setError('Failed to update profile')
} finally {
setIsLoading(false)
}
}
const handleRegenerateApiToken = async () => {
setIsLoading(true)
setError('')
setMessage('')
try {
const response = await fetch(
'http://localhost:5000/api/auth/regenerate-api-token',
{
method: 'POST',
credentials: 'include',
},
)
const data = await response.json()
if (response.ok) {
setApiToken(data.api_token)
setShowFullToken(true)
setMessage(
"New API token generated successfully! Copy it now - it won't be shown again.",
)
await refreshUser() // Refresh user data
// Auto-select the token text for easy copying
setTimeout(() => {
if (tokenInputRef.current) {
tokenInputRef.current.select()
tokenInputRef.current.focus()
}
}, 100)
} else {
setError(data.error || 'Failed to regenerate API token')
}
} catch (error) {
console.error('Failed to regenerate API token:', error)
setError('Failed to regenerate API token')
} finally {
setIsLoading(false)
}
}
const handleCopyToken = async () => {
if (!apiToken) return
try {
await navigator.clipboard.writeText(apiToken)
setMessage('API token copied to clipboard!')
} catch (error) {
console.error('Failed to copy token:', error)
setError('Failed to copy token to clipboard')
}
}
const handleHideToken = () => {
setApiToken('')
setShowFullToken(false)
setMessage('')
}
const handleThemeChange = (newTheme: string) => {
setTheme(newTheme as 'light' | 'dark' | 'system')
setMessage('Theme updated successfully!')
}
const handlePasswordUpdate = async () => {
if (!newPassword.trim()) {
setError('New password is required')
return
}
if (newPassword.length < 6) {
setError('Password must be at least 6 characters long')
return
}
if (newPassword !== confirmPassword) {
setError('New password and confirmation do not match')
return
}
// Check if user logged in via password (requires current password)
// If logged in via OAuth, they can change password without current password
const loggedInViaPassword = user?.provider === 'password'
if (loggedInViaPassword && !currentPassword.trim()) {
setError('Current password is required')
return
}
setIsLoading(true)
setError('')
setMessage('')
try {
const requestBody: any = { new_password: newPassword }
if (loggedInViaPassword) {
requestBody.current_password = currentPassword
}
const response = await fetch('http://localhost:5000/api/auth/password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(requestBody),
})
const data = await response.json()
if (response.ok) {
setMessage(loggedInViaPassword ? 'Password changed successfully!' : 'Password set successfully!')
setCurrentPassword('')
setNewPassword('')
setConfirmPassword('')
setShowPasswordForm(false)
await refreshUser() // Refresh user data
} else {
setError(data.error || 'Failed to update password')
}
} catch (error) {
console.error('Failed to update password:', error)
setError('Failed to update password')
} finally {
setIsLoading(false)
}
}
const resetPasswordForm = () => {
setCurrentPassword('')
setNewPassword('')
setConfirmPassword('')
setShowPasswordForm(false)
setError('')
}
if (!user) return null
if (isRefreshing) {
return (
<div className="flex items-center justify-center py-8">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">
Loading latest account information...
</p>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Success/Error Messages */}
{message && (
<div className="p-4 rounded-md bg-green-50 border border-green-200">
<p className="text-green-800 text-sm">{message}</p>
</div>
)}
{error && (
<div className="p-4 rounded-md bg-red-50 border border-red-200">
<p className="text-red-800 text-sm">{error}</p>
</div>
)}
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Update your personal information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Enter your full name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" value={user.email} disabled />
<p className="text-xs text-muted-foreground">
Email cannot be changed directly. Contact support if needed.
</p>
</div>
<Button onClick={handleUpdateProfile} disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save Changes'}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Password Management</CardTitle>
<CardDescription>
{user?.provider === 'password'
? 'Change your account password'
: 'Set or update your account password'
}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!showPasswordForm ? (
<Button onClick={() => setShowPasswordForm(true)} variant="outline">
{user?.provider === 'password'
? 'Change Password'
: 'Set/Update Password'
}
</Button>
) : (
<div className="space-y-4">
{user?.provider === 'password' && (
<div className="space-y-2">
<Label htmlFor="current-password">Current Password</Label>
<Input
id="current-password"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="Enter your current password"
/>
</div>
)}
{user?.provider !== 'password' && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-blue-800 text-sm">
💡 Since you're logged in via {user?.provider}, you can set or update your password without entering the current one.
</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<Input
id="new-password"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter your new password"
/>
<p className="text-xs text-muted-foreground">
Password must be at least 6 characters long
</p>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password">Confirm New Password</Label>
<Input
id="confirm-password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm your new password"
/>
</div>
<div className="flex gap-2">
<Button
onClick={handlePasswordUpdate}
disabled={isLoading}
>
{isLoading ? 'Updating...' : 'Update Password'}
</Button>
<Button
onClick={resetPasswordForm}
variant="outline"
disabled={isLoading}
>
Cancel
</Button>
</div>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Account Information</CardTitle>
<CardDescription>View your account details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>User ID</Label>
<Input value={user.id} disabled />
</div>
<div className="space-y-2">
<Label>Role</Label>
<Input value={user.role} disabled />
</div>
<div className="space-y-2">
<Label>Status</Label>
<Input value={user.is_active ? 'Active' : 'Disabled'} disabled />
</div>
<div className="space-y-2">
<Label>Authentication Methods</Label>
<div className="flex gap-2 flex-wrap">
{user.providers.map(provider => (
<span
key={provider}
className="px-2 py-1 bg-secondary rounded-md text-xs font-medium"
>
{provider}
</span>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Plan & Credits</CardTitle>
<CardDescription>
View your current plan and credit usage
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{user.plan ? (
<>
<div className="space-y-2">
<Label>Current Plan</Label>
<div className="flex items-center gap-2">
<Input value={user.plan.name} disabled />
<span
className={`px-2 py-1 rounded-md text-xs font-medium ${
user.plan.code === 'pro'
? 'bg-purple-100 text-purple-800'
: user.plan.code === 'premium'
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{user.plan.code.toUpperCase()}
</span>
</div>
</div>
<div className="space-y-2">
<Label>Plan Description</Label>
<p className="text-sm text-muted-foreground">
{user.plan.description}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Current Credits</Label>
<Input value={user.credits ?? 0} disabled />
</div>
<div className="space-y-2">
<Label>Plan Credit Limit</Label>
<Input value={user.plan.max_credits} disabled />
</div>
</div>
<div className="space-y-2">
<Label>Credit Usage</Label>
<div className="w-full bg-secondary rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{
width: `${Math.min(100, ((user.credits ?? 0) / user.plan.max_credits) * 100)}%`,
}}
/>
</div>
<p className="text-xs text-muted-foreground">
{user.credits ?? 0} of {user.plan.max_credits} credits
remaining
</p>
</div>
<div className="pt-2">
<Button variant="outline" disabled>
Upgrade Plan
</Button>
<p className="text-xs text-muted-foreground mt-1">
Plan upgrades coming soon
</p>
</div>
</>
) : (
<div className="text-center py-4">
<p className="text-muted-foreground">
No plan information available
</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>API Access</CardTitle>
<CardDescription>
Manage your API token for programmatic access
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>API Token</Label>
<div className="flex gap-2">
<Input
ref={tokenInputRef}
value={
showFullToken && apiToken
? apiToken
: user.api_token
? `${user.api_token.substring(0, 8)}...`
: 'No token'
}
readOnly
className="font-mono text-sm"
placeholder="Generate a new token to see it here"
/>
{showFullToken && apiToken && (
<>
<Button
onClick={handleCopyToken}
variant="outline"
size="sm"
>
Copy
</Button>
<Button
onClick={handleHideToken}
variant="outline"
size="sm"
>
Hide
</Button>
</>
)}
</div>
<p className="text-xs text-muted-foreground">
{showFullToken && apiToken
? "Copy this token now - it won't be displayed again once hidden."
: 'Use this token for API authentication. Keep it secure.'}
</p>
</div>
<Button
onClick={handleRegenerateApiToken}
disabled={isLoading}
variant="outline"
>
{isLoading ? 'Generating...' : 'Regenerate API Token'}
</Button>
{showFullToken && apiToken && (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-yellow-800 text-sm font-medium">
⚠️ Security Warning
</p>
<p className="text-yellow-700 text-xs mt-1">
Store this token securely. Anyone with this token can access
your account via API.
</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Preferences</CardTitle>
<CardDescription>Customize your experience</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Theme</Label>
<select
className="w-full h-9 px-3 rounded-md border border-input bg-background"
value={theme}
onChange={e => handleThemeChange(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
<p className="text-xs text-muted-foreground">
Choose your preferred theme or use system setting
</p>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -1,160 +0,0 @@
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { useAuth } from '@/contexts/AuthContext'
export function SettingsPage() {
const { user } = useAuth()
const [isLoading, setIsLoading] = useState(false)
const handleSave = async () => {
setIsLoading(true)
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
setIsLoading(false)
}
const handleRegenerateApiToken = async () => {
setIsLoading(true)
try {
const response = await fetch('/api/auth/regenerate-api-token', {
method: 'POST',
credentials: 'include',
})
if (response.ok) {
const data = await response.json()
alert(`New API token: ${data.api_token}`)
}
} catch (error) {
console.error('Failed to regenerate API token:', error)
} finally {
setIsLoading(false)
}
}
if (!user) return null
return (
<div className="space-y-6">
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Update your personal information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input id="name" defaultValue={user.name} />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" defaultValue={user.email} disabled />
<p className="text-xs text-muted-foreground">
Email cannot be changed directly. Contact support if needed.
</p>
</div>
<Button onClick={handleSave} disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save Changes'}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Account Information</CardTitle>
<CardDescription>View your account details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>User ID</Label>
<Input value={user.id} disabled />
</div>
<div className="space-y-2">
<Label>Role</Label>
<Input value={user.role} disabled />
</div>
<div className="space-y-2">
<Label>Status</Label>
<Input value={user.is_active ? 'Active' : 'Disabled'} disabled />
</div>
<div className="space-y-2">
<Label>Authentication Methods</Label>
<div className="flex gap-2 flex-wrap">
{user.providers.map((provider) => (
<span
key={provider}
className="px-2 py-1 bg-secondary rounded-md text-xs font-medium"
>
{provider}
</span>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>API Access</CardTitle>
<CardDescription>Manage your API token for programmatic access</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>API Token</Label>
<Input
value={user.api_token ? `${user.api_token.substring(0, 8)}...` : 'No token'}
disabled
/>
<p className="text-xs text-muted-foreground">
Use this token for API authentication. Keep it secure.
</p>
</div>
<Button onClick={handleRegenerateApiToken} disabled={isLoading} variant="outline">
{isLoading ? 'Generating...' : 'Regenerate API Token'}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Preferences</CardTitle>
<CardDescription>Customize your experience</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Theme</Label>
<select className="w-full h-9 px-3 rounded-md border border-input bg-background">
<option>Light</option>
<option>Dark</option>
<option>System</option>
</select>
</div>
<div className="space-y-2">
<Label>Language</Label>
<select className="w-full h-9 px-3 rounded-md border border-input bg-background">
<option>English</option>
<option>French</option>
<option>Spanish</option>
</select>
</div>
<Button onClick={handleSave} disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save Preferences'}
</Button>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -1,3 +1,12 @@
interface Plan {
id: number
code: string
name: string
description: string
credits: number
max_credits: number
}
interface User { interface User {
id: string id: string
email: string email: string
@@ -9,6 +18,8 @@ interface User {
providers: string[] providers: string[]
api_token?: string api_token?: string
api_token_expires_at?: string | null api_token_expires_at?: string | null
plan?: Plan
credits?: number
} }
interface AuthResponse { interface AuthResponse {
@@ -125,4 +136,4 @@ class AuthService {
} }
export const authService = new AuthService() export const authService = new AuthService()
export type { User } export type { User, Plan }