feat: implement Account page and ThemeProvider; refactor App component and sidebar navigation
This commit is contained in:
595
src/pages/AccountPage.tsx
Normal file
595
src/pages/AccountPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user