From 851738f04ff309dc6b70347809db07d31f274e13 Mon Sep 17 00:00:00 2001 From: JSC Date: Fri, 29 Aug 2025 03:33:38 +0200 Subject: [PATCH] feat: integrate Combobox component for timezone selection in CreateTaskDialog and AccountPage --- .../schedulers/CreateTaskDialog.tsx | 285 +++++------------- src/components/ui/combobox.tsx | 119 ++++++++ src/pages/AccountPage.tsx | 27 +- 3 files changed, 215 insertions(+), 216 deletions(-) create mode 100644 src/components/ui/combobox.tsx diff --git a/src/components/schedulers/CreateTaskDialog.tsx b/src/components/schedulers/CreateTaskDialog.tsx index 4fdab76..be3ccdf 100644 --- a/src/components/schedulers/CreateTaskDialog.tsx +++ b/src/components/schedulers/CreateTaskDialog.tsx @@ -1,12 +1,5 @@ import { Button } from '@/components/ui/button' -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from '@/components/ui/command' +import { Combobox, type ComboboxOption } from '@/components/ui/combobox' import { Dialog, DialogContent, @@ -15,11 +8,6 @@ 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, @@ -39,7 +27,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, Check, ChevronsUpDown, Loader2, Music, PlayCircle } from 'lucide-react' +import { CalendarPlus, Loader2, Music, PlayCircle } from 'lucide-react' import { useState, useEffect } from 'react' interface CreateTaskDialogProps { @@ -62,10 +50,35 @@ export function CreateTaskDialog({ }: CreateTaskDialogProps) { const { timezone } = useLocale() + const getDefaultScheduledTime = () => { + const now = new Date() + now.setMinutes(now.getMinutes() + 10) // Default to 10 minutes from now + + // Format the time in the user's timezone + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }) + + const parts = formatter.formatToParts(now) + const partsObj = parts.reduce((acc, part) => { + acc[part.type] = part.value + return acc + }, {} as Record) + + // Return in datetime-local format (YYYY-MM-DDTHH:MM) + return `${partsObj.year}-${partsObj.month}-${partsObj.day}T${partsObj.hour}:${partsObj.minute}` + } + const [formData, setFormData] = useState({ name: '', task_type: 'play_sound', - scheduled_at: '', + scheduled_at: getDefaultScheduledTime(), timezone: timezone, parameters: {}, recurrence_type: 'none', @@ -89,10 +102,6 @@ export function CreateTaskDialog({ 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(() => { @@ -128,6 +137,30 @@ export function CreateTaskDialog({ } } + // Prepare options for comboboxes + const soundOptions: ComboboxOption[] = sounds.map((sound) => ({ + value: sound.id.toString(), + label: sound.name || sound.filename, + icon: , + searchValue: `${sound.id}-${sound.name || sound.filename}` + })) + + const playlistOptions: ComboboxOption[] = playlists.map((playlist) => ({ + value: playlist.id.toString(), + label: playlist.name, + icon: , + searchValue: `${playlist.id}-${playlist.name}` + })) + + const timezoneOptions: ComboboxOption[] = [ + { value: 'UTC', label: 'UTC' }, + ...getSupportedTimezones().map((tz) => ({ + value: tz, + label: tz.replace('_', ' '), + searchValue: tz.replace('_', ' ') + })) + ] + const handleSubmit = (e: React.FormEvent) => { e.preventDefault() @@ -195,7 +228,7 @@ export function CreateTaskDialog({ setFormData({ name: '', task_type: 'play_sound', - scheduled_at: '', + scheduled_at: getDefaultScheduledTime(), timezone: timezone, parameters: {}, recurrence_type: 'none', @@ -209,37 +242,9 @@ export function CreateTaskDialog({ setSelectedPlaylistId('') setPlayMode('continuous') setShuffle(false) - setSoundComboOpen(false) - setPlaylistComboOpen(false) - setTimezoneComboOpen(false) onCancel() } - const getDefaultScheduledTime = () => { - const now = new Date() - now.setMinutes(now.getMinutes() + 10) // Default to 10 minutes from now - - // Format the time in the user's timezone - const formatter = new Intl.DateTimeFormat('en-US', { - timeZone: timezone, - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false, - }) - - const parts = formatter.formatToParts(now) - const partsObj = parts.reduce((acc, part) => { - acc[part.type] = part.value - return acc - }, {} as Record) - - // Return in datetime-local format (YYYY-MM-DDTHH:MM) - return `${partsObj.year}-${partsObj.month}-${partsObj.day}T${partsObj.hour}:${partsObj.minute}` - } - return ( @@ -289,7 +294,7 @@ export function CreateTaskDialog({ setFormData(prev => ({ ...prev, scheduled_at: e.target.value }))} required /> @@ -297,61 +302,14 @@ export function CreateTaskDialog({
- - - - - - e.stopPropagation()}> - - - No timezone found. - - { - setFormData(prev => ({ ...prev, timezone: 'UTC' })) - setTimezoneComboOpen(false) - }} - > - UTC - - - {getSupportedTimezones().map((tz) => ( - { - setFormData(prev => ({ ...prev, timezone: tz })) - setTimezoneComboOpen(false) - }} - > - {tz.replace('_', ' ')} - - - ))} - - - - - + setFormData(prev => ({ ...prev, timezone: value }))} + options={timezoneOptions} + placeholder="Select timezone..." + searchPlaceholder="Search timezone..." + emptyMessage="No timezone found." + />
@@ -418,56 +376,16 @@ export function CreateTaskDialog({ {formData.task_type === 'play_sound' && (
- - - - - - e.stopPropagation()}> - - - No sound found. - - {sounds.map((sound) => ( - { - setSelectedSoundId(sound.id.toString()) - setSoundComboOpen(false) - }} - > -
- - {sound.name || sound.filename} -
- -
- ))} -
-
-
-
-
+ {parametersError && (

{parametersError}

)} @@ -478,55 +396,16 @@ export function CreateTaskDialog({
- - - - - - e.stopPropagation()}> - - - No playlist found. - - {playlists.map((playlist) => ( - { - setSelectedPlaylistId(playlist.id.toString()) - setPlaylistComboOpen(false) - }} - > -
- - {playlist.name} -
- -
- ))} -
-
-
-
-
+
diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx new file mode 100644 index 0000000..d8f8b4f --- /dev/null +++ b/src/components/ui/combobox.tsx @@ -0,0 +1,119 @@ +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Check, ChevronsUpDown } from "lucide-react" +import { cn } from "@/lib/utils" + +export interface ComboboxOption { + value: string + label: string + icon?: React.ReactNode + searchValue?: string +} + +interface ComboboxProps { + value?: string + onValueChange: (value: string) => void + options: ComboboxOption[] + placeholder?: string + searchPlaceholder?: string + emptyMessage?: string + disabled?: boolean + loading?: boolean + loadingMessage?: string + className?: string +} + +export function Combobox({ + value, + onValueChange, + options, + placeholder = "Select option...", + searchPlaceholder = "Search options...", + emptyMessage = "No option found.", + disabled = false, + loading = false, + loadingMessage = "Loading...", + className, +}: ComboboxProps) { + const [open, setOpen] = React.useState(false) + + const selectedOption = options.find((option) => option.value === value) + + return ( + + + + + + e.stopPropagation()}> + + + {emptyMessage} + + {options.map((option) => ( + { + onValueChange(option.value) + setOpen(false) + }} + > +
+ {option.icon} + {option.label} +
+ +
+ ))} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx index 712ed59..2f33dea 100644 --- a/src/pages/AccountPage.tsx +++ b/src/pages/AccountPage.tsx @@ -2,6 +2,7 @@ import { AppLayout } from '@/components/AppLayout' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Combobox, type ComboboxOption } from '@/components/ui/combobox' import { Dialog, DialogContent, @@ -75,6 +76,13 @@ export function AccountPage() { const [providers, setProviders] = useState([]) const [providersLoading, setProvidersLoading] = useState(true) + // Prepare timezone options for combobox + const timezoneOptions: ComboboxOption[] = getSupportedTimezones().map((tz) => ({ + value: tz, + label: tz.replace('_', ' '), + searchValue: tz.replace('_', ' ') + })) + useEffect(() => { if (user) { setProfileName(user.name) @@ -390,21 +398,14 @@ export function AccountPage() {
- + options={timezoneOptions} + placeholder="Select timezone..." + searchPlaceholder="Search timezone..." + emptyMessage="No timezone found." + />

Choose your timezone for date and time display