From 8db8ce668c11daadf3bcea4ab31d37c0e9120c11 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Sat, 2 Aug 2025 18:51:22 +0900 Subject: [PATCH] =?UTF-8?q?-=20Biome=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=B0=8F=20=EA=B7=9C=EC=B9=99=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=E2=94=82=20=20=E2=94=82=20=20=20=20-=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EB=8C=80=EC=83=81=20=ED=8F=AC=EB=A7=B7?= =?UTF-8?q?=ED=8C=85=20=EB=B0=8F=20=EB=A6=B0=ED=8A=B8=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=E2=94=82=20=20=E2=94=82=20=20=20?= =?UTF-8?q?=20-=20=20=ED=83=80=EC=9E=85=EC=9D=84=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=98=EC=97=AC=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=95=ED=99=94=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=E2=94=82=20=20=E2=94=82=20=20=20=20-=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20useEffect=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EB=B0=B0=EC=97=B4=20=EC=88=98=EC=A0=95=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=E2=94=82=20=20=E2=94=82=20=20=20=20-=20=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=9D=84=20/=EB=A1=9C=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=BC=EA=B4=80=EC=84=B1=20=ED=99=95=EB=B3=B4=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=E2=94=82=20=20=E2=94=82=20=20=20?= =?UTF-8?q?=20-=20=EC=97=90=20=EA=B0=9C=EB=B0=9C=20=EC=9B=90=EC=B9=99=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + GEMINI.md | 6 + viewer/.env | 2 +- viewer/biome.json | 2 +- viewer/components.json | 28 +- viewer/src/App.js | 14 - viewer/src/App.tsx | 2 +- viewer/src/components/DynamicForm.d.ts | 17 +- viewer/src/components/DynamicForm.js | 146 ++-- viewer/src/components/DynamicForm.tsx | 9 +- viewer/src/components/DynamicTable.d.ts | 15 +- viewer/src/components/DynamicTable.js | 656 ++++++++++++++---- viewer/src/components/DynamicTable.tsx | 6 +- viewer/src/components/ErrorDisplay.d.ts | 7 +- viewer/src/components/ErrorDisplay.js | 12 +- viewer/src/components/Header.js | 63 +- viewer/src/components/Header.tsx | 2 +- viewer/src/components/LanguageSelectBox.js | 12 +- viewer/src/components/MainLayout.js | 8 +- viewer/src/components/ProjectSelectBox.js | 64 +- viewer/src/components/ProjectSelectBox.tsx | 8 +- viewer/src/components/ThemeSelectBox.js | 52 +- viewer/src/components/UserProfileBox.js | 12 +- .../components/providers/ThemeProvider.d.ts | 7 +- .../src/components/providers/ThemeProvider.js | 30 +- viewer/src/components/ui/button.d.ts | 33 +- viewer/src/components/ui/button.js | 60 +- viewer/src/components/ui/calendar.d.ts | 22 +- viewer/src/components/ui/calendar.js | 244 +++++-- viewer/src/components/ui/calendar.tsx | 388 +++++------ viewer/src/components/ui/card.d.ts | 45 +- viewer/src/components/ui/card.js | 61 +- viewer/src/components/ui/card.tsx | 136 ++-- viewer/src/components/ui/dropdown-menu.d.ts | 121 +++- viewer/src/components/ui/dropdown-menu.js | 158 ++++- viewer/src/components/ui/dropdown-menu.tsx | 324 ++++----- viewer/src/components/ui/input.d.ts | 6 +- viewer/src/components/ui/input.js | 12 +- viewer/src/components/ui/input.tsx | 32 +- viewer/src/components/ui/label.d.ts | 13 +- viewer/src/components/ui/label.js | 12 +- viewer/src/components/ui/label.tsx | 34 +- viewer/src/components/ui/popover.d.ts | 16 +- viewer/src/components/ui/popover.js | 16 +- viewer/src/components/ui/popover.tsx | 48 +- viewer/src/components/ui/select.d.ts | 79 ++- viewer/src/components/ui/select.js | 133 +++- viewer/src/components/ui/select.tsx | 262 +++---- viewer/src/components/ui/separator.d.ts | 9 +- viewer/src/components/ui/separator.js | 18 +- viewer/src/components/ui/separator.tsx | 40 +- viewer/src/components/ui/table.d.ts | 50 +- viewer/src/components/ui/table.js | 90 ++- viewer/src/components/ui/table.tsx | 186 ++--- viewer/src/components/ui/textarea.d.ts | 5 +- viewer/src/components/ui/textarea.js | 9 +- viewer/src/components/ui/textarea.tsx | 26 +- viewer/src/hooks/useSyncChannelId.ts | 2 +- viewer/src/index.css | 6 +- viewer/src/lib/utils.js | 2 +- viewer/src/main.js | 7 - viewer/src/main.tsx | 8 +- viewer/src/pages/FeedbackCreatePage.js | 168 +++-- viewer/src/pages/FeedbackCreatePage.tsx | 18 +- viewer/src/pages/FeedbackDetailPage.js | 235 ++++--- viewer/src/pages/FeedbackDetailPage.tsx | 35 +- viewer/src/pages/FeedbackListPage.js | 103 +-- viewer/src/pages/FeedbackListPage.tsx | 8 +- viewer/src/pages/IssueViewerPage.js | 138 +++- viewer/src/pages/IssueViewerPage.tsx | 2 +- viewer/src/services/error.d.ts | 5 +- viewer/src/services/error.js | 12 +- viewer/src/services/feedback.d.ts | 56 +- viewer/src/services/feedback.js | 178 ++--- viewer/src/services/feedback.ts | 25 +- viewer/src/services/issue.d.ts | 18 +- viewer/src/services/issue.js | 30 +- viewer/src/services/issue.ts | 9 +- viewer/src/services/project.d.ts | 24 +- viewer/src/services/project.js | 48 +- viewer/src/store/useSettingsStore.d.ts | 45 +- viewer/src/store/useSettingsStore.js | 10 - viewer/src/store/useSettingsStore.ts | 20 +- viewer/tsconfig.json | 52 +- viewer/vite.config.js | 60 +- viewer/vite.config.ts | 4 +- 86 files changed, 3434 insertions(+), 1767 deletions(-) delete mode 100644 viewer/src/App.js delete mode 100644 viewer/src/main.js delete mode 100644 viewer/src/store/useSettingsStore.js diff --git a/.gitignore b/.gitignore index a547bf3..7fcc45f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,8 @@ dist-ssr *.njsproj *.sln *.sw? + +.env +.env.local + +.DS_Store \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md index fd3146e..3567ac5 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -57,3 +57,9 @@ - **인증 상태 관리**: 사용자의 로그인 상태를 전역으로 관리하고, 인증 상태에 따라 UI가 동적으로 변경되도록 설정 - **라우트 보호**: 인증이 필요한 페이지에 접근 제어(Route Guard)를 적용하여 비인가 사용자의 접근 차단 - **컨테이너화**: Dockerfile을 작성하고 Docker Compose를 설정하여 개발 및 프로덕션 환경을 컨테이너 기반으로 구축 + +## 6. 개발 원칙 + +- **코드 품질**: 모든 기능 구현 후, Biome 설정을 기반으로 코드를 정리하여 일관된 스타일과 높은 품질을 유지합니다. +- **버전 관리**: 각 기능 단위의 개발이 완료되�� 정상 동작이 확인되면, 사용자에게 git commit 여부를 물어봅니다. + diff --git a/viewer/.env b/viewer/.env index 877a236..0d67dc5 100644 --- a/viewer/.env +++ b/viewer/.env @@ -1,4 +1,4 @@ -VITE_API_PROXY_TARGET=http://172.16.10.175:3030/_back +VITE_API_PROXY_TARGET=https://feedback.hmac.kr/_back # API 요청 시 필요한 인증 키 VITE_API_KEY=F5FE0363E37C012204F5 diff --git a/viewer/biome.json b/viewer/biome.json index 7f47e82..7fbb6e3 100644 --- a/viewer/biome.json +++ b/viewer/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", "vcs": { "enabled": false, "clientKind": "git", diff --git a/viewer/components.json b/viewer/components.json index a087820..b84fb87 100644 --- a/viewer/components.json +++ b/viewer/components.json @@ -1,16 +1,16 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/index.css", - "baseColor": "neutral", - "cssVariables": true - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils" - } + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } } diff --git a/viewer/src/App.js b/viewer/src/App.js deleted file mode 100644 index 6c53d51..0000000 --- a/viewer/src/App.js +++ /dev/null @@ -1,14 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -// src/App.tsx -import { Routes, Route, Navigate } from "react-router-dom"; -import { MainLayout } from "@/components/MainLayout"; -import { FeedbackCreatePage } from "@/pages/FeedbackCreatePage"; -import { FeedbackListPage } from "@/pages/FeedbackListPage"; -import { FeedbackDetailPage } from "@/pages/FeedbackDetailPage"; -import { IssueViewerPage } from "@/pages/IssueViewerPage"; -import { ThemeProvider } from "./components/providers/ThemeProvider"; -function App() { - return (_jsx(ThemeProvider, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: "/projects/1/channels/4/feedbacks" }) }), _jsxs(Route, { path: "/projects/:projectId/channels/:channelId/feedbacks", element: _jsx(MainLayout, {}), children: [_jsx(Route, { index: true, element: _jsx(FeedbackListPage, {}) }), _jsx(Route, { path: "new", element: _jsx(FeedbackCreatePage, {}) }), _jsx(Route, { path: ":feedbackId", element: _jsx(FeedbackDetailPage, {}) })] }), _jsx(Route, { path: "/issues/:issueId" // 이슈 ID만 받도록 단순화 - , element: _jsx(IssueViewerPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/" }) })] }) })); -} -export default App; diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index 3e0bcf6..33b64e7 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -1,5 +1,5 @@ // src/App.tsx -import { Routes, Route, Navigate, Outlet } from "react-router-dom"; +import { Routes, Route, Navigate } from "react-router-dom"; import { MainLayout } from "@/components/MainLayout"; import { FeedbackCreatePage } from "@/pages/FeedbackCreatePage"; import { FeedbackListPage } from "@/pages/FeedbackListPage"; diff --git a/viewer/src/components/DynamicForm.d.ts b/viewer/src/components/DynamicForm.d.ts index c6a4e60..f67bf9f 100644 --- a/viewer/src/components/DynamicForm.d.ts +++ b/viewer/src/components/DynamicForm.d.ts @@ -1,10 +1,13 @@ import type { FeedbackField } from "@/services/feedback"; interface DynamicFormProps { - fields: FeedbackField[]; - onSubmit: (formData: Record) => Promise; - initialData?: Record; - submitButtonText?: string; + fields: FeedbackField[]; + onSubmit: (formData: Record) => Promise; + initialData?: Record; + submitButtonText?: string; } -export declare function DynamicForm({ fields, onSubmit, initialData, // 기본값으로 상수 사용 -submitButtonText, }: DynamicFormProps): import("react/jsx-runtime").JSX.Element; -export {}; +export declare function DynamicForm({ + fields, + onSubmit, + initialData, // 기본값으로 상수 사용 + submitButtonText, +}: DynamicFormProps): import("react/jsx-runtime").JSX.Element; diff --git a/viewer/src/components/DynamicForm.js b/viewer/src/components/DynamicForm.js index 8a0ae60..b4a0f8b 100644 --- a/viewer/src/components/DynamicForm.js +++ b/viewer/src/components/DynamicForm.js @@ -3,51 +3,109 @@ import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; // 컴포넌트 외부에 안정적인 참조를 가진 빈 객체 상수 선언 const EMPTY_INITIAL_DATA = {}; -export function DynamicForm({ fields, onSubmit, initialData = EMPTY_INITIAL_DATA, // 기본값으로 상수 사용 -submitButtonText = "제출", }) { - const [formData, setFormData] = useState(initialData); - const [isSubmitting, setIsSubmitting] = useState(false); - useEffect(() => { - // initialData prop이 변경될 때만 폼 데이터를 동기화 - setFormData(initialData); - }, [initialData]); - const handleFormChange = (fieldId, value) => { - setFormData((prev) => ({ ...prev, [fieldId]: value })); - }; - const handleSubmit = async (e) => { - e.preventDefault(); - setIsSubmitting(true); - try { - await onSubmit(formData); - } - catch (error) { - console.error("Form submission error:", error); - } - finally { - setIsSubmitting(false); - } - }; - const renderField = (field) => { - const commonProps = { - id: field.id, - value: formData[field.id] ?? "", - disabled: field.readOnly, - }; - switch (field.type) { - case "textarea": - return (_jsx(Textarea, { ...commonProps, onChange: (e) => handleFormChange(field.id, e.target.value), placeholder: field.readOnly ? "" : `${field.name}...`, rows: 5 })); - case "text": - case "number": - return (_jsx(Input, { ...commonProps, type: field.type, onChange: (e) => handleFormChange(field.id, e.target.value), placeholder: field.readOnly ? "" : field.name })); - case "select": - return (_jsxs(Select, { value: commonProps.value, onValueChange: (value) => handleFormChange(field.id, value), disabled: field.readOnly, children: [_jsx(SelectTrigger, { id: field.id, children: _jsx(SelectValue, { placeholder: `-- ${field.name} 선택 --` }) }), _jsx(SelectContent, {})] })); - default: - return _jsxs("p", { children: ["\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD544\uB4DC \uD0C0\uC785: ", field.type] }); - } - }; - return (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [fields.map((field) => (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: field.id, children: field.name }), renderField(field)] }, field.id))), _jsx(Button, { type: "submit", disabled: isSubmitting, children: isSubmitting ? "전송 중..." : submitButtonText })] })); +export function DynamicForm({ + fields, + onSubmit, + initialData = EMPTY_INITIAL_DATA, // 기본값으로 상수 사용 + submitButtonText = "제출", +}) { + const [formData, setFormData] = useState(initialData); + const [isSubmitting, setIsSubmitting] = useState(false); + useEffect(() => { + // initialData prop이 변경될 때만 폼 데이터를 동기화 + setFormData(initialData); + }, [initialData]); + const handleFormChange = (fieldId, value) => { + setFormData((prev) => ({ ...prev, [fieldId]: value })); + }; + const handleSubmit = async (e) => { + e.preventDefault(); + setIsSubmitting(true); + try { + await onSubmit(formData); + } catch (error) { + console.error("Form submission error:", error); + } finally { + setIsSubmitting(false); + } + }; + const renderField = (field) => { + const commonProps = { + id: field.id, + value: formData[field.id] ?? "", + disabled: field.readOnly, + }; + switch (field.type) { + case "textarea": + return _jsx(Textarea, { + ...commonProps, + onChange: (e) => handleFormChange(field.id, e.target.value), + placeholder: field.readOnly ? "" : `${field.name}...`, + rows: 5, + }); + case "text": + case "number": + return _jsx(Input, { + ...commonProps, + type: field.type, + onChange: (e) => handleFormChange(field.id, e.target.value), + placeholder: field.readOnly ? "" : field.name, + }); + case "select": + return _jsxs(Select, { + value: commonProps.value, + onValueChange: (value) => handleFormChange(field.id, value), + disabled: field.readOnly, + children: [ + _jsx(SelectTrigger, { + id: field.id, + children: _jsx(SelectValue, { + placeholder: `-- ${field.name} 선택 --`, + }), + }), + _jsx(SelectContent, {}), + ], + }); + default: + return _jsxs("p", { + children: [ + "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD544\uB4DC \uD0C0\uC785: ", + field.type, + ], + }); + } + }; + return _jsxs("form", { + onSubmit: handleSubmit, + className: "space-y-4", + children: [ + fields.map((field) => + _jsxs( + "div", + { + className: "space-y-2", + children: [ + _jsx(Label, { htmlFor: field.id, children: field.name }), + renderField(field), + ], + }, + field.id, + ), + ), + _jsx(Button, { + type: "submit", + disabled: isSubmitting, + children: isSubmitting ? "전송 중..." : submitButtonText, + }), + ], + }); } diff --git a/viewer/src/components/DynamicForm.tsx b/viewer/src/components/DynamicForm.tsx index a9794d2..097ab16 100644 --- a/viewer/src/components/DynamicForm.tsx +++ b/viewer/src/components/DynamicForm.tsx @@ -1,4 +1,3 @@ - import { useState, useEffect } from "react"; import type { FeedbackField } from "@/services/feedback"; import { Button } from "@/components/ui/button"; @@ -17,8 +16,8 @@ const EMPTY_INITIAL_DATA = {}; interface DynamicFormProps { fields: FeedbackField[]; - onSubmit: (formData: Record) => Promise; - initialData?: Record; + onSubmit: (formData: Record) => Promise; + initialData?: Record; submitButtonText?: string; } @@ -28,7 +27,7 @@ export function DynamicForm({ initialData = EMPTY_INITIAL_DATA, // 기본값으로 상수 사용 submitButtonText = "제출", }: DynamicFormProps) { - const [formData, setFormData] = useState>(initialData); + const [formData, setFormData] = useState>(initialData); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { @@ -36,7 +35,7 @@ export function DynamicForm({ setFormData(initialData); }, [initialData]); - const handleFormChange = (fieldId: string, value: any) => { + const handleFormChange = (fieldId: string, value: unknown) => { setFormData((prev) => ({ ...prev, [fieldId]: value })); }; diff --git a/viewer/src/components/DynamicTable.d.ts b/viewer/src/components/DynamicTable.d.ts index b36fe81..a0fec4a 100644 --- a/viewer/src/components/DynamicTable.d.ts +++ b/viewer/src/components/DynamicTable.d.ts @@ -1,9 +1,14 @@ import type { Feedback, FeedbackField } from "@/services/feedback"; interface DynamicTableProps { - columns: FeedbackField[]; - data: Feedback[]; - projectId: string; - channelId: string; + columns: FeedbackField[]; + data: Feedback[]; + projectId: string; + channelId: string; } -export declare function DynamicTable({ columns: rawColumns, data, projectId, channelId, }: DynamicTableProps): import("react/jsx-runtime").JSX.Element; +export declare function DynamicTable({ + columns: rawColumns, + data, + projectId, + channelId, +}: DynamicTableProps): import("react/jsx-runtime").JSX.Element; export default DynamicTable; diff --git a/viewer/src/components/DynamicTable.js b/viewer/src/components/DynamicTable.js index 2786be8..6d95503 100644 --- a/viewer/src/components/DynamicTable.js +++ b/viewer/src/components/DynamicTable.js @@ -1,146 +1,534 @@ -import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; -import { flexRender, getCoreRowModel, getExpandedRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; +import { + jsx as _jsx, + jsxs as _jsxs, + Fragment as _Fragment, +} from "react/jsx-runtime"; +import { + flexRender, + getCoreRowModel, + getExpandedRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; import { addDays, format } from "date-fns"; -import { ArrowUpDown, Calendar as CalendarIcon, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, } from "lucide-react"; +import { + ArrowUpDown, + Calendar as CalendarIcon, + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, +} from "lucide-react"; import { useMemo, useState, Fragment } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { Card, CardContent } from "@/components/ui/card"; -import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; -import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { cn } from "@/lib/utils"; const DEFAULT_COLUMN_ORDER = [ - "id", - "title", - "contents", - "issues", - "customer", - "updatedAt", + "id", + "title", + "contents", + "issues", + "customer", + "updatedAt", ]; -export function DynamicTable({ columns: rawColumns, data, projectId, channelId, }) { - const navigate = useNavigate(); - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [expanded, setExpanded] = useState({}); - const [globalFilter, setGlobalFilter] = useState(""); - const [date, setDate] = useState(); - const columns = useMemo(() => { - const orderedRawColumns = [...rawColumns].sort((a, b) => { - const indexA = DEFAULT_COLUMN_ORDER.indexOf(a.id); - const indexB = DEFAULT_COLUMN_ORDER.indexOf(b.id); - if (indexA === -1 && indexB === -1) - return 0; - if (indexA === -1) - return 1; - if (indexB === -1) - return -1; - return indexA - indexB; - }); - const generatedColumns = orderedRawColumns.map((field) => ({ - accessorKey: field.id, - header: ({ column }) => { - if (field.id === "issues") { - return _jsx("div", { children: field.name }); - } - return (_jsxs(Button, { variant: "ghost", onClick: () => column.toggleSorting(column.getIsSorted() === "asc"), children: [field.name, _jsx(ArrowUpDown, { className: "ml-2 h-4 w-4" })] })); - }, - cell: ({ row }) => { - const value = row.original[field.id]; - switch (field.id) { - case "issues": { - const issues = value; - if (!issues || issues.length === 0) - return "N/A"; - return (_jsx("div", { className: "flex flex-col space-y-1", children: issues.map((issue) => (_jsx(Link, { to: `/issues/${issue.id}`, className: "text-blue-600 hover:underline", onClick: (e) => e.stopPropagation(), children: issue.name }, issue.id))) })); - } - case "title": - return (_jsx("div", { className: "whitespace-normal break-words w-48", children: String(value ?? "N/A") })); - case "contents": { - const content = String(value ?? "N/A"); - const truncated = content.length > 50 - ? `${content.substring(0, 50)}...` - : content; - return (_jsx("div", { className: "whitespace-normal break-words w-60", children: truncated })); - } - case "createdAt": - case "updatedAt": - return String(value ?? "N/A").substring(0, 10); - default: - if (typeof value === "object" && value !== null) { - return JSON.stringify(value); - } - return String(value ?? "N/A"); - } - }, - })); - return [ - { - id: "expander", - header: () => null, - cell: ({ row }) => { - return (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { - e.stopPropagation(); - row.toggleExpanded(); - }, children: row.getIsExpanded() ? "▼" : "▶" })); - }, - }, - ...generatedColumns, - ]; - }, [rawColumns]); - const filteredData = useMemo(() => { - if (!date?.from) { - return data; - } - const fromDate = date.from; - const toDate = date.to ? addDays(date.to, 1) : addDays(fromDate, 1); - return data.filter((item) => { - const itemDate = new Date(item.updatedAt); - return itemDate >= fromDate && itemDate < toDate; - }); - }, [data, date]); - const table = useReactTable({ - data: filteredData, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onExpandedChange: setExpanded, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getExpandedRowModel: getExpandedRowModel(), - initialState: { - pagination: { - pageSize: 20, - }, - }, - state: { - sorting, - columnFilters, - columnVisibility, - expanded, - globalFilter, - }, - onGlobalFilterChange: setGlobalFilter, - }); - const handleRowClick = (feedbackId) => { - navigate(`/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`); - }; - return (_jsx(Card, { children: _jsxs(CardContent, { children: [_jsxs("div", { className: "flex items-center justify-between py-4", children: [_jsx(Input, { placeholder: "\uC804\uCCB4 \uB370\uC774\uD130\uC5D0\uC11C \uAC80\uC0C9...", value: globalFilter, onChange: (event) => setGlobalFilter(event.target.value), className: "max-w-sm" }), _jsxs("div", { className: "flex items-center space-x-2", children: [_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: "date", variant: "outline", className: cn("w-[300px] justify-start text-left font-normal", !date && "text-muted-foreground"), children: [_jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }), date?.from ? (date.to ? (_jsxs(_Fragment, { children: [format(date.from, "LLL dd, y"), " -", " ", format(date.to, "LLL dd, y")] })) : (format(date.from, "LLL dd, y"))) : (_jsx("span", { children: "\uAE30\uAC04 \uC120\uD0DD" }))] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "end", children: _jsx(Calendar, { initialFocus: true, mode: "range", defaultMonth: date?.from, selected: date, onSelect: setDate, numberOfMonths: 2 }) })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: "ml-auto", children: ["\uCEEC\uB7FC \uD45C\uC2DC ", _jsx(ChevronDown, { className: "ml-2 h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return (_jsx(DropdownMenuCheckboxItem, { className: "capitalize", checked: column.getIsVisible(), onCheckedChange: (value) => column.toggleVisibility(!!value), children: column.id }, column.id)); - }) })] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { children: headerGroup.headers.map((header) => (_jsx(TableHead, { style: { width: header.getSize() }, children: header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext()) }, header.id))) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows?.length ? (table.getRowModel().rows.map((row) => (_jsxs(Fragment, { children: [_jsx(TableRow, { "data-state": row.getIsSelected() && "selected", onClick: () => handleRowClick(row.original.id.toString()), className: "cursor-pointer hover:bg-muted/50", children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }), row.getIsExpanded() && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length + 1, children: _jsxs("div", { className: "p-4 bg-muted rounded-md", children: [_jsx("h4", { className: "font-bold text-lg", children: row.original.title }), _jsx("p", { className: "mt-2 whitespace-pre-wrap", children: row.original.contents })] }) }) }, `${row.id}-expanded`))] }, row.id)))) : (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length + 1, className: "h-24 text-center", children: "\uD45C\uC2DC\uD560 \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) })) })] }) }), _jsxs("div", { className: "flex items-center justify-between py-4", children: [_jsxs("div", { className: "flex-1 text-sm text-muted-foreground", children: ["\uCD1D ", table.getFilteredRowModel().rows.length, "\uAC1C"] }), _jsxs("div", { className: "flex items-center space-x-6", children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsx("p", { className: "text-sm font-medium", children: "\uD398\uC774\uC9C0 \uB2F9 \uD589 \uC218" }), _jsxs(Select, { value: `${table.getState().pagination.pageSize}`, onValueChange: (value) => { - table.setPageSize(Number(value)); - }, children: [_jsx(SelectTrigger, { className: "h-8 w-[70px]", children: _jsx(SelectValue, { placeholder: table.getState().pagination.pageSize }) }), _jsx(SelectContent, { side: "top", children: [20, 30, 50].map((pageSize) => (_jsx(SelectItem, { value: `${pageSize}`, children: pageSize }, pageSize))) })] })] }), _jsxs("div", { className: "flex w-[100px] items-center justify-center text-sm font-medium", children: [table.getPageCount(), " \uD398\uC774\uC9C0 \uC911", " ", table.getState().pagination.pageIndex + 1] }), _jsxs("div", { className: "flex items-center space-x-2", children: [_jsxs(Button, { variant: "outline", className: "hidden h-8 w-8 p-0 lg:flex", onClick: () => table.setPageIndex(0), disabled: !table.getCanPreviousPage(), children: [_jsx("span", { className: "sr-only", children: "\uCCAB \uD398\uC774\uC9C0\uB85C" }), _jsx(ChevronsLeft, { className: "h-4 w-4" })] }), _jsxs(Button, { variant: "outline", className: "h-8 w-8 p-0", onClick: () => table.previousPage(), disabled: !table.getCanPreviousPage(), children: [_jsx("span", { className: "sr-only", children: "\uC774\uC804 \uD398\uC774\uC9C0\uB85C" }), _jsx(ChevronLeft, { className: "h-4 w-4" })] }), _jsxs(Button, { variant: "outline", className: "h-8 w-8 p-0", onClick: () => table.nextPage(), disabled: !table.getCanNextPage(), children: [_jsx("span", { className: "sr-only", children: "\uB2E4\uC74C \uD398\uC774\uC9C0\uB85C" }), _jsx(ChevronRight, { className: "h-4 w-4" })] }), _jsxs(Button, { variant: "outline", className: "hidden h-8 w-8 p-0 lg:flex", onClick: () => table.setPageIndex(table.getPageCount() - 1), disabled: !table.getCanNextPage(), children: [_jsx("span", { className: "sr-only", children: "\uB9C8\uC9C0\uB9C9 \uD398\uC774\uC9C0\uB85C" }), _jsx(ChevronsRight, { className: "h-4 w-4" })] })] })] })] })] }) })); +export function DynamicTable({ + columns: rawColumns, + data, + projectId, + channelId, +}) { + const navigate = useNavigate(); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [expanded, setExpanded] = useState({}); + const [globalFilter, setGlobalFilter] = useState(""); + const [date, setDate] = useState(); + const columns = useMemo(() => { + const orderedRawColumns = [...rawColumns].sort((a, b) => { + const indexA = DEFAULT_COLUMN_ORDER.indexOf(a.id); + const indexB = DEFAULT_COLUMN_ORDER.indexOf(b.id); + if (indexA === -1 && indexB === -1) return 0; + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + const generatedColumns = orderedRawColumns.map((field) => ({ + accessorKey: field.id, + header: ({ column }) => { + if (field.id === "issues") { + return _jsx("div", { children: field.name }); + } + return _jsxs(Button, { + variant: "ghost", + onClick: () => column.toggleSorting(column.getIsSorted() === "asc"), + children: [ + field.name, + _jsx(ArrowUpDown, { className: "ml-2 h-4 w-4" }), + ], + }); + }, + cell: ({ row }) => { + const value = row.original[field.id]; + switch (field.id) { + case "issues": { + const issues = value; + if (!issues || issues.length === 0) return "N/A"; + return _jsx("div", { + className: "flex flex-col space-y-1", + children: issues.map((issue) => + _jsx( + Link, + { + to: `/issues/${issue.id}`, + className: "text-blue-600 hover:underline", + onClick: (e) => e.stopPropagation(), + children: issue.name, + }, + issue.id, + ), + ), + }); + } + case "title": + return _jsx("div", { + className: "whitespace-normal break-words w-48", + children: String(value ?? "N/A"), + }); + case "contents": { + const content = String(value ?? "N/A"); + const truncated = + content.length > 50 ? `${content.substring(0, 50)}...` : content; + return _jsx("div", { + className: "whitespace-normal break-words w-60", + children: truncated, + }); + } + case "createdAt": + case "updatedAt": + return String(value ?? "N/A").substring(0, 10); + default: + if (typeof value === "object" && value !== null) { + return JSON.stringify(value); + } + return String(value ?? "N/A"); + } + }, + })); + return [ + { + id: "expander", + header: () => null, + cell: ({ row }) => { + return _jsx(Button, { + variant: "ghost", + size: "sm", + onClick: (e) => { + e.stopPropagation(); + row.toggleExpanded(); + }, + children: row.getIsExpanded() ? "▼" : "▶", + }); + }, + }, + ...generatedColumns, + ]; + }, [rawColumns]); + const filteredData = useMemo(() => { + if (!date?.from) { + return data; + } + const fromDate = date.from; + const toDate = date.to ? addDays(date.to, 1) : addDays(fromDate, 1); + return data.filter((item) => { + const itemDate = new Date(item.updatedAt); + return itemDate >= fromDate && itemDate < toDate; + }); + }, [data, date]); + const table = useReactTable({ + data: filteredData, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onExpandedChange: setExpanded, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getExpandedRowModel: getExpandedRowModel(), + initialState: { + pagination: { + pageSize: 20, + }, + }, + state: { + sorting, + columnFilters, + columnVisibility, + expanded, + globalFilter, + }, + onGlobalFilterChange: setGlobalFilter, + }); + const handleRowClick = (feedbackId) => { + navigate( + `/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, + ); + }; + return _jsx(Card, { + children: _jsxs(CardContent, { + children: [ + _jsxs("div", { + className: "flex items-center justify-between py-4", + children: [ + _jsx(Input, { + placeholder: + "\uC804\uCCB4 \uB370\uC774\uD130\uC5D0\uC11C \uAC80\uC0C9...", + value: globalFilter, + onChange: (event) => setGlobalFilter(event.target.value), + className: "max-w-sm", + }), + _jsxs("div", { + className: "flex items-center space-x-2", + children: [ + _jsxs(Popover, { + children: [ + _jsx(PopoverTrigger, { + asChild: true, + children: _jsxs(Button, { + id: "date", + variant: "outline", + className: cn( + "w-[300px] justify-start text-left font-normal", + !date && "text-muted-foreground", + ), + children: [ + _jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }), + date?.from + ? date.to + ? _jsxs(_Fragment, { + children: [ + format(date.from, "LLL dd, y"), + " -", + " ", + format(date.to, "LLL dd, y"), + ], + }) + : format(date.from, "LLL dd, y") + : _jsx("span", { + children: "\uAE30\uAC04 \uC120\uD0DD", + }), + ], + }), + }), + _jsx(PopoverContent, { + className: "w-auto p-0", + align: "end", + children: _jsx(Calendar, { + initialFocus: true, + mode: "range", + defaultMonth: date?.from, + selected: date, + onSelect: setDate, + numberOfMonths: 2, + }), + }), + ], + }), + _jsxs(DropdownMenu, { + children: [ + _jsx(DropdownMenuTrigger, { + asChild: true, + children: _jsxs(Button, { + variant: "outline", + className: "ml-auto", + children: [ + "\uCEEC\uB7FC \uD45C\uC2DC ", + _jsx(ChevronDown, { className: "ml-2 h-4 w-4" }), + ], + }), + }), + _jsx(DropdownMenuContent, { + align: "end", + children: table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return _jsx( + DropdownMenuCheckboxItem, + { + className: "capitalize", + checked: column.getIsVisible(), + onCheckedChange: (value) => + column.toggleVisibility(!!value), + children: column.id, + }, + column.id, + ); + }), + }), + ], + }), + ], + }), + ], + }), + _jsx("div", { + className: "rounded-md border", + children: _jsxs(Table, { + children: [ + _jsx(TableHeader, { + children: table + .getHeaderGroups() + .map((headerGroup) => + _jsx( + TableRow, + { + children: headerGroup.headers.map((header) => + _jsx( + TableHead, + { + style: { width: header.getSize() }, + children: header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + ), + }, + header.id, + ), + ), + }, + headerGroup.id, + ), + ), + }), + _jsx(TableBody, { + children: table.getRowModel().rows?.length + ? table + .getRowModel() + .rows.map((row) => + _jsxs( + Fragment, + { + children: [ + _jsx(TableRow, { + "data-state": row.getIsSelected() && "selected", + onClick: () => + handleRowClick(row.original.id.toString()), + className: "cursor-pointer hover:bg-muted/50", + children: row + .getVisibleCells() + .map((cell) => + _jsx( + TableCell, + { + children: flexRender( + cell.column.columnDef.cell, + cell.getContext(), + ), + }, + cell.id, + ), + ), + }), + row.getIsExpanded() && + _jsx( + TableRow, + { + children: _jsx(TableCell, { + colSpan: columns.length + 1, + children: _jsxs("div", { + className: "p-4 bg-muted rounded-md", + children: [ + _jsx("h4", { + className: "font-bold text-lg", + children: row.original.title, + }), + _jsx("p", { + className: + "mt-2 whitespace-pre-wrap", + children: row.original.contents, + }), + ], + }), + }), + }, + `${row.id}-expanded`, + ), + ], + }, + row.id, + ), + ) + : _jsx(TableRow, { + children: _jsx(TableCell, { + colSpan: columns.length + 1, + className: "h-24 text-center", + children: + "\uD45C\uC2DC\uD560 \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", + }), + }), + }), + ], + }), + }), + _jsxs("div", { + className: "flex items-center justify-between py-4", + children: [ + _jsxs("div", { + className: "flex-1 text-sm text-muted-foreground", + children: [ + "\uCD1D ", + table.getFilteredRowModel().rows.length, + "\uAC1C", + ], + }), + _jsxs("div", { + className: "flex items-center space-x-6", + children: [ + _jsxs("div", { + className: "flex items-center space-x-2", + children: [ + _jsx("p", { + className: "text-sm font-medium", + children: "\uD398\uC774\uC9C0 \uB2F9 \uD589 \uC218", + }), + _jsxs(Select, { + value: `${table.getState().pagination.pageSize}`, + onValueChange: (value) => { + table.setPageSize(Number(value)); + }, + children: [ + _jsx(SelectTrigger, { + className: "h-8 w-[70px]", + children: _jsx(SelectValue, { + placeholder: table.getState().pagination.pageSize, + }), + }), + _jsx(SelectContent, { + side: "top", + children: [20, 30, 50].map((pageSize) => + _jsx( + SelectItem, + { value: `${pageSize}`, children: pageSize }, + pageSize, + ), + ), + }), + ], + }), + ], + }), + _jsxs("div", { + className: + "flex w-[100px] items-center justify-center text-sm font-medium", + children: [ + table.getPageCount(), + " \uD398\uC774\uC9C0 \uC911", + " ", + table.getState().pagination.pageIndex + 1, + ], + }), + _jsxs("div", { + className: "flex items-center space-x-2", + children: [ + _jsxs(Button, { + variant: "outline", + className: "hidden h-8 w-8 p-0 lg:flex", + onClick: () => table.setPageIndex(0), + disabled: !table.getCanPreviousPage(), + children: [ + _jsx("span", { + className: "sr-only", + children: "\uCCAB \uD398\uC774\uC9C0\uB85C", + }), + _jsx(ChevronsLeft, { className: "h-4 w-4" }), + ], + }), + _jsxs(Button, { + variant: "outline", + className: "h-8 w-8 p-0", + onClick: () => table.previousPage(), + disabled: !table.getCanPreviousPage(), + children: [ + _jsx("span", { + className: "sr-only", + children: "\uC774\uC804 \uD398\uC774\uC9C0\uB85C", + }), + _jsx(ChevronLeft, { className: "h-4 w-4" }), + ], + }), + _jsxs(Button, { + variant: "outline", + className: "h-8 w-8 p-0", + onClick: () => table.nextPage(), + disabled: !table.getCanNextPage(), + children: [ + _jsx("span", { + className: "sr-only", + children: "\uB2E4\uC74C \uD398\uC774\uC9C0\uB85C", + }), + _jsx(ChevronRight, { className: "h-4 w-4" }), + ], + }), + _jsxs(Button, { + variant: "outline", + className: "hidden h-8 w-8 p-0 lg:flex", + onClick: () => + table.setPageIndex(table.getPageCount() - 1), + disabled: !table.getCanNextPage(), + children: [ + _jsx("span", { + className: "sr-only", + children: + "\uB9C8\uC9C0\uB9C9 \uD398\uC774\uC9C0\uB85C", + }), + _jsx(ChevronsRight, { className: "h-4 w-4" }), + ], + }), + ], + }), + ], + }), + ], + }), + ], + }), + }); } export default DynamicTable; diff --git a/viewer/src/components/DynamicTable.tsx b/viewer/src/components/DynamicTable.tsx index ac80a08..6ebbf96 100644 --- a/viewer/src/components/DynamicTable.tsx +++ b/viewer/src/components/DynamicTable.tsx @@ -1,4 +1,3 @@ - import { flexRender, getCoreRowModel, @@ -321,7 +320,10 @@ export function DynamicTable({ {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( - + {header.isPlaceholder ? null : flexRender( diff --git a/viewer/src/components/ErrorDisplay.d.ts b/viewer/src/components/ErrorDisplay.d.ts index 78c29e1..1b35ee0 100644 --- a/viewer/src/components/ErrorDisplay.d.ts +++ b/viewer/src/components/ErrorDisplay.d.ts @@ -1,5 +1,6 @@ interface ErrorDisplayProps { - message: string; + message: string; } -export declare function ErrorDisplay({ message }: ErrorDisplayProps): import("react/jsx-runtime").JSX.Element; -export {}; +export declare function ErrorDisplay({ + message, +}: ErrorDisplayProps): import("react/jsx-runtime").JSX.Element; diff --git a/viewer/src/components/ErrorDisplay.js b/viewer/src/components/ErrorDisplay.js index 38a21f6..df5516b 100644 --- a/viewer/src/components/ErrorDisplay.js +++ b/viewer/src/components/ErrorDisplay.js @@ -1,4 +1,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; export function ErrorDisplay({ message }) { - return (_jsxs("div", { className: "bg-destructive/15 text-destructive p-4 rounded-md text-center", role: "alert", children: [_jsx("p", { className: "font-semibold", children: "\uC624\uB958 \uBC1C\uC0DD" }), _jsx("p", { className: "text-sm", children: message })] })); + return _jsxs("div", { + className: "bg-destructive/15 text-destructive p-4 rounded-md text-center", + role: "alert", + children: [ + _jsx("p", { + className: "font-semibold", + children: "\uC624\uB958 \uBC1C\uC0DD", + }), + _jsx("p", { className: "text-sm", children: message }), + ], + }); } diff --git a/viewer/src/components/Header.js b/viewer/src/components/Header.js index 63edc27..7adb418 100644 --- a/viewer/src/components/Header.js +++ b/viewer/src/components/Header.js @@ -1,30 +1,57 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { NavLink } from "react-router-dom"; import { ProjectSelectBox } from "./ProjectSelectBox"; -import { Separator } from "./ui/separator"; + import { ThemeSelectBox } from "./ThemeSelectBox"; import { LanguageSelectBox } from "./LanguageSelectBox"; import { UserProfileBox } from "./UserProfileBox"; import { useSettingsStore } from "@/store/useSettingsStore"; const menuItems = [ - { name: "Home", path: "/" }, - { name: "Feedback", path: "/feedbacks" }, - { name: "Issue", path: "/issues" }, + { name: "Home", path: "/" }, + { name: "Feedback", path: "/feedbacks" }, + { name: "Issue", path: "/issues" }, ]; export function Header() { - const projectId = useSettingsStore((state) => state.projectId); - const channelId = useSettingsStore((state) => state.channelId); - const getFullPath = (path) => { - if (path === "/") - return `/`; // Landing 페이지 경로 예시 - if (path.startsWith("/feedbacks")) { - return `/projects/${projectId}/channels/${channelId}${path}`; - } - if (path.startsWith("/issues")){ - return `/projects/${projectId}${path}`; - } - return path; - }; - return (_jsxs("header", { className: "flex h-16 items-center border-b px-4", children: [_jsx(ProjectSelectBox, { className: "mr-4"}), _jsx("nav", { className: "flex items-center space-x-4 lg:space-x-6 flex-1 ml-8", children: menuItems.map((item) => (_jsx(NavLink, { to: getFullPath(item.path), className: ({ isActive }) => `text-sm font-medium transition-colors hover:text-primary ${!isActive ? "text-muted-foreground" : ""}`, children: item.name }, item.name))) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(ThemeSelectBox, {}), _jsx(LanguageSelectBox, {}), _jsx(UserProfileBox, {})] })] })); + const projectId = useSettingsStore((state) => state.projectId); + const channelId = useSettingsStore((state) => state.channelId); + const getFullPath = (path) => { + if (path === "/") return `/`; // Landing 페이지 경로 예시 + if (path.startsWith("/feedbacks")) { + return `/projects/${projectId}/channels/${channelId}${path}`; + } + if (path.startsWith("/issues")) { + return `/projects/${projectId}${path}`; + } + return path; + }; + return _jsxs("header", { + className: "flex h-16 items-center border-b px-4", + children: [ + _jsx(ProjectSelectBox, { className: "mr-4" }), + _jsx("nav", { + className: "flex items-center space-x-4 lg:space-x-6 flex-1 ml-8", + children: menuItems.map((item) => + _jsx( + NavLink, + { + to: getFullPath(item.path), + className: ({ isActive }) => + `text-sm font-medium transition-colors hover:text-primary ${!isActive ? "text-muted-foreground" : ""}`, + children: item.name, + }, + item.name, + ), + ), + }), + _jsxs("div", { + className: "flex items-center gap-3", + children: [ + _jsx(ThemeSelectBox, {}), + _jsx(LanguageSelectBox, {}), + _jsx(UserProfileBox, {}), + ], + }), + ], + }); } diff --git a/viewer/src/components/Header.tsx b/viewer/src/components/Header.tsx index 1b2c169..21ac292 100644 --- a/viewer/src/components/Header.tsx +++ b/viewer/src/components/Header.tsx @@ -62,4 +62,4 @@ export function Header() { ); -} \ No newline at end of file +} diff --git a/viewer/src/components/LanguageSelectBox.js b/viewer/src/components/LanguageSelectBox.js index 361b9e7..d3ca3a8 100644 --- a/viewer/src/components/LanguageSelectBox.js +++ b/viewer/src/components/LanguageSelectBox.js @@ -2,5 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Languages } from "lucide-react"; import { Button } from "./ui/button"; export function LanguageSelectBox() { - return (_jsxs(Button, { variant: "ghost", size: "icon", children: [_jsx(Languages, {}), _jsx("span", { className: "sr-only", children: "\uC5B8\uC5B4 \uBCC0\uACBD" })] })); + return _jsxs(Button, { + variant: "ghost", + size: "icon", + children: [ + _jsx(Languages, {}), + _jsx("span", { + className: "sr-only", + children: "\uC5B8\uC5B4 \uBCC0\uACBD", + }), + ], + }); } diff --git a/viewer/src/components/MainLayout.js b/viewer/src/components/MainLayout.js index 5583d2d..7ed9cde 100644 --- a/viewer/src/components/MainLayout.js +++ b/viewer/src/components/MainLayout.js @@ -3,5 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Outlet } from "react-router-dom"; import { Header } from "./Header"; export function MainLayout() { - return (_jsxs("div", { className: "flex flex-col min-h-screen", children: [_jsx(Header, {}), _jsx("main", { className: "flex-1 p-6", children: _jsx(Outlet, {}) })] })); + return _jsxs("div", { + className: "flex flex-col min-h-screen", + children: [ + _jsx(Header, {}), + _jsx("main", { className: "flex-1 p-6", children: _jsx(Outlet, {}) }), + ], + }); } diff --git a/viewer/src/components/ProjectSelectBox.js b/viewer/src/components/ProjectSelectBox.js index ef0e7e5..dde2f96 100644 --- a/viewer/src/components/ProjectSelectBox.js +++ b/viewer/src/components/ProjectSelectBox.js @@ -1,25 +1,51 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useEffect, useState } from "react"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { getProjects } from "@/services/project"; import { useSettingsStore } from "@/store/useSettingsStore"; export function ProjectSelectBox() { - const [projects, setProjects] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const { projectId, setProjectId } = useSettingsStore(); - useEffect(() => { - getProjects().then((loadedProjects) => { - setProjects(loadedProjects); - // 로드된 프로젝트 목록에 현재 ID가 없으면, 첫 번째 프로젝트로 ID를 설정 - if (loadedProjects.length > 0 && - !loadedProjects.find((p) => p.id === projectId)) { - setProjectId(loadedProjects[0].id); - } - setIsLoading(false); - }); - }, []); // 마운트 시 한 번만 실행 - if (isLoading) { - return (_jsx("div", { className: "w-[180px] h-10 bg-muted rounded-md animate-pulse" })); - } - return (_jsxs(Select, { value: projectId ?? "", onValueChange: setProjectId, children: [_jsx(SelectTrigger, { className: "w-[180px]", children: _jsx(SelectValue, { placeholder: "\uD504\uB85C\uC81D\uD2B8 \uC120\uD0DD" }) }), _jsx(SelectContent, { children: projects.map((p) => (_jsx(SelectItem, { value: p.id, children: p.name }, p.id))) })] })); + const [projects, setProjects] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { projectId, setProjectId } = useSettingsStore(); + useEffect(() => { + getProjects().then((loadedProjects) => { + setProjects(loadedProjects); + // 로드된 프로젝트 목록에 현재 ID가 없으면, 첫 번째 프로젝트로 ID를 설정 + if ( + loadedProjects.length > 0 && + !loadedProjects.find((p) => p.id === projectId) + ) { + setProjectId(loadedProjects[0].id); + } + setIsLoading(false); + }); + }, [projectId, setProjectId]); // 마운트 시 한 번만 실행 + if (isLoading) { + return _jsx("div", { + className: "w-[180px] h-10 bg-muted rounded-md animate-pulse", + }); + } + return _jsxs(Select, { + value: projectId ?? "", + onValueChange: setProjectId, + children: [ + _jsx(SelectTrigger, { + className: "w-[180px]", + children: _jsx(SelectValue, { + placeholder: "\uD504\uB85C\uC81D\uD2B8 \uC120\uD0DD", + }), + }), + _jsx(SelectContent, { + children: projects.map((p) => + _jsx(SelectItem, { value: p.id, children: p.name }, p.id), + ), + }), + ], + }); } diff --git a/viewer/src/components/ProjectSelectBox.tsx b/viewer/src/components/ProjectSelectBox.tsx index b6f34b4..2460cfe 100644 --- a/viewer/src/components/ProjectSelectBox.tsx +++ b/viewer/src/components/ProjectSelectBox.tsx @@ -26,12 +26,10 @@ export function ProjectSelectBox() { } setIsLoading(false); }); - }, []); // 마운트 시 한 번만 실행 + }, [projectId, setProjectId]); // 마운트 시 한 번만 실행 if (isLoading) { - return ( -
- ); + return
; } return ( @@ -48,4 +46,4 @@ export function ProjectSelectBox() { ); -} \ No newline at end of file +} diff --git a/viewer/src/components/ThemeSelectBox.js b/viewer/src/components/ThemeSelectBox.js index 3933ae0..f3e78ed 100644 --- a/viewer/src/components/ThemeSelectBox.js +++ b/viewer/src/components/ThemeSelectBox.js @@ -1,9 +1,55 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Moon, Sun, Laptop } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { useSettingsStore } from "@/store/useSettingsStore"; export function ThemeSelectBox() { - const { setTheme } = useSettingsStore(); - return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon", children: [_jsx(Sun, { className: "h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" }), _jsx(Moon, { className: "absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" }), _jsx("span", { className: "sr-only", children: "\uD14C\uB9C8 \uBCC0\uACBD" })] }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => setTheme("light"), children: [_jsx(Sun, { className: "mr-2 h-4 w-4" }), "Light"] }), _jsxs(DropdownMenuItem, { onClick: () => setTheme("dark"), children: [_jsx(Moon, { className: "mr-2 h-4 w-4" }), "Dark"] }), _jsxs(DropdownMenuItem, { onClick: () => setTheme("system"), children: [_jsx(Laptop, { className: "mr-2 h-4 w-4" }), "System"] })] })] })); + const { setTheme } = useSettingsStore(); + return _jsxs(DropdownMenu, { + children: [ + _jsx(DropdownMenuTrigger, { + asChild: true, + children: _jsxs(Button, { + variant: "ghost", + size: "icon", + children: [ + _jsx(Sun, { + className: + "h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0", + }), + _jsx(Moon, { + className: + "absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100", + }), + _jsx("span", { + className: "sr-only", + children: "\uD14C\uB9C8 \uBCC0\uACBD", + }), + ], + }), + }), + _jsxs(DropdownMenuContent, { + align: "end", + children: [ + _jsxs(DropdownMenuItem, { + onClick: () => setTheme("light"), + children: [_jsx(Sun, { className: "mr-2 h-4 w-4" }), "Light"], + }), + _jsxs(DropdownMenuItem, { + onClick: () => setTheme("dark"), + children: [_jsx(Moon, { className: "mr-2 h-4 w-4" }), "Dark"], + }), + _jsxs(DropdownMenuItem, { + onClick: () => setTheme("system"), + children: [_jsx(Laptop, { className: "mr-2 h-4 w-4" }), "System"], + }), + ], + }), + ], + }); } diff --git a/viewer/src/components/UserProfileBox.js b/viewer/src/components/UserProfileBox.js index f8a36f4..1678b25 100644 --- a/viewer/src/components/UserProfileBox.js +++ b/viewer/src/components/UserProfileBox.js @@ -2,5 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { CircleUser } from "lucide-react"; import { Button } from "./ui/button"; export function UserProfileBox() { - return (_jsxs(Button, { variant: "ghost", size: "icon", children: [_jsx(CircleUser, {}), _jsx("span", { className: "sr-only", children: "\uC0AC\uC6A9\uC790 \uD504\uB85C\uD544" })] })); + return _jsxs(Button, { + variant: "ghost", + size: "icon", + children: [ + _jsx(CircleUser, {}), + _jsx("span", { + className: "sr-only", + children: "\uC0AC\uC6A9\uC790 \uD504\uB85C\uD544", + }), + ], + }); } diff --git a/viewer/src/components/providers/ThemeProvider.d.ts b/viewer/src/components/providers/ThemeProvider.d.ts index 83ecded..ea2a6cd 100644 --- a/viewer/src/components/providers/ThemeProvider.d.ts +++ b/viewer/src/components/providers/ThemeProvider.d.ts @@ -1,5 +1,6 @@ interface ThemeProviderProps { - children: React.ReactNode; + children: React.ReactNode; } -export declare function ThemeProvider({ children }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element; -export {}; +export declare function ThemeProvider({ + children, +}: ThemeProviderProps): import("react/jsx-runtime").JSX.Element; diff --git a/viewer/src/components/providers/ThemeProvider.js b/viewer/src/components/providers/ThemeProvider.js index 1b0c7f0..87fcb89 100644 --- a/viewer/src/components/providers/ThemeProvider.js +++ b/viewer/src/components/providers/ThemeProvider.js @@ -2,19 +2,19 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime"; import { useEffect } from "react"; import { useSettingsStore } from "@/store/useSettingsStore"; export function ThemeProvider({ children }) { - const theme = useSettingsStore((state) => state.theme); - useEffect(() => { - const root = window.document.documentElement; - root.classList.remove("light", "dark"); - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light"; - root.classList.add(systemTheme); - return; - } - root.classList.add(theme); - }, [theme]); - return _jsx(_Fragment, { children: children }); + const theme = useSettingsStore((state) => state.theme); + useEffect(() => { + const root = window.document.documentElement; + root.classList.remove("light", "dark"); + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + root.classList.add(systemTheme); + return; + } + root.classList.add(theme); + }, [theme]); + return _jsx(_Fragment, { children: children }); } diff --git a/viewer/src/components/ui/button.d.ts b/viewer/src/components/ui/button.d.ts index 38cf895..aa71c25 100644 --- a/viewer/src/components/ui/button.d.ts +++ b/viewer/src/components/ui/button.d.ts @@ -1,10 +1,29 @@ import type * as React from "react"; import { type VariantProps } from "class-variance-authority"; -declare const buttonVariants: (props?: ({ - variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined; - size?: "default" | "sm" | "lg" | "icon" | null | undefined; -} & import("class-variance-authority/types").ClassProp) | undefined) => string; -declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean; -}): import("react/jsx-runtime").JSX.Element; +declare const buttonVariants: ( + props?: + | ({ + variant?: + | "link" + | "default" + | "destructive" + | "outline" + | "secondary" + | "ghost" + | null + | undefined; + size?: "default" | "sm" | "lg" | "icon" | null | undefined; + } & import("class-variance-authority/types").ClassProp) + | undefined, +) => string; +declare function Button({ + className, + variant, + size, + asChild, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }): import("react/jsx-runtime").JSX.Element; export { Button, buttonVariants }; diff --git a/viewer/src/components/ui/button.js b/viewer/src/components/ui/button.js index dc1b063..47649a1 100644 --- a/viewer/src/components/ui/button.js +++ b/viewer/src/components/ui/button.js @@ -2,30 +2,42 @@ import { jsx as _jsx } from "react/jsx-runtime"; import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; import { cn } from "@/lib/utils"; -const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { - variants: { - variant: { - default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, -}); +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); function Button({ className, variant, size, asChild = false, ...props }) { - const Comp = asChild ? Slot : "button"; - return (_jsx(Comp, { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ...props })); + const Comp = asChild ? Slot : "button"; + return _jsx(Comp, { + "data-slot": "button", + className: cn(buttonVariants({ variant, size, className })), + ...props, + }); } export { Button, buttonVariants }; diff --git a/viewer/src/components/ui/calendar.d.ts b/viewer/src/components/ui/calendar.d.ts index 676b993..90c9573 100644 --- a/viewer/src/components/ui/calendar.d.ts +++ b/viewer/src/components/ui/calendar.d.ts @@ -1,8 +1,24 @@ import * as React from "react"; import { DayButton, DayPicker } from "react-day-picker"; import { Button } from "@/components/ui/button"; -declare function Calendar({ className, classNames, showOutsideDays, captionLayout, buttonVariant, formatters, components, ...props }: React.ComponentProps & { - buttonVariant?: React.ComponentProps["variant"]; +declare function Calendar({ + className, + classNames, + showOutsideDays, + captionLayout, + buttonVariant, + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"]; }): import("react/jsx-runtime").JSX.Element; -declare function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps): import("react/jsx-runtime").JSX.Element; +declare function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps< + typeof DayButton +>): import("react/jsx-runtime").JSX.Element; export { Calendar, CalendarDayButton }; diff --git a/viewer/src/components/ui/calendar.js b/viewer/src/components/ui/calendar.js index 18c3cbf..4efd963 100644 --- a/viewer/src/components/ui/calendar.js +++ b/viewer/src/components/ui/calendar.js @@ -1,73 +1,187 @@ import { jsx as _jsx } from "react/jsx-runtime"; import * as React from "react"; -import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, } from "lucide-react"; -import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react"; +import { DayPicker, getDefaultClassNames } from "react-day-picker"; import { cn } from "@/lib/utils"; import { Button, buttonVariants } from "@/components/ui/button"; -function Calendar({ className, classNames, showOutsideDays = true, captionLayout = "label", buttonVariant = "ghost", formatters, components, ...props }) { - const defaultClassNames = getDefaultClassNames(); - return (_jsx(DayPicker, { showOutsideDays: showOutsideDays, className: cn("bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", String.raw `rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw `rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className), captionLayout: captionLayout, formatters: { - formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }), - ...formatters, - }, classNames: { - root: cn("w-fit", defaultClassNames.root), - months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months), - month: cn("flex w-full flex-col gap-4", defaultClassNames.month), - nav: cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", defaultClassNames.nav), - button_previous: cn(buttonVariants({ variant: buttonVariant }), "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", defaultClassNames.button_previous), - button_next: cn(buttonVariants({ variant: buttonVariant }), "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", defaultClassNames.button_next), - month_caption: cn("flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", defaultClassNames.month_caption), - dropdowns: cn("flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", defaultClassNames.dropdowns), - dropdown_root: cn("has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border", defaultClassNames.dropdown_root), - dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown), - caption_label: cn("select-none font-medium", captionLayout === "label" - ? "text-sm" - : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", defaultClassNames.caption_label), - table: "w-full border-collapse", - weekdays: cn("flex", defaultClassNames.weekdays), - weekday: cn("text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", defaultClassNames.weekday), - week: cn("mt-2 flex w-full", defaultClassNames.week), - week_number_header: cn("w-[--cell-size] select-none", defaultClassNames.week_number_header), - week_number: cn("text-muted-foreground select-none text-[0.8rem]", defaultClassNames.week_number), - day: cn("group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", defaultClassNames.day), - range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start), - range_middle: cn("rounded-none", defaultClassNames.range_middle), - range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end), - today: cn("bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", defaultClassNames.today), - outside: cn("text-muted-foreground aria-selected:text-muted-foreground", defaultClassNames.outside), - disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled), - hidden: cn("invisible", defaultClassNames.hidden), - ...classNames, - }, components: { - Root: ({ className, rootRef, ...props }) => { - return (_jsx("div", { "data-slot": "calendar", ref: rootRef, className: cn(className), ...props })); - }, - Chevron: ({ className, orientation, ...props }) => { - if (orientation === "left") { - return (_jsx(ChevronLeftIcon, { className: cn("size-4", className), ...props })); - } - if (orientation === "right") { - return (_jsx(ChevronRightIcon, { className: cn("size-4", className), ...props })); - } - return (_jsx(ChevronDownIcon, { className: cn("size-4", className), ...props })); - }, - DayButton: CalendarDayButton, - WeekNumber: ({ children, ...props }) => { - return (_jsx("td", { ...props, children: _jsx("div", { className: "flex size-[--cell-size] items-center justify-center text-center", children: children }) })); - }, - ...components, - }, ...props })); +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}) { + const defaultClassNames = getDefaultClassNames(); + return _jsx(DayPicker, { + showOutsideDays: showOutsideDays, + className: cn( + "bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", + String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + ), + captionLayout: captionLayout, + formatters: { + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }, + classNames: { + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months, + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_next, + ), + month_caption: cn( + "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", + defaultClassNames.month_caption, + ), + dropdowns: cn( + "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border", + defaultClassNames.dropdown_root, + ), + dropdown: cn( + "bg-popover absolute inset-0 opacity-0", + defaultClassNames.dropdown, + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", + defaultClassNames.caption_label, + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", + defaultClassNames.weekday, + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-[--cell-size] select-none", + defaultClassNames.week_number_header, + ), + week_number: cn( + "text-muted-foreground select-none text-[0.8rem]", + defaultClassNames.week_number, + ), + day: cn( + "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", + defaultClassNames.day, + ), + range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today, + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside, + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled, + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }, + components: { + Root: ({ className, rootRef, ...props }) => { + return _jsx("div", { + "data-slot": "calendar", + ref: rootRef, + className: cn(className), + ...props, + }); + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return _jsx(ChevronLeftIcon, { + className: cn("size-4", className), + ...props, + }); + } + if (orientation === "right") { + return _jsx(ChevronRightIcon, { + className: cn("size-4", className), + ...props, + }); + } + return _jsx(ChevronDownIcon, { + className: cn("size-4", className), + ...props, + }); + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return _jsx("td", { + ...props, + children: _jsx("div", { + className: + "flex size-[--cell-size] items-center justify-center text-center", + children: children, + }), + }); + }, + ...components, + }, + ...props, + }); } function CalendarDayButton({ className, day, modifiers, ...props }) { - const defaultClassNames = getDefaultClassNames(); - const ref = React.useRef(null); - React.useEffect(() => { - if (modifiers.focused) - ref.current?.focus(); - }, [modifiers.focused]); - return (_jsx(Button, { ref: ref, variant: "ghost", size: "icon", "data-day": day.date.toLocaleDateString(), "data-selected-single": modifiers.selected && - !modifiers.range_start && - !modifiers.range_end && - !modifiers.range_middle, "data-range-start": modifiers.range_start, "data-range-end": modifiers.range_end, "data-range-middle": modifiers.range_middle, className: cn("data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70", defaultClassNames.day, className), ...props })); + const defaultClassNames = getDefaultClassNames(); + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); + return _jsx(Button, { + ref: ref, + variant: "ghost", + size: "icon", + "data-day": day.date.toLocaleDateString(), + "data-selected-single": + modifiers.selected && + !modifiers.range_start && + !modifiers.range_end && + !modifiers.range_middle, + "data-range-start": modifiers.range_start, + "data-range-end": modifiers.range_end, + "data-range-middle": modifiers.range_middle, + className: cn( + "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70", + defaultClassNames.day, + className, + ), + ...props, + }); } export { Calendar, CalendarDayButton }; diff --git a/viewer/src/components/ui/calendar.tsx b/viewer/src/components/ui/calendar.tsx index 665b029..0b596da 100644 --- a/viewer/src/components/ui/calendar.tsx +++ b/viewer/src/components/ui/calendar.tsx @@ -1,211 +1,211 @@ -import * as React from "react" +import * as React from "react"; import { - ChevronDownIcon, - ChevronLeftIcon, - ChevronRightIcon, -} from "lucide-react" -import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react"; +import { type DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; -import { cn } from "@/lib/utils" -import { Button, buttonVariants } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +import { Button, buttonVariants } from "@/components/ui/button"; function Calendar({ - className, - classNames, - showOutsideDays = true, - captionLayout = "label", - buttonVariant = "ghost", - formatters, - components, - ...props + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props }: React.ComponentProps & { - buttonVariant?: React.ComponentProps["variant"] + buttonVariant?: React.ComponentProps["variant"]; }) { - const defaultClassNames = getDefaultClassNames() + const defaultClassNames = getDefaultClassNames(); - return ( - svg]:rotate-180`, - String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, - className - )} - captionLayout={captionLayout} - formatters={{ - formatMonthDropdown: (date) => - date.toLocaleString("default", { month: "short" }), - ...formatters, - }} - classNames={{ - root: cn("w-fit", defaultClassNames.root), - months: cn( - "relative flex flex-col gap-4 md:flex-row", - defaultClassNames.months - ), - month: cn("flex w-full flex-col gap-4", defaultClassNames.month), - nav: cn( - "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", - defaultClassNames.nav - ), - button_previous: cn( - buttonVariants({ variant: buttonVariant }), - "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", - defaultClassNames.button_previous - ), - button_next: cn( - buttonVariants({ variant: buttonVariant }), - "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", - defaultClassNames.button_next - ), - month_caption: cn( - "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", - defaultClassNames.month_caption - ), - dropdowns: cn( - "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", - defaultClassNames.dropdowns - ), - dropdown_root: cn( - "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border", - defaultClassNames.dropdown_root - ), - dropdown: cn( - "bg-popover absolute inset-0 opacity-0", - defaultClassNames.dropdown - ), - caption_label: cn( - "select-none font-medium", - captionLayout === "label" - ? "text-sm" - : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", - defaultClassNames.caption_label - ), - table: "w-full border-collapse", - weekdays: cn("flex", defaultClassNames.weekdays), - weekday: cn( - "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", - defaultClassNames.weekday - ), - week: cn("mt-2 flex w-full", defaultClassNames.week), - week_number_header: cn( - "w-[--cell-size] select-none", - defaultClassNames.week_number_header - ), - week_number: cn( - "text-muted-foreground select-none text-[0.8rem]", - defaultClassNames.week_number - ), - day: cn( - "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", - defaultClassNames.day - ), - range_start: cn( - "bg-accent rounded-l-md", - defaultClassNames.range_start - ), - range_middle: cn("rounded-none", defaultClassNames.range_middle), - range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end), - today: cn( - "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", - defaultClassNames.today - ), - outside: cn( - "text-muted-foreground aria-selected:text-muted-foreground", - defaultClassNames.outside - ), - disabled: cn( - "text-muted-foreground opacity-50", - defaultClassNames.disabled - ), - hidden: cn("invisible", defaultClassNames.hidden), - ...classNames, - }} - components={{ - Root: ({ className, rootRef, ...props }) => { - return ( -
- ) - }, - Chevron: ({ className, orientation, ...props }) => { - if (orientation === "left") { - return ( - - ) - } + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months, + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_next, + ), + month_caption: cn( + "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", + defaultClassNames.month_caption, + ), + dropdowns: cn( + "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border", + defaultClassNames.dropdown_root, + ), + dropdown: cn( + "bg-popover absolute inset-0 opacity-0", + defaultClassNames.dropdown, + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", + defaultClassNames.caption_label, + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", + defaultClassNames.weekday, + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-[--cell-size] select-none", + defaultClassNames.week_number_header, + ), + week_number: cn( + "text-muted-foreground select-none text-[0.8rem]", + defaultClassNames.week_number, + ), + day: cn( + "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", + defaultClassNames.day, + ), + range_start: cn( + "bg-accent rounded-l-md", + defaultClassNames.range_start, + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today, + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside, + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled, + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ); + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ); + } - if (orientation === "right") { - return ( - - ) - } + if (orientation === "right") { + return ( + + ); + } - return ( - - ) - }, - DayButton: CalendarDayButton, - WeekNumber: ({ children, ...props }) => { - return ( - -
- {children} -
- - ) - }, - ...components, - }} - {...props} - /> - ) + return ( + + ); + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ); + }, + ...components, + }} + {...props} + /> + ); } function CalendarDayButton({ - className, - day, - modifiers, - ...props + className, + day, + modifiers, + ...props }: React.ComponentProps) { - const defaultClassNames = getDefaultClassNames() + const defaultClassNames = getDefaultClassNames(); - const ref = React.useRef(null) - React.useEffect(() => { - if (modifiers.focused) ref.current?.focus() - }, [modifiers.focused]) + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); - return ( -