feat: integrate Combobox component for timezone selection in CreateTaskDialog and AccountPage
This commit is contained in:
@@ -1,12 +1,5 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import { Combobox, type ComboboxOption } from '@/components/ui/combobox'
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from '@/components/ui/command'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -15,11 +8,6 @@ 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,
|
||||||
@@ -39,7 +27,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, Check, ChevronsUpDown, Loader2, Music, PlayCircle } from 'lucide-react'
|
import { CalendarPlus, Loader2, Music, PlayCircle } from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
interface CreateTaskDialogProps {
|
interface CreateTaskDialogProps {
|
||||||
@@ -62,10 +50,35 @@ export function CreateTaskDialog({
|
|||||||
}: CreateTaskDialogProps) {
|
}: CreateTaskDialogProps) {
|
||||||
const { timezone } = useLocale()
|
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<string, string>)
|
||||||
|
|
||||||
|
// 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<CreateScheduledTaskRequest>({
|
const [formData, setFormData] = useState<CreateScheduledTaskRequest>({
|
||||||
name: '',
|
name: '',
|
||||||
task_type: 'play_sound',
|
task_type: 'play_sound',
|
||||||
scheduled_at: '',
|
scheduled_at: getDefaultScheduledTime(),
|
||||||
timezone: timezone,
|
timezone: timezone,
|
||||||
parameters: {},
|
parameters: {},
|
||||||
recurrence_type: 'none',
|
recurrence_type: 'none',
|
||||||
@@ -89,10 +102,6 @@ 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(() => {
|
||||||
@@ -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: <Music className="h-4 w-4" />,
|
||||||
|
searchValue: `${sound.id}-${sound.name || sound.filename}`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const playlistOptions: ComboboxOption[] = playlists.map((playlist) => ({
|
||||||
|
value: playlist.id.toString(),
|
||||||
|
label: playlist.name,
|
||||||
|
icon: <PlayCircle className="h-4 w-4" />,
|
||||||
|
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) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -195,7 +228,7 @@ export function CreateTaskDialog({
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
task_type: 'play_sound',
|
task_type: 'play_sound',
|
||||||
scheduled_at: '',
|
scheduled_at: getDefaultScheduledTime(),
|
||||||
timezone: timezone,
|
timezone: timezone,
|
||||||
parameters: {},
|
parameters: {},
|
||||||
recurrence_type: 'none',
|
recurrence_type: 'none',
|
||||||
@@ -209,37 +242,9 @@ export function CreateTaskDialog({
|
|||||||
setSelectedPlaylistId('')
|
setSelectedPlaylistId('')
|
||||||
setPlayMode('continuous')
|
setPlayMode('continuous')
|
||||||
setShuffle(false)
|
setShuffle(false)
|
||||||
setSoundComboOpen(false)
|
|
||||||
setPlaylistComboOpen(false)
|
|
||||||
setTimezoneComboOpen(false)
|
|
||||||
onCancel()
|
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<string, string>)
|
|
||||||
|
|
||||||
// Return in datetime-local format (YYYY-MM-DDTHH:MM)
|
|
||||||
return `${partsObj.year}-${partsObj.month}-${partsObj.day}T${partsObj.hour}:${partsObj.minute}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
@@ -289,7 +294,7 @@ export function CreateTaskDialog({
|
|||||||
<Input
|
<Input
|
||||||
id="scheduled_at"
|
id="scheduled_at"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={formData.scheduled_at || getDefaultScheduledTime()}
|
value={formData.scheduled_at}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, scheduled_at: e.target.value }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, scheduled_at: e.target.value }))}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -297,61 +302,14 @@ export function CreateTaskDialog({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="timezone">Timezone</Label>
|
<Label htmlFor="timezone">Timezone</Label>
|
||||||
<Popover open={timezoneComboOpen} onOpenChange={setTimezoneComboOpen}>
|
<Combobox
|
||||||
<PopoverTrigger asChild>
|
value={formData.timezone}
|
||||||
<Button
|
onValueChange={(value) => setFormData(prev => ({ ...prev, timezone: value }))}
|
||||||
variant="outline"
|
options={timezoneOptions}
|
||||||
role="combobox"
|
placeholder="Select timezone..."
|
||||||
aria-expanded={timezoneComboOpen}
|
searchPlaceholder="Search timezone..."
|
||||||
className="w-full justify-between"
|
emptyMessage="No timezone found."
|
||||||
>
|
|
||||||
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -418,56 +376,16 @@ 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>
|
||||||
<Popover open={soundComboOpen} onOpenChange={setSoundComboOpen}>
|
<Combobox
|
||||||
<PopoverTrigger asChild>
|
value={selectedSoundId}
|
||||||
<Button
|
onValueChange={setSelectedSoundId}
|
||||||
variant="outline"
|
options={soundOptions}
|
||||||
role="combobox"
|
placeholder="Select a sound"
|
||||||
aria-expanded={soundComboOpen}
|
searchPlaceholder="Search sounds..."
|
||||||
className="w-full justify-between"
|
emptyMessage="No sound found."
|
||||||
disabled={loadingSounds}
|
loading={loadingSounds}
|
||||||
>
|
loadingMessage="Loading sounds..."
|
||||||
{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 && (
|
{parametersError && (
|
||||||
<p className="text-sm text-destructive">{parametersError}</p>
|
<p className="text-sm text-destructive">{parametersError}</p>
|
||||||
)}
|
)}
|
||||||
@@ -478,55 +396,16 @@ 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>
|
||||||
<Popover open={playlistComboOpen} onOpenChange={setPlaylistComboOpen}>
|
<Combobox
|
||||||
<PopoverTrigger asChild>
|
value={selectedPlaylistId}
|
||||||
<Button
|
onValueChange={setSelectedPlaylistId}
|
||||||
variant="outline"
|
options={playlistOptions}
|
||||||
role="combobox"
|
placeholder="Select a playlist"
|
||||||
aria-expanded={playlistComboOpen}
|
searchPlaceholder="Search playlists..."
|
||||||
className="w-full justify-between"
|
emptyMessage="No playlist found."
|
||||||
disabled={loadingPlaylists}
|
loading={loadingPlaylists}
|
||||||
>
|
loadingMessage="Loading playlists..."
|
||||||
{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>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
119
src/components/ui/combobox.tsx
Normal file
119
src/components/ui/combobox.tsx
Normal file
@@ -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 (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className={cn("w-full justify-between", className)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
>
|
||||||
|
{selectedOption ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedOption.icon}
|
||||||
|
<span>{selectedOption.label}</span>
|
||||||
|
</div>
|
||||||
|
) : loading ? (
|
||||||
|
loadingMessage
|
||||||
|
) : (
|
||||||
|
placeholder
|
||||||
|
)}
|
||||||
|
<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={searchPlaceholder} />
|
||||||
|
<CommandList
|
||||||
|
className="max-h-[200px] overflow-y-auto overscroll-contain"
|
||||||
|
style={{ touchAction: 'pan-y' }}
|
||||||
|
>
|
||||||
|
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option) => (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.searchValue || `${option.value}-${option.label}`}
|
||||||
|
onSelect={() => {
|
||||||
|
onValueChange(option.value)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{option.icon}
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</div>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
value === option.value ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { AppLayout } from '@/components/AppLayout'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Combobox, type ComboboxOption } from '@/components/ui/combobox'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -75,6 +76,13 @@ export function AccountPage() {
|
|||||||
const [providers, setProviders] = useState<UserProvider[]>([])
|
const [providers, setProviders] = useState<UserProvider[]>([])
|
||||||
const [providersLoading, setProvidersLoading] = useState(true)
|
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(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
setProfileName(user.name)
|
setProfileName(user.name)
|
||||||
@@ -390,21 +398,14 @@ export function AccountPage() {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Timezone</Label>
|
<Label>Timezone</Label>
|
||||||
<Select
|
<Combobox
|
||||||
value={timezone}
|
value={timezone}
|
||||||
onValueChange={setTimezone}
|
onValueChange={setTimezone}
|
||||||
>
|
options={timezoneOptions}
|
||||||
<SelectTrigger>
|
placeholder="Select timezone..."
|
||||||
<SelectValue />
|
searchPlaceholder="Search timezone..."
|
||||||
</SelectTrigger>
|
emptyMessage="No timezone found."
|
||||||
<SelectContent>
|
/>
|
||||||
{getSupportedTimezones().map((tz) => (
|
|
||||||
<SelectItem key={tz} value={tz}>
|
|
||||||
{tz.replace('_', ' ')}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Choose your timezone for date and time display
|
Choose your timezone for date and time display
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user