Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"react": "^19.1.0",
"react-aria-components": "^1.14.0",
"react-day-picker": "^9.13.0",
"react-dom": "^19.1.0",
"react-router": "^7.11.0",
"sonner": "^2.0.7",
Expand Down
140 changes: 140 additions & 0 deletions src/components/DateTimePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Calendar as CalendarIcon } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';

interface DateTimePickerProps {
date: Date | undefined;
setDate: (date: Date) => void;
placeholder: string;
}

export function DateTimePicker(props: DateTimePickerProps) {
const { date, setDate, placeholder } = props;

const hours = Array.from({ length: 12 }, (_, i) => i + 1);
const handleDateSelect = (selectedDate: Date | undefined) => {
if (selectedDate) {
setDate(selectedDate);
}
};

const handleTimeChange = (
type: 'hour' | 'minute' | 'ampm',
value: string
) => {
if (date) {
const newDate = new Date(date);
if (type === 'hour') {
newDate.setHours(
(parseInt(value) % 12) + (newDate.getHours() >= 12 ? 12 : 0)
);
} else if (type === 'minute') {
newDate.setMinutes(parseInt(value));
} else if (type === 'ampm') {
const currentHours = newDate.getHours();
newDate.setHours(
value === 'PM' ? currentHours + 12 : currentHours - 12
);
}
setDate(newDate);
}
};

return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
'w-full justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date
? date.toLocaleString('ko-KR', {
dateStyle: 'medium',
timeStyle: 'short',
})
: placeholder}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<div className="sm:flex">
<Calendar mode="single" selected={date} onSelect={handleDateSelect} />
<div className="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
<ScrollArea className="w-64 sm:w-auto">
<div className="flex sm:flex-col p-2">
{hours.reverse().map((hour) => (
<Button
key={hour}
size="icon"
variant={
date && date.getHours() % 12 === hour % 12
? 'default'
: 'ghost'
}
className="sm:w-full shrink-0 aspect-square"
onClick={() => handleTimeChange('hour', hour.toString())}
>
{hour}
</Button>
))}
</div>
<ScrollBar orientation="horizontal" className="sm:hidden" />
</ScrollArea>
<ScrollArea className="w-64 sm:w-auto">
<div className="flex sm:flex-col p-2">
{Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (
<Button
key={minute}
size="icon"
variant={
date && date.getMinutes() === minute ? 'default' : 'ghost'
}
className="sm:w-full shrink-0 aspect-square"
onClick={() =>
handleTimeChange('minute', minute.toString())
}
>
{minute}
</Button>
))}
</div>
<ScrollBar orientation="horizontal" className="sm:hidden" />
</ScrollArea>
<ScrollArea className="">
<div className="flex sm:flex-col p-2">
{['AM', 'PM'].map((ampm) => (
<Button
key={ampm}
size="icon"
variant={
date &&
((ampm === 'AM' && date.getHours() < 12) ||
(ampm === 'PM' && date.getHours() >= 12))
? 'default'
: 'ghost'
}
className="sm:w-full shrink-0 aspect-square"
onClick={() => handleTimeChange('ampm', ampm)}
>
{ampm}
</Button>
))}
</div>
</ScrollArea>
</div>
</div>
</PopoverContent>
</Popover>
);
}
36 changes: 36 additions & 0 deletions src/components/InputWithPlusMinusButtton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MinusIcon, PlusIcon } from 'lucide-react';

import { Button, Group, Input, NumberField } from 'react-aria-components';

export function InputWithPlusMinusButtons() {
return (
<NumberField
aria-label="Quantity"
defaultValue={4}
minValue={1}
className="w-full max-w-xs space-y-2"
>
<Group className="dark:bg-input/30 border-input data-focus-within:border-ring data-focus-within:ring-ring/50 data-focus-within:has-aria-invalid:ring-destructive/20 dark:data-focus-within:has-aria-invalid:ring-destructive/40 data-focus-within:has-aria-invalid:border-destructive relative inline-flex h-9 w-full min-w-0 items-center overflow-hidden rounded-md border bg-transparent text-base whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 data-focus-within:ring-[3px] md:text-sm">
<Button
slot="decrement"
className="border-input bg-background text-muted-foreground hover:bg-accent hover:text-foreground -ms-px flex aspect-square h-[inherit] items-center justify-center rounded-l-md border text-sm transition-[color,box-shadow] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
>
<MinusIcon />

<span className="sr-only">Decrement</span>
</Button>

<Input className="selection:bg-primary selection:text-primary-foreground w-full grow px-3 py-2 text-center tabular-nums outline-none" />

<Button
slot="increment"
className="border-input bg-background text-muted-foreground hover:bg-accent hover:text-foreground -me-px flex aspect-square h-[inherit] items-center justify-center rounded-r-md border text-sm transition-[color,box-shadow] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
>
<PlusIcon />

<span className="sr-only">Increment</span>
</Button>
</Group>
</NumberField>
);
}
Loading