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