feat: add search and sorting functionality to SoundsPage; update fetch logic to support new parameters
This commit is contained in:
@@ -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> = {}
|
|
||||||
|
|
||||||
if (params?.types) {
|
|
||||||
// Handle multiple types by building query string manually
|
|
||||||
const searchParams = new URLSearchParams()
|
const searchParams = new URLSearchParams()
|
||||||
|
|
||||||
|
// Handle multiple types
|
||||||
|
if (params?.types) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -52,6 +55,11 @@ export function SoundsPage() {
|
|||||||
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 {
|
||||||
await soundsService.playSound(sound.id)
|
await soundsService.playSound(sound.id)
|
||||||
@@ -76,12 +84,15 @@ 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,
|
||||||
|
sort_by: sortBy,
|
||||||
|
sort_order: sortOrder,
|
||||||
|
})
|
||||||
setSounds(sdbSounds)
|
setSounds(sdbSounds)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch sounds'
|
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch sounds'
|
||||||
@@ -92,8 +103,20 @@ export function SoundsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user