feat: implement combobox for timezone, sound, and playlist selection in CreateTaskDialog

This commit is contained in:
JSC
2025-08-29 03:02:15 +02:00
parent 40b053c446
commit 70de6ad919

View File

@@ -1,4 +1,12 @@
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Dialog,
DialogContent,
@@ -7,6 +15,11 @@ import {
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import {
Select,
SelectContent,
@@ -26,7 +39,7 @@ import { soundsService } from '@/lib/api/services/sounds'
import { playlistsService } from '@/lib/api/services/playlists'
import { getSupportedTimezones } from '@/utils/locale'
import { useLocale } from '@/hooks/use-locale'
import { CalendarPlus, Loader2, Music, PlayCircle } from 'lucide-react'
import { CalendarPlus, Check, ChevronsUpDown, Loader2, Music, PlayCircle } from 'lucide-react'
import { useState, useEffect } from 'react'
interface CreateTaskDialogProps {
@@ -75,6 +88,11 @@ export function CreateTaskDialog({
const [playlists, setPlaylists] = useState<Array<{id: number, name: string}>>([])
const [loadingSounds, setLoadingSounds] = useState(false)
const [loadingPlaylists, setLoadingPlaylists] = useState(false)
// Combobox state
const [soundComboOpen, setSoundComboOpen] = useState(false)
const [playlistComboOpen, setPlaylistComboOpen] = useState(false)
const [timezoneComboOpen, setTimezoneComboOpen] = useState(false)
// Load sounds and playlists when dialog opens
useEffect(() => {
@@ -191,6 +209,9 @@ export function CreateTaskDialog({
setSelectedPlaylistId('')
setPlayMode('continuous')
setShuffle(false)
setSoundComboOpen(false)
setPlaylistComboOpen(false)
setTimezoneComboOpen(false)
onCancel()
}
@@ -276,22 +297,61 @@ export function CreateTaskDialog({
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<Select
value={formData.timezone}
onValueChange={(value) => setFormData(prev => ({ ...prev, timezone: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="UTC">UTC</SelectItem>
{getSupportedTimezones().map((tz) => (
<SelectItem key={tz} value={tz}>
{tz.replace('_', ' ')}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={timezoneComboOpen} onOpenChange={setTimezoneComboOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={timezoneComboOpen}
className="w-full justify-between"
>
{formData.timezone?.replace('_', ' ') || 'UTC'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}>
<Command onWheel={(e) => e.stopPropagation()}>
<CommandInput placeholder="Search timezone..." />
<CommandList className="max-h-[200px] overflow-y-auto overscroll-contain" style={{ touchAction: 'pan-y' }}>
<CommandEmpty>No timezone found.</CommandEmpty>
<CommandGroup>
<CommandItem
key="UTC"
value="UTC"
onSelect={() => {
setFormData(prev => ({ ...prev, timezone: 'UTC' }))
setTimezoneComboOpen(false)
}}
>
<span>UTC</span>
<Check
className={`ml-auto h-4 w-4 ${
formData.timezone === 'UTC' ? "opacity-100" : "opacity-0"
}`}
/>
</CommandItem>
{getSupportedTimezones().map((tz) => (
<CommandItem
key={tz}
value={tz.replace('_', ' ')}
onSelect={() => {
setFormData(prev => ({ ...prev, timezone: tz }))
setTimezoneComboOpen(false)
}}
>
<span>{tz.replace('_', ' ')}</span>
<Check
className={`ml-auto h-4 w-4 ${
formData.timezone === tz ? "opacity-100" : "opacity-0"
}`}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
@@ -358,24 +418,56 @@ export function CreateTaskDialog({
{formData.task_type === 'play_sound' && (
<div className="space-y-2">
<Label htmlFor="sound">Sound to Play</Label>
<Select
value={selectedSoundId}
onValueChange={setSelectedSoundId}
>
<SelectTrigger>
<SelectValue placeholder={loadingSounds ? "Loading sounds..." : "Select a sound"} />
</SelectTrigger>
<SelectContent>
{sounds.map((sound) => (
<SelectItem key={sound.id} value={sound.id.toString()}>
<div className="flex items-center gap-2">
<Music className="h-4 w-4" />
<span>{sound.name || sound.filename}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={soundComboOpen} onOpenChange={setSoundComboOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={soundComboOpen}
className="w-full justify-between"
disabled={loadingSounds}
>
{selectedSoundId
? sounds.find((sound) => sound.id.toString() === selectedSoundId)?.name ||
sounds.find((sound) => sound.id.toString() === selectedSoundId)?.filename ||
"Selected sound"
: loadingSounds
? "Loading sounds..."
: "Select a sound"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}>
<Command onWheel={(e) => e.stopPropagation()}>
<CommandInput placeholder="Search sounds..." />
<CommandList className="max-h-[200px] overflow-y-auto overscroll-contain" style={{ touchAction: 'pan-y' }}>
<CommandEmpty>No sound found.</CommandEmpty>
<CommandGroup>
{sounds.map((sound) => (
<CommandItem
key={sound.id}
value={`${sound.id}-${sound.name || sound.filename}`}
onSelect={() => {
setSelectedSoundId(sound.id.toString())
setSoundComboOpen(false)
}}
>
<div className="flex items-center gap-2">
<Music className="h-4 w-4" />
<span>{sound.name || sound.filename}</span>
</div>
<Check
className={`ml-auto h-4 w-4 ${
selectedSoundId === sound.id.toString() ? "opacity-100" : "opacity-0"
}`}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{parametersError && (
<p className="text-sm text-destructive">{parametersError}</p>
)}
@@ -386,24 +478,55 @@ export function CreateTaskDialog({
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="playlist">Playlist to Play</Label>
<Select
value={selectedPlaylistId}
onValueChange={setSelectedPlaylistId}
>
<SelectTrigger>
<SelectValue placeholder={loadingPlaylists ? "Loading playlists..." : "Select a playlist"} />
</SelectTrigger>
<SelectContent>
{playlists.map((playlist) => (
<SelectItem key={playlist.id} value={playlist.id.toString()}>
<div className="flex items-center gap-2">
<PlayCircle className="h-4 w-4" />
<span>{playlist.name}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={playlistComboOpen} onOpenChange={setPlaylistComboOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={playlistComboOpen}
className="w-full justify-between"
disabled={loadingPlaylists}
>
{selectedPlaylistId
? playlists.find((playlist) => playlist.id.toString() === selectedPlaylistId)?.name ||
"Selected playlist"
: loadingPlaylists
? "Loading playlists..."
: "Select a playlist"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}>
<Command onWheel={(e) => e.stopPropagation()}>
<CommandInput placeholder="Search playlists..." />
<CommandList className="max-h-[200px] overflow-y-auto overscroll-contain" style={{ touchAction: 'pan-y' }}>
<CommandEmpty>No playlist found.</CommandEmpty>
<CommandGroup>
{playlists.map((playlist) => (
<CommandItem
key={playlist.id}
value={`${playlist.id}-${playlist.name}`}
onSelect={() => {
setSelectedPlaylistId(playlist.id.toString())
setPlaylistComboOpen(false)
}}
>
<div className="flex items-center gap-2">
<PlayCircle className="h-4 w-4" />
<span>{playlist.name}</span>
</div>
<Check
className={`ml-auto h-4 w-4 ${
selectedPlaylistId === playlist.id.toString() ? "opacity-100" : "opacity-0"
}`}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="grid grid-cols-2 gap-4">