268 lines
8.1 KiB
TypeScript
268 lines
8.1 KiB
TypeScript
import { AppLayout } from '@/components/AppLayout'
|
|
import { AppPagination } from '@/components/AppPagination'
|
|
import { CreatePlaylistDialog } from '@/components/playlists/CreatePlaylistDialog'
|
|
import { PlaylistsHeader } from '@/components/playlists/PlaylistsHeader'
|
|
import {
|
|
PlaylistsEmpty,
|
|
PlaylistsError,
|
|
PlaylistsLoading,
|
|
} from '@/components/playlists/PlaylistsLoadingStates'
|
|
import { PlaylistTable } from '@/components/playlists/PlaylistTable'
|
|
import {
|
|
type Playlist,
|
|
type PlaylistSortField,
|
|
type SortOrder,
|
|
playlistsService,
|
|
} from '@/lib/api/services/playlists'
|
|
import { favoritesService } from '@/lib/api/services/favorites'
|
|
import { useEffect, useState } from 'react'
|
|
import { useNavigate } from 'react-router'
|
|
import { toast } from 'sonner'
|
|
|
|
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')
|
|
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
|
|
|
|
// Pagination state
|
|
const [currentPage, setCurrentPage] = useState(1)
|
|
const [totalPages, setTotalPages] = useState(1)
|
|
const [totalCount, setTotalCount] = useState(0)
|
|
const [pageSize, setPageSize] = useState(10)
|
|
|
|
// Create playlist dialog state
|
|
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
|
const [createLoading, setCreateLoading] = useState(false)
|
|
const [newPlaylist, setNewPlaylist] = useState({
|
|
name: '',
|
|
description: '',
|
|
genre: '',
|
|
})
|
|
|
|
// Debounce search query
|
|
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery)
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedSearchQuery(searchQuery)
|
|
}, 300)
|
|
|
|
return () => clearTimeout(handler)
|
|
}, [searchQuery])
|
|
|
|
const fetchPlaylists = async () => {
|
|
try {
|
|
setLoading(true)
|
|
setError(null)
|
|
const response = await playlistsService.getPlaylists({
|
|
search: debouncedSearchQuery.trim() || undefined,
|
|
sort_by: sortBy,
|
|
sort_order: sortOrder,
|
|
favorites_only: showFavoritesOnly,
|
|
page: currentPage,
|
|
limit: pageSize,
|
|
})
|
|
setPlaylists(response.playlists)
|
|
setTotalPages(response.total_pages)
|
|
setTotalCount(response.total)
|
|
} catch (err) {
|
|
const errorMessage =
|
|
err instanceof Error ? err.message : 'Failed to fetch playlists'
|
|
setError(errorMessage)
|
|
toast.error(errorMessage)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchPlaylists()
|
|
}, [debouncedSearchQuery, sortBy, sortOrder, showFavoritesOnly, currentPage, pageSize])
|
|
|
|
// Reset to page 1 when filters change
|
|
useEffect(() => {
|
|
if (currentPage !== 1) {
|
|
setCurrentPage(1)
|
|
}
|
|
}, [debouncedSearchQuery, sortBy, sortOrder, showFavoritesOnly, pageSize])
|
|
|
|
const handlePageChange = (page: number) => {
|
|
setCurrentPage(page)
|
|
}
|
|
|
|
const handlePageSizeChange = (size: number) => {
|
|
setPageSize(size)
|
|
setCurrentPage(1) // Reset to first page when changing page size
|
|
}
|
|
|
|
const handleCreatePlaylist = async () => {
|
|
if (!newPlaylist.name.trim()) {
|
|
toast.error('Playlist name is required')
|
|
return
|
|
}
|
|
|
|
try {
|
|
setCreateLoading(true)
|
|
await playlistsService.createPlaylist({
|
|
name: newPlaylist.name.trim(),
|
|
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'
|
|
toast.error(errorMessage)
|
|
} finally {
|
|
setCreateLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleCancelCreate = () => {
|
|
setNewPlaylist({ name: '', description: '', genre: '' })
|
|
setShowCreateDialog(false)
|
|
}
|
|
|
|
const handleSetCurrent = async (playlist: Playlist) => {
|
|
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'
|
|
toast.error(errorMessage)
|
|
}
|
|
}
|
|
|
|
const handleEditPlaylist = (playlist: Playlist) => {
|
|
navigate(`/playlists/${playlist.id}/edit`)
|
|
}
|
|
|
|
const handleFavoriteToggle = async (playlistId: number, shouldFavorite: boolean) => {
|
|
try {
|
|
if (shouldFavorite) {
|
|
await favoritesService.addPlaylistFavorite(playlistId)
|
|
toast.success('Added to favorites')
|
|
} else {
|
|
await favoritesService.removePlaylistFavorite(playlistId)
|
|
toast.success('Removed from favorites')
|
|
}
|
|
|
|
// Update the playlist in the local state
|
|
setPlaylists(prevPlaylists =>
|
|
prevPlaylists.map(playlist =>
|
|
playlist.id === playlistId
|
|
? {
|
|
...playlist,
|
|
is_favorited: shouldFavorite,
|
|
favorite_count: shouldFavorite
|
|
? playlist.favorite_count + 1
|
|
: Math.max(0, playlist.favorite_count - 1),
|
|
}
|
|
: playlist,
|
|
),
|
|
)
|
|
} catch (error) {
|
|
toast.error(
|
|
`Failed to ${shouldFavorite ? 'add to' : 'remove from'} favorites: ${
|
|
error instanceof Error ? error.message : 'Unknown error'
|
|
}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
const renderContent = () => {
|
|
if (loading) {
|
|
return <PlaylistsLoading />
|
|
}
|
|
|
|
if (error) {
|
|
return <PlaylistsError error={error} onRetry={fetchPlaylists} />
|
|
}
|
|
|
|
if (playlists.length === 0) {
|
|
return <PlaylistsEmpty searchQuery={searchQuery} showFavoritesOnly={showFavoritesOnly} />
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<PlaylistTable
|
|
playlists={playlists}
|
|
onEdit={handleEditPlaylist}
|
|
onSetCurrent={handleSetCurrent}
|
|
onFavoriteToggle={handleFavoriteToggle}
|
|
/>
|
|
<AppPagination
|
|
currentPage={currentPage}
|
|
totalPages={totalPages}
|
|
totalCount={totalCount}
|
|
pageSize={pageSize}
|
|
onPageChange={handlePageChange}
|
|
onPageSizeChange={handlePageSizeChange}
|
|
itemName="playlists"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AppLayout
|
|
breadcrumb={{
|
|
items: [{ label: 'Dashboard', href: '/' }, { label: 'Playlists' }],
|
|
}}
|
|
>
|
|
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
|
<PlaylistsHeader
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
sortBy={sortBy}
|
|
onSortByChange={setSortBy}
|
|
sortOrder={sortOrder}
|
|
onSortOrderChange={setSortOrder}
|
|
onRefresh={fetchPlaylists}
|
|
onCreateClick={() => setShowCreateDialog(true)}
|
|
loading={loading}
|
|
error={error}
|
|
playlistCount={totalCount}
|
|
showFavoritesOnly={showFavoritesOnly}
|
|
onFavoritesToggle={setShowFavoritesOnly}
|
|
/>
|
|
|
|
<CreatePlaylistDialog
|
|
open={showCreateDialog}
|
|
onOpenChange={setShowCreateDialog}
|
|
loading={createLoading}
|
|
name={newPlaylist.name}
|
|
description={newPlaylist.description}
|
|
genre={newPlaylist.genre}
|
|
onNameChange={name => setNewPlaylist(prev => ({ ...prev, name }))}
|
|
onDescriptionChange={description => setNewPlaylist(prev => ({ ...prev, description }))}
|
|
onGenreChange={genre => setNewPlaylist(prev => ({ ...prev, genre }))}
|
|
onSubmit={handleCreatePlaylist}
|
|
onCancel={handleCancelCreate}
|
|
/>
|
|
|
|
{renderContent()}
|
|
</div>
|
|
</AppLayout>
|
|
)
|
|
}
|