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,48 +1,88 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
type Playlist,
|
||||
type PlaylistSortField,
|
||||
type SortOrder,
|
||||
playlistsService,
|
||||
} from '@/lib/api/services/playlists'
|
||||
import { formatDuration } from '@/utils/format-duration'
|
||||
import {
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
Clock,
|
||||
Edit,
|
||||
Music,
|
||||
Play,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Search,
|
||||
SortAsc,
|
||||
SortDesc,
|
||||
User,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { playlistsService, type Playlist, type PlaylistSortField, type SortOrder } from '@/lib/api/services/playlists'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { AlertCircle, Search, SortAsc, SortDesc, X, RefreshCw, Music, User, Calendar, Clock, Plus, Play, Edit } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { formatDuration } from '@/utils/format-duration'
|
||||
|
||||
export function PlaylistsPage() {
|
||||
const navigate = useNavigate()
|
||||
const [playlists, setPlaylists] = useState<Playlist[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
|
||||
// Search and sorting state
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [sortBy, setSortBy] = useState<PlaylistSortField>('name')
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('asc')
|
||||
|
||||
|
||||
// Create playlist dialog state
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [newPlaylist, setNewPlaylist] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
genre: ''
|
||||
genre: '',
|
||||
})
|
||||
|
||||
// Debounce search query
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearchQuery(searchQuery)
|
||||
}, 300)
|
||||
|
||||
|
||||
return () => clearTimeout(handler)
|
||||
}, [searchQuery])
|
||||
|
||||
@@ -57,7 +97,8 @@ export function PlaylistsPage() {
|
||||
})
|
||||
setPlaylists(playlistData)
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch playlists'
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'Failed to fetch playlists'
|
||||
setError(errorMessage)
|
||||
toast.error(errorMessage)
|
||||
} finally {
|
||||
@@ -82,17 +123,18 @@ export function PlaylistsPage() {
|
||||
description: newPlaylist.description.trim() || undefined,
|
||||
genre: newPlaylist.genre.trim() || undefined,
|
||||
})
|
||||
|
||||
|
||||
toast.success(`Playlist "${newPlaylist.name}" created successfully`)
|
||||
|
||||
|
||||
// Reset form and close dialog
|
||||
setNewPlaylist({ name: '', description: '', genre: '' })
|
||||
setShowCreateDialog(false)
|
||||
|
||||
|
||||
// Refresh the playlists list
|
||||
fetchPlaylists()
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to create playlist'
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'Failed to create playlist'
|
||||
toast.error(errorMessage)
|
||||
} finally {
|
||||
setCreateLoading(false)
|
||||
@@ -108,11 +150,12 @@ export function PlaylistsPage() {
|
||||
try {
|
||||
await playlistsService.setCurrentPlaylist(playlist.id)
|
||||
toast.success(`"${playlist.name}" is now the current playlist`)
|
||||
|
||||
|
||||
// Refresh the playlists list to update the current status
|
||||
fetchPlaylists()
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to set current playlist'
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'Failed to set current playlist'
|
||||
toast.error(errorMessage)
|
||||
}
|
||||
}
|
||||
@@ -137,10 +180,12 @@ export function PlaylistsPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">Failed to load playlists</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
Failed to load playlists
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">{error}</p>
|
||||
<button
|
||||
onClick={fetchPlaylists}
|
||||
<button
|
||||
onClick={fetchPlaylists}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Try again
|
||||
@@ -157,7 +202,9 @@ export function PlaylistsPage() {
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">No playlists found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{searchQuery ? 'No playlists match your search criteria.' : 'No playlists are available.'}
|
||||
{searchQuery
|
||||
? 'No playlists match your search criteria.'
|
||||
: 'No playlists are available.'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
@@ -179,13 +226,15 @@ export function PlaylistsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{playlists.map((playlist) => (
|
||||
{playlists.map(playlist => (
|
||||
<TableRow key={playlist.id} className="hover:bg-muted/50">
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium truncate">{playlist.name}</div>
|
||||
<div className="font-medium truncate">
|
||||
{playlist.name}
|
||||
</div>
|
||||
{playlist.description && (
|
||||
<div className="text-sm text-muted-foreground truncate">
|
||||
{playlist.description}
|
||||
@@ -234,9 +283,7 @@ export function PlaylistsPage() {
|
||||
{playlist.is_current && (
|
||||
<Badge variant="default">Current</Badge>
|
||||
)}
|
||||
{playlist.is_main && (
|
||||
<Badge variant="outline">Main</Badge>
|
||||
)}
|
||||
{playlist.is_main && <Badge variant="outline">Main</Badge>}
|
||||
{!playlist.is_current && !playlist.is_main && (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
@@ -275,12 +322,9 @@ export function PlaylistsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Playlists' }
|
||||
]
|
||||
items: [{ label: 'Dashboard', href: '/' }, { label: 'Playlists' }],
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
@@ -303,7 +347,8 @@ export function PlaylistsPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Playlist</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new playlist to organize your sounds. Give it a name and optionally add a description and genre.
|
||||
Add a new playlist to organize your sounds. Give it a name
|
||||
and optionally add a description and genre.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
@@ -313,8 +358,13 @@ export function PlaylistsPage() {
|
||||
id="name"
|
||||
placeholder="My awesome playlist"
|
||||
value={newPlaylist.name}
|
||||
onChange={(e) => setNewPlaylist(prev => ({ ...prev, name: e.target.value }))}
|
||||
onKeyDown={(e) => {
|
||||
onChange={e =>
|
||||
setNewPlaylist(prev => ({
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
}))
|
||||
}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleCreatePlaylist()
|
||||
@@ -328,7 +378,12 @@ export function PlaylistsPage() {
|
||||
id="description"
|
||||
placeholder="A collection of my favorite sounds..."
|
||||
value={newPlaylist.description}
|
||||
onChange={(e) => setNewPlaylist(prev => ({ ...prev, description: e.target.value }))}
|
||||
onChange={e =>
|
||||
setNewPlaylist(prev => ({
|
||||
...prev,
|
||||
description: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
</div>
|
||||
@@ -338,15 +393,27 @@ export function PlaylistsPage() {
|
||||
id="genre"
|
||||
placeholder="Electronic, Rock, Comedy, etc."
|
||||
value={newPlaylist.genre}
|
||||
onChange={(e) => setNewPlaylist(prev => ({ ...prev, genre: e.target.value }))}
|
||||
onChange={e =>
|
||||
setNewPlaylist(prev => ({
|
||||
...prev,
|
||||
genre: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancelCreate} disabled={createLoading}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancelCreate}
|
||||
disabled={createLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreatePlaylist} disabled={createLoading || !newPlaylist.name.trim()}>
|
||||
<Button
|
||||
onClick={handleCreatePlaylist}
|
||||
disabled={createLoading || !newPlaylist.name.trim()}
|
||||
>
|
||||
{createLoading ? 'Creating...' : 'Create Playlist'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -359,7 +426,7 @@ export function PlaylistsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Search and Sort Controls */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<div className="flex-1">
|
||||
@@ -368,7 +435,7 @@ export function PlaylistsPage() {
|
||||
<Input
|
||||
placeholder="Search playlists..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="pl-9 pr-9"
|
||||
/>
|
||||
{searchQuery && (
|
||||
@@ -384,9 +451,12 @@ export function PlaylistsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Select value={sortBy} onValueChange={(value) => setSortBy(value as PlaylistSortField)}>
|
||||
<Select
|
||||
value={sortBy}
|
||||
onValueChange={value => setSortBy(value as PlaylistSortField)}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
@@ -399,16 +469,20 @@ export function PlaylistsPage() {
|
||||
<SelectItem value="updated_at">Updated Date</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||
title={sortOrder === 'asc' ? 'Sort ascending' : 'Sort descending'}
|
||||
>
|
||||
{sortOrder === 'asc' ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
|
||||
{sortOrder === 'asc' ? (
|
||||
<SortAsc className="h-4 w-4" />
|
||||
) : (
|
||||
<SortDesc className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
@@ -416,13 +490,15 @@ export function PlaylistsPage() {
|
||||
disabled={loading}
|
||||
title="Refresh playlists"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{renderContent()}
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user