Refactor and enhance UI components across multiple pages
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped

- 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:
JSC
2025-08-14 23:51:47 +02:00
parent 8358aa16aa
commit 4e50e7e79d
53 changed files with 2477 additions and 1520 deletions

View File

@@ -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>
)
}
}