Refactor and enhance UI components across multiple pages
- Improved import organization and formatting in PlaylistsPage, RegisterPage, SoundsPage, SettingsPage, and UsersPage for better readability. - Added error handling and user feedback with toast notifications in SoundsPage and SettingsPage. - Enhanced user experience by implementing debounced search functionality in PlaylistsPage and SoundsPage. - Updated the layout and structure of forms in SettingsPage and UsersPage for better usability. - Improved accessibility and semantics by ensuring proper labeling and descriptions in forms. - Fixed minor bugs related to state management and API calls in various components.
This commit is contained in:
@@ -1,56 +1,72 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
User,
|
||||
Key,
|
||||
Shield,
|
||||
Palette,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Copy,
|
||||
Trash2,
|
||||
Github,
|
||||
Mail,
|
||||
CheckCircle2
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useTheme } from '@/hooks/use-theme'
|
||||
import { authService, type ApiTokenStatusResponse, type UserProvider } from '@/lib/api/services/auth'
|
||||
import {
|
||||
type ApiTokenStatusResponse,
|
||||
type UserProvider,
|
||||
authService,
|
||||
} from '@/lib/api/services/auth'
|
||||
import {
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Github,
|
||||
Key,
|
||||
Mail,
|
||||
Palette,
|
||||
Shield,
|
||||
Trash2,
|
||||
User,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function AccountPage() {
|
||||
const { user, setUser } = useAuth()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
|
||||
// Profile state
|
||||
const [profileName, setProfileName] = useState('')
|
||||
const [profileSaving, setProfileSaving] = useState(false)
|
||||
|
||||
|
||||
// Password state
|
||||
const [passwordData, setPasswordData] = useState({
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
confirm_password: '',
|
||||
})
|
||||
const [passwordSaving, setPasswordSaving] = useState(false)
|
||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
|
||||
const [showNewPassword, setShowNewPassword] = useState(false)
|
||||
|
||||
|
||||
// API Token state
|
||||
const [apiTokenStatus, setApiTokenStatus] = useState<ApiTokenStatusResponse | null>(null)
|
||||
const [apiTokenStatus, setApiTokenStatus] =
|
||||
useState<ApiTokenStatusResponse | null>(null)
|
||||
const [apiTokenLoading, setApiTokenLoading] = useState(true)
|
||||
const [generatedToken, setGeneratedToken] = useState('')
|
||||
const [showGeneratedToken, setShowGeneratedToken] = useState(false)
|
||||
const [tokenExpireDays, setTokenExpireDays] = useState('365')
|
||||
|
||||
|
||||
// Providers state
|
||||
const [providers, setProviders] = useState<UserProvider[]>([])
|
||||
const [providersLoading, setProvidersLoading] = useState(true)
|
||||
@@ -91,7 +107,9 @@ export function AccountPage() {
|
||||
|
||||
setProfileSaving(true)
|
||||
try {
|
||||
const updatedUser = await authService.updateProfile({ name: profileName.trim() })
|
||||
const updatedUser = await authService.updateProfile({
|
||||
name: profileName.trim(),
|
||||
})
|
||||
setUser?.(updatedUser)
|
||||
toast.success('Profile updated successfully')
|
||||
} catch (error) {
|
||||
@@ -104,14 +122,16 @@ export function AccountPage() {
|
||||
|
||||
const handlePasswordChange = async () => {
|
||||
// Check if user has password authentication from providers
|
||||
const hasPasswordProvider = providers.some(provider => provider.provider === 'password')
|
||||
|
||||
const hasPasswordProvider = providers.some(
|
||||
provider => provider.provider === 'password',
|
||||
)
|
||||
|
||||
// Validate required fields
|
||||
if (hasPasswordProvider && !passwordData.current_password) {
|
||||
toast.error('Current password is required')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!passwordData.new_password) {
|
||||
toast.error('New password is required')
|
||||
return
|
||||
@@ -130,12 +150,22 @@ export function AccountPage() {
|
||||
setPasswordSaving(true)
|
||||
try {
|
||||
await authService.changePassword({
|
||||
current_password: hasPasswordProvider ? passwordData.current_password : undefined,
|
||||
new_password: passwordData.new_password
|
||||
current_password: hasPasswordProvider
|
||||
? passwordData.current_password
|
||||
: undefined,
|
||||
new_password: passwordData.new_password,
|
||||
})
|
||||
setPasswordData({ current_password: '', new_password: '', confirm_password: '' })
|
||||
toast.success(hasPasswordProvider ? 'Password changed successfully' : 'Password set successfully')
|
||||
|
||||
setPasswordData({
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
toast.success(
|
||||
hasPasswordProvider
|
||||
? 'Password changed successfully'
|
||||
: 'Password set successfully',
|
||||
)
|
||||
|
||||
// Reload providers since password status might have changed
|
||||
loadProviders()
|
||||
} catch (error) {
|
||||
@@ -148,8 +178,8 @@ export function AccountPage() {
|
||||
|
||||
const handleGenerateApiToken = async () => {
|
||||
try {
|
||||
const response = await authService.generateApiToken({
|
||||
expires_days: parseInt(tokenExpireDays)
|
||||
const response = await authService.generateApiToken({
|
||||
expires_days: parseInt(tokenExpireDays),
|
||||
})
|
||||
setGeneratedToken(response.api_token)
|
||||
setShowGeneratedToken(true)
|
||||
@@ -192,12 +222,9 @@ export function AccountPage() {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<AppLayout
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Account' }
|
||||
]
|
||||
items: [{ label: 'Dashboard', href: '/' }, { label: 'Account' }],
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
@@ -223,12 +250,9 @@ export function AccountPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Account' }
|
||||
]
|
||||
items: [{ label: 'Dashboard', href: '/' }, { label: 'Account' }],
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 space-y-6">
|
||||
@@ -264,7 +288,7 @@ export function AccountPage() {
|
||||
<Input
|
||||
id="name"
|
||||
value={profileName}
|
||||
onChange={(e) => setProfileName(e.target.value)}
|
||||
onChange={e => setProfileName(e.target.value)}
|
||||
placeholder="Enter your display name"
|
||||
/>
|
||||
</div>
|
||||
@@ -272,15 +296,34 @@ export function AccountPage() {
|
||||
<div className="space-y-2">
|
||||
<Label>Account Details</Label>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div>Role: <Badge variant={user.role === 'admin' ? 'destructive' : 'secondary'}>{user.role}</Badge></div>
|
||||
<div>Credits: <span className="font-medium">{user.credits.toLocaleString()}</span></div>
|
||||
<div>Plan: <span className="font-medium">{user.plan.name}</span></div>
|
||||
<div>Member since: {new Date(user.created_at).toLocaleDateString()}</div>
|
||||
<div>
|
||||
Role:{' '}
|
||||
<Badge
|
||||
variant={
|
||||
user.role === 'admin' ? 'destructive' : 'secondary'
|
||||
}
|
||||
>
|
||||
{user.role}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
Credits:{' '}
|
||||
<span className="font-medium">
|
||||
{user.credits.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Plan: <span className="font-medium">{user.plan.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
Member since:{' '}
|
||||
{new Date(user.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleProfileSave}
|
||||
<Button
|
||||
onClick={handleProfileSave}
|
||||
disabled={profileSaving || profileName === user.name}
|
||||
className="w-full"
|
||||
>
|
||||
@@ -300,7 +343,12 @@ export function AccountPage() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Theme Preference</Label>
|
||||
<Select value={theme} onValueChange={(value: 'light' | 'dark' | 'system') => setTheme(value)}>
|
||||
<Select
|
||||
value={theme}
|
||||
onValueChange={(value: 'light' | 'dark' | 'system') =>
|
||||
setTheme(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -317,7 +365,8 @@ export function AccountPage() {
|
||||
|
||||
<div className="pt-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Current theme: <span className="font-medium capitalize">{theme}</span>
|
||||
Current theme:{' '}
|
||||
<span className="font-medium capitalize">{theme}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -341,7 +390,12 @@ export function AccountPage() {
|
||||
id="current-password"
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
value={passwordData.current_password}
|
||||
onChange={(e) => setPasswordData(prev => ({ ...prev, current_password: e.target.value }))}
|
||||
onChange={e =>
|
||||
setPasswordData(prev => ({
|
||||
...prev,
|
||||
current_password: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
<Button
|
||||
@@ -349,7 +403,9 @@ export function AccountPage() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
onClick={() =>
|
||||
setShowCurrentPassword(!showCurrentPassword)
|
||||
}
|
||||
>
|
||||
{showCurrentPassword ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
@@ -367,7 +423,12 @@ export function AccountPage() {
|
||||
id="new-password"
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
value={passwordData.new_password}
|
||||
onChange={(e) => setPasswordData(prev => ({ ...prev, new_password: e.target.value }))}
|
||||
onChange={e =>
|
||||
setPasswordData(prev => ({
|
||||
...prev,
|
||||
new_password: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
<Button
|
||||
@@ -387,22 +448,31 @@ export function AccountPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm-password">Confirm New Password</Label>
|
||||
<Label htmlFor="confirm-password">
|
||||
Confirm New Password
|
||||
</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
value={passwordData.confirm_password}
|
||||
onChange={(e) => setPasswordData(prev => ({ ...prev, confirm_password: e.target.value }))}
|
||||
onChange={e =>
|
||||
setPasswordData(prev => ({
|
||||
...prev,
|
||||
confirm_password: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePasswordChange}
|
||||
<Button
|
||||
onClick={handlePasswordChange}
|
||||
disabled={passwordSaving}
|
||||
className="w-full"
|
||||
>
|
||||
{passwordSaving ? 'Changing Password...' : 'Change Password'}
|
||||
{passwordSaving
|
||||
? 'Changing Password...'
|
||||
: 'Change Password'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -411,7 +481,8 @@ export function AccountPage() {
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
💡 <strong>Set up password authentication</strong>
|
||||
<br />
|
||||
You signed up with OAuth and don't have a password yet. Set one now to enable password login.
|
||||
You signed up with OAuth and don't have a password yet.
|
||||
Set one now to enable password login.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -422,7 +493,12 @@ export function AccountPage() {
|
||||
id="new-password"
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
value={passwordData.new_password}
|
||||
onChange={(e) => setPasswordData(prev => ({ ...prev, new_password: e.target.value }))}
|
||||
onChange={e =>
|
||||
setPasswordData(prev => ({
|
||||
...prev,
|
||||
new_password: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Enter your new password"
|
||||
/>
|
||||
<Button
|
||||
@@ -447,13 +523,18 @@ export function AccountPage() {
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
value={passwordData.confirm_password}
|
||||
onChange={(e) => setPasswordData(prev => ({ ...prev, confirm_password: e.target.value }))}
|
||||
onChange={e =>
|
||||
setPasswordData(prev => ({
|
||||
...prev,
|
||||
confirm_password: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Confirm your password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePasswordChange}
|
||||
<Button
|
||||
onClick={handlePasswordChange}
|
||||
disabled={passwordSaving}
|
||||
className="w-full"
|
||||
>
|
||||
@@ -487,11 +568,15 @@ export function AccountPage() {
|
||||
<span>API Token Active</span>
|
||||
{apiTokenStatus.expires_at && (
|
||||
<span className="text-muted-foreground">
|
||||
(Expires: {new Date(apiTokenStatus.expires_at).toLocaleDateString()})
|
||||
(Expires:{' '}
|
||||
{new Date(
|
||||
apiTokenStatus.expires_at,
|
||||
).toLocaleDateString()}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleDeleteApiToken}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
@@ -505,7 +590,10 @@ export function AccountPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="expire-days">Token Expiration</Label>
|
||||
<Select value={tokenExpireDays} onValueChange={setTokenExpireDays}>
|
||||
<Select
|
||||
value={tokenExpireDays}
|
||||
onValueChange={setTokenExpireDays}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -517,7 +605,10 @@ export function AccountPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button onClick={handleGenerateApiToken} className="w-full">
|
||||
<Button
|
||||
onClick={handleGenerateApiToken}
|
||||
className="w-full"
|
||||
>
|
||||
Generate API Token
|
||||
</Button>
|
||||
</div>
|
||||
@@ -526,7 +617,8 @@ export function AccountPage() {
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
API tokens allow external applications to access your account programmatically
|
||||
API tokens allow external applications to access your account
|
||||
programmatically
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -540,14 +632,18 @@ export function AccountPage() {
|
||||
Authentication Methods
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Available methods to sign in to your account. Use any of these to access your account.
|
||||
Available methods to sign in to your account. Use any of these to
|
||||
access your account.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{providersLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
@@ -556,30 +652,41 @@ export function AccountPage() {
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* All Authentication Providers from API */}
|
||||
{providers.map((provider) => {
|
||||
{providers.map(provider => {
|
||||
const isOAuth = provider.provider !== 'password'
|
||||
|
||||
|
||||
return (
|
||||
<div key={provider.provider} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div
|
||||
key={provider.provider}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{getProviderIcon(provider.provider)}
|
||||
<span className="font-medium">{provider.display_name}</span>
|
||||
<span className="font-medium">
|
||||
{provider.display_name}
|
||||
</span>
|
||||
<Badge variant="secondary">
|
||||
{isOAuth ? 'OAuth' : 'Password Authentication'}
|
||||
</Badge>
|
||||
{provider.connected_at && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Connected {new Date(provider.connected_at).toLocaleDateString()}
|
||||
Connected{' '}
|
||||
{new Date(
|
||||
provider.connected_at,
|
||||
).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-green-700 border-green-200 bg-green-50 dark:text-green-400 dark:border-green-800 dark:bg-green-900/20">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-green-700 border-green-200 bg-green-50 dark:text-green-400 dark:border-green-800 dark:bg-green-900/20"
|
||||
>
|
||||
Available
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
|
||||
{/* API Token Provider */}
|
||||
{apiTokenStatus?.has_token && (
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
@@ -589,11 +696,17 @@ export function AccountPage() {
|
||||
<Badge variant="secondary">API Access</Badge>
|
||||
{apiTokenStatus.expires_at && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Expires {new Date(apiTokenStatus.expires_at).toLocaleDateString()}
|
||||
Expires{' '}
|
||||
{new Date(
|
||||
apiTokenStatus.expires_at,
|
||||
).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-blue-700 border-blue-200 bg-blue-50 dark:text-blue-400 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-blue-700 border-blue-200 bg-blue-50 dark:text-blue-400 dark:border-blue-800 dark:bg-blue-900/20"
|
||||
>
|
||||
Available
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -637,11 +750,14 @@ export function AccountPage() {
|
||||
</div>
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-lg">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
⚠️ <strong>Important:</strong> This token will only be shown once.
|
||||
Copy it now and store it securely.
|
||||
⚠️ <strong>Important:</strong> This token will only be shown
|
||||
once. Copy it now and store it securely.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowGeneratedToken(false)} className="w-full">
|
||||
<Button
|
||||
onClick={() => setShowGeneratedToken(false)}
|
||||
className="w-full"
|
||||
>
|
||||
I've Saved My Token
|
||||
</Button>
|
||||
</div>
|
||||
@@ -649,4 +765,4 @@ export function AccountPage() {
|
||||
</Dialog>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user