feat: add search and sorting functionality to SoundsPage; update fetch logic to support new parameters

This commit is contained in:
JSC
2025-08-10 15:33:32 +02:00
parent b47486aaf9
commit 3d16b36ee9
2 changed files with 136 additions and 29 deletions

View File

@@ -21,8 +21,16 @@ export interface Sound {
updated_at: string updated_at: string
} }
export type SoundSortField = 'name' | 'filename' | 'duration' | 'size' | 'type' | 'play_count' | 'created_at' | 'updated_at'
export type SortOrder = 'asc' | 'desc'
export interface GetSoundsParams { export interface GetSoundsParams {
types?: string[] types?: string[]
search?: string
sort_by?: SoundSortField
sort_order?: SortOrder
limit?: number
offset?: number
} }
export interface GetSoundsResponse { export interface GetSoundsResponse {
@@ -31,38 +39,52 @@ export interface GetSoundsResponse {
export class SoundsService { export class SoundsService {
/** /**
* Get all sounds with optional type filtering * Get all sounds with optional filtering, searching, and sorting
*/ */
async getSounds(params?: GetSoundsParams): Promise<Sound[]> { async getSounds(params?: GetSoundsParams): Promise<Sound[]> {
const queryParams: Record<string, string | number | boolean | undefined> = {} const searchParams = new URLSearchParams()
// Handle multiple types
if (params?.types) { if (params?.types) {
// Handle multiple types by building query string manually
const searchParams = new URLSearchParams()
params.types.forEach(type => { params.types.forEach(type => {
searchParams.append('types', type) searchParams.append('types', type)
}) })
const response = await apiClient.get<GetSoundsResponse>(`/api/v1/sounds/?${searchParams.toString()}`)
return response.sounds || []
} }
const response = await apiClient.get<GetSoundsResponse>('/api/v1/sounds/', { params: queryParams }) // Handle other parameters
if (params?.search) {
searchParams.append('search', params.search)
}
if (params?.sort_by) {
searchParams.append('sort_by', params.sort_by)
}
if (params?.sort_order) {
searchParams.append('sort_order', params.sort_order)
}
if (params?.limit) {
searchParams.append('limit', params.limit.toString())
}
if (params?.offset) {
searchParams.append('offset', params.offset.toString())
}
const url = searchParams.toString() ? `/api/v1/sounds/?${searchParams.toString()}` : '/api/v1/sounds/'
const response = await apiClient.get<GetSoundsResponse>(url)
return response.sounds || [] return response.sounds || []
} }
/** /**
* Get sounds of a specific type * Get sounds of a specific type
*/ */
async getSoundsByType(type: string): Promise<Sound[]> { async getSoundsByType(type: string, params?: Omit<GetSoundsParams, 'types'>): Promise<Sound[]> {
return this.getSounds({ types: [type] }) return this.getSounds({ ...params, types: [type] })
} }
/** /**
* Get SDB type sounds * Get SDB type sounds
*/ */
async getSDBSounds(): Promise<Sound[]> { async getSDBSounds(params?: Omit<GetSoundsParams, 'types'>): Promise<Sound[]> {
return this.getSoundsByType('SDB') return this.getSoundsByType('SDB', params)
} }
/** /**

View File

@@ -1,9 +1,12 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { AppLayout } from '@/components/AppLayout' import { AppLayout } from '@/components/AppLayout'
import { SoundCard } from '@/components/sounds/SoundCard' import { SoundCard } from '@/components/sounds/SoundCard'
import { soundsService, type Sound } from '@/lib/api/services/sounds' import { soundsService, type Sound, type SoundSortField, type SortOrder } from '@/lib/api/services/sounds'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { AlertCircle } from 'lucide-react' import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { AlertCircle, Search, SortAsc, SortDesc, X, RefreshCw } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useTheme } from '@/hooks/use-theme' import { useTheme } from '@/hooks/use-theme'
import { soundEvents, SOUND_EVENTS } from '@/lib/events' import { soundEvents, SOUND_EVENTS } from '@/lib/events'
@@ -51,6 +54,11 @@ export function SoundsPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [currentColors, setCurrentColors] = useState<string[]>(lightModeColors) const [currentColors, setCurrentColors] = useState<string[]>(lightModeColors)
// Search and sorting state
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState<SoundSortField>('name')
const [sortOrder, setSortOrder] = useState<SortOrder>('asc')
const handlePlaySound = async (sound: Sound) => { const handlePlaySound = async (sound: Sound) => {
try { try {
@@ -76,24 +84,39 @@ export function SoundsPage() {
return currentColors[index] return currentColors[index]
} }
useEffect(() => { const fetchSounds = async () => {
const fetchSounds = async () => { try {
try { setLoading(true)
setLoading(true) setError(null)
setError(null) const sdbSounds = await soundsService.getSDBSounds({
const sdbSounds = await soundsService.getSDBSounds() search: debouncedSearchQuery.trim() || undefined,
setSounds(sdbSounds) sort_by: sortBy,
} catch (err) { sort_order: sortOrder,
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch sounds' })
setError(errorMessage) setSounds(sdbSounds)
toast.error(errorMessage) } catch (err) {
} finally { const errorMessage = err instanceof Error ? err.message : 'Failed to fetch sounds'
setLoading(false) setError(errorMessage)
} toast.error(errorMessage)
} finally {
setLoading(false)
} }
}
// Debounce search query
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearchQuery(searchQuery)
}, 300)
return () => clearTimeout(handler)
}, [searchQuery])
useEffect(() => {
fetchSounds() fetchSounds()
}, []) }, [debouncedSearchQuery, sortBy, sortOrder])
// Listen for sound_played events and update play_count // Listen for sound_played events and update play_count
useEffect(() => { useEffect(() => {
@@ -189,6 +212,68 @@ export function SoundsPage() {
</div> </div>
)} )}
</div> </div>
{/* Search and Sort Controls */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search sounds..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-9"
/>
{searchQuery && (
<Button
variant="ghost"
size="sm"
onClick={() => setSearchQuery('')}
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
title="Clear search"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
</div>
<div className="flex gap-2">
<Select value={sortBy} onValueChange={(value) => setSortBy(value as SoundSortField)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="duration">Duration</SelectItem>
<SelectItem value="play_count">Play Count</SelectItem>
<SelectItem value="size">File Size</SelectItem>
<SelectItem value="created_at">Created Date</SelectItem>
<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" />}
</Button>
<Button
variant="outline"
size="icon"
onClick={fetchSounds}
disabled={loading}
title="Refresh sounds"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
{renderContent()} {renderContent()}
</div> </div>
</AppLayout> </AppLayout>