diff --git a/packages/web/src/components/board/poll-editor.tsx b/packages/web/src/components/board/poll-editor.tsx index 8ae5324..0d9f7ab 100644 --- a/packages/web/src/components/board/poll-editor.tsx +++ b/packages/web/src/components/board/poll-editor.tsx @@ -2,16 +2,12 @@ import { useState } from 'react'; import { toast } from 'sonner'; -import { Plus, Trash2, BarChart3, Calendar, Clock, Edit2, Check, Lock } from 'lucide-react'; +import { BarChart3, Calendar, Check, Clock, Edit2, Lock, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Calendar as CalendarComponent } from '@/components/ui/calendar'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { format } from 'date-fns'; import { ko } from 'date-fns/locale'; @@ -60,14 +56,9 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { const [dateOptions, setDateOptions] = useState([]); const [datePickerOpen, setDatePickerOpen] = useState(false); const [editingIndex, setEditingIndex] = useState(null); + const [formOpen, setFormOpen] = useState(false); const handleAddPoll = () => { - // Check if poll already exists - if (polls.length >= 1) { - toast.error('게시글당 투표는 1개만 생성할 수 있습니다.'); - return; - } - // Validation if (!newPoll.question?.trim()) { toast.error('질문을 입력해주세요.'); @@ -89,7 +80,7 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { // Format dates as strings finalOptions = dateOptions .sort((a, b) => a.getTime() - b.getTime()) - .map(date => format(date, 'yyyy-MM-dd')); + .map((date) => format(date, 'yyyy-MM-dd')); } else { const validOptions = options.filter((opt) => opt.trim()); if (validOptions.length < 2) { @@ -125,6 +116,7 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { }); setOptions(['', '']); setDateOptions([]); + setFormOpen(false); }; const handleRemovePoll = (index: number) => { @@ -145,12 +137,14 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { // Set options based on poll type if (poll.pollType === 'date') { - setDateOptions(poll.options.map(dateStr => { - const parts = dateStr.split('-').map(Number); - const [year, month, day] = parts; - if (!year || !month || !day) return new Date(); - return new Date(year, month - 1, day); - })); + setDateOptions( + poll.options.map((dateStr) => { + const parts = dateStr.split('-').map(Number); + const [year, month, day] = parts; + if (!year || !month || !day) return new Date(); + return new Date(year, month - 1, day); + }) + ); } else { setOptions(poll.options); } @@ -181,7 +175,7 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { } finalOptions = dateOptions .sort((a, b) => a.getTime() - b.getTime()) - .map(date => format(date, 'yyyy-MM-dd')); + .map((date) => format(date, 'yyyy-MM-dd')); } else { const validOptions = options.filter((opt) => opt.trim()); if (validOptions.length < 2) { @@ -218,6 +212,7 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { setOptions(['', '']); setDateOptions([]); setEditingIndex(null); + setFormOpen(false); }; const handleCancelEdit = () => { @@ -230,6 +225,7 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { setOptions(['', '']); setDateOptions([]); setEditingIndex(null); + setFormOpen(false); }; const handleOptionChange = (index: number, value: string) => { @@ -255,7 +251,7 @@ export function PollEditor({ polls, onPollsChange }: PollEditorProps) { {/* Existing polls */} {polls.length > 0 && editingIndex === null && (
- + {polls.map((poll, index) => (
))} + + {/* Add more polls button */} + {!formOpen && ( + + )}
)} - {/* New poll form - show if no poll exists OR editing */} - {(polls.length === 0 || editingIndex !== null) && ( + {/* New poll form - show if no poll exists, editing, or form opened */} + {(polls.length === 0 || editingIndex !== null || formOpen) && (
-
-
- -
- - {/* Question */} -
- - setNewPoll({ ...newPoll, question: e.target.value })} - className="text-sm" - /> -
+
+
+ +
- {/* Poll type */} -
- -
- - + {/* Question */} +
+ + setNewPoll({ ...newPoll, question: e.target.value })} + className="text-sm" + />
-
- {/* Poll options */} -
- -
- - + {/* Poll type */} +
+ +
+ + +
-
- {/* Expiry */} -
- - - + {/* Poll options */} +
+ +
- - -
- {/* Date picker */} -
- - { - if (date) { - // Preserve the time from current expiresAt - const currentExpiresAt = newPoll.expiresAt - ? new Date(newPoll.expiresAt) - : new Date(); - date.setHours(currentExpiresAt.getHours(), currentExpiresAt.getMinutes()); - setNewPoll({ - ...newPoll, - expiresAt: date.toISOString(), - }); - } - }} - disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} - initialFocus - className="rounded-md border" - classNames={{ - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", - month: "space-y-4", - caption: "flex justify-center pt-1 relative items-center", - caption_label: "text-sm font-medium", - nav: "space-x-1 flex items-center", - nav_button: "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", - head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", - row: "flex w-full mt-2", - cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", - day: "h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors", - day_range_end: "day-range-end", - day_selected: "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", - day_today: "bg-accent text-accent-foreground", - day_outside: "day-outside text-muted-foreground opacity-50", - day_disabled: "text-muted-foreground opacity-50", - day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", - day_hidden: "invisible", - }} - /> -
+ +
+
- {/* Time picker */} -
- -
- - { - const [hours, minutes] = e.target.value.split(':').map(Number); - if (newPoll.expiresAt) { - const date = new Date(newPoll.expiresAt); - date.setHours(hours || 0, minutes || 0); + {/* Expiry */} +
+ + + + + + +
+ {/* Date picker */} +
+ + { + if (date) { + // Preserve the time from current expiresAt + const currentExpiresAt = newPoll.expiresAt + ? new Date(newPoll.expiresAt) + : new Date(); + date.setHours( + currentExpiresAt.getHours(), + currentExpiresAt.getMinutes() + ); setNewPoll({ ...newPoll, expiresAt: date.toISOString(), }); } }} - className="flex-1" + disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} + initialFocus + className="rounded-md border" + classNames={{ + months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', + month: 'space-y-4', + caption: 'flex justify-center pt-1 relative items-center', + caption_label: 'text-sm font-medium', + nav: 'space-x-1 flex items-center', + nav_button: 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100', + nav_button_previous: 'absolute left-1', + nav_button_next: 'absolute right-1', + table: 'w-full border-collapse space-y-1', + head_row: 'flex', + head_cell: + 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]', + row: 'flex w-full mt-2', + cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20', + day: 'h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors', + day_range_end: 'day-range-end', + day_selected: + 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', + day_today: 'bg-accent text-accent-foreground', + day_outside: 'day-outside text-muted-foreground opacity-50', + day_disabled: 'text-muted-foreground opacity-50', + day_range_middle: + 'aria-selected:bg-accent aria-selected:text-accent-foreground', + day_hidden: 'invisible', + }} />
-
-
- - -
- {/* Options or Date Picker */} - {newPoll.pollType === 'date' ? ( -
- - - - - - -
- { - if (dates) { - setDateOptions(dates as Date[]); - } - }} - disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} - initialFocus - className="rounded-md border" - classNames={{ - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", - month: "space-y-4", - caption: "flex justify-center pt-1 relative items-center", - caption_label: "text-sm font-medium", - nav: "space-x-1 flex items-center", - nav_button: "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", - head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", - row: "flex w-full mt-2", - cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", - day: "h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors", - day_range_end: "day-range-end", - day_selected: "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", - day_today: "bg-accent text-accent-foreground", - day_outside: "day-outside text-muted-foreground opacity-50", - day_disabled: "text-muted-foreground opacity-50", - day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", - day_hidden: "invisible", - }} - /> + {/* Time picker */} +
+ +
+ + { + const [hours, minutes] = e.target.value.split(':').map(Number); + if (newPoll.expiresAt) { + const date = new Date(newPoll.expiresAt); + date.setHours(hours || 0, minutes || 0); + setNewPoll({ + ...newPoll, + expiresAt: date.toISOString(), + }); + } + }} + className="flex-1" + /> +
+
+
- {/* Selected dates */} - {dateOptions.length > 0 && ( -
- {dateOptions - .sort((a, b) => a.getTime() - b.getTime()) - .map((date, index) => ( -
- {format(date, 'MM월 dd일 (E)', { locale: ko })} - + + +
+ { + if (dates) { + setDateOptions(dates as Date[]); + } + }} + disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} + initialFocus + className="rounded-md border" + classNames={{ + months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', + month: 'space-y-4', + caption: 'flex justify-center pt-1 relative items-center', + caption_label: 'text-sm font-medium', + nav: 'space-x-1 flex items-center', + nav_button: 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100', + nav_button_previous: 'absolute left-1', + nav_button_next: 'absolute right-1', + table: 'w-full border-collapse space-y-1', + head_row: 'flex', + head_cell: + 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]', + row: 'flex w-full mt-2', + cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20', + day: 'h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors', + day_range_end: 'day-range-end', + day_selected: + 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', + day_today: 'bg-accent text-accent-foreground', + day_outside: 'day-outside text-muted-foreground opacity-50', + day_disabled: 'text-muted-foreground opacity-50', + day_range_middle: + 'aria-selected:bg-accent aria-selected:text-accent-foreground', + day_hidden: 'invisible', + }} + /> +
+
+ + + {/* Selected dates */} + {dateOptions.length > 0 && ( +
+ {dateOptions + .sort((a, b) => a.getTime() - b.getTime()) + .map((date, index) => ( +
- × - -
- ))} -
+ {format(date, 'MM월 dd일 (E)', { locale: ko })} + +
+ ))} +
+ )} +
+ ) : ( +
+ + {options.map((option, index) => ( +
+ handleOptionChange(index, e.target.value)} + className="flex-1 text-sm" + /> + +
+ ))} + +
+ )} + + {/* Add/Update poll button */} +
+ {(editingIndex !== null || formOpen) && ( + )} -
- ) : ( -
- - {options.map((option, index) => ( -
- handleOptionChange(index, e.target.value)} - className="flex-1 text-sm" - /> - -
- ))}
- )} - - {/* Add/Update poll button */} -
- {editingIndex !== null && ( - - )} -
-
)}
);