From 99a2893d5194334af55defab7ce54286e05e1007 Mon Sep 17 00:00:00 2001 From: Peace Date: Thu, 28 Aug 2025 17:27:31 +0900 Subject: [PATCH] fec --- web/package-lock.json | 147 +++++++++++++ web/package.json | 4 + web/src/app/layout.tsx | 8 +- web/src/app/login/page.tsx | 106 +++++---- web/src/app/me/page.tsx | 40 ++-- web/src/app/page.tsx | 6 +- web/src/app/sensor-groups/[groupId]/page.tsx | 90 ++++---- web/src/app/sensor-groups/page.tsx | 84 ++++---- web/src/app/shadcn/alert-dialog/page.tsx | 50 +++++ web/src/app/shadcn/badge/page.tsx | 31 +++ web/src/app/shadcn/calendar/page.tsx | 40 ++++ web/src/app/signup/page.tsx | 182 ++++++++-------- web/src/components/app-header.tsx | 42 ++++ web/src/components/ui/alert-dialog.tsx | 157 ++++++++++++++ web/src/components/ui/alert.tsx | 66 ++++++ web/src/components/ui/badge.tsx | 46 ++++ web/src/components/ui/button.tsx | 49 +++-- web/src/components/ui/calendar.tsx | 213 +++++++++++++++++++ web/src/components/ui/navigation-menu.tsx | 168 +++++++++++++++ 19 files changed, 1243 insertions(+), 286 deletions(-) create mode 100644 web/src/app/shadcn/alert-dialog/page.tsx create mode 100644 web/src/app/shadcn/badge/page.tsx create mode 100644 web/src/app/shadcn/calendar/page.tsx create mode 100644 web/src/components/ui/alert-dialog.tsx create mode 100644 web/src/components/ui/alert.tsx create mode 100644 web/src/components/ui/badge.tsx create mode 100644 web/src/components/ui/calendar.tsx create mode 100644 web/src/components/ui/navigation-menu.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 78aea70..72ffb37 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,9 +9,11 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -19,10 +21,12 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.540.0", "luxon": "^3.7.1", "next": "15.5.0", "react": "19.1.0", + "react-day-picker": "^9.9.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", "recharts": "^3.1.2", @@ -57,6 +61,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -1045,6 +1055,34 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -1151,6 +1189,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -1343,6 +1417,42 @@ } } }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -3549,6 +3659,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -6414,6 +6540,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.9.0.tgz", + "integrity": "sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/web/package.json b/web/package.json index 17a49f5..c7227c3 100644 --- a/web/package.json +++ b/web/package.json @@ -10,9 +10,11 @@ }, "dependencies": { "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -20,10 +22,12 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.540.0", "luxon": "^3.7.1", "next": "15.5.0", "react": "19.1.0", + "react-day-picker": "^9.9.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", "recharts": "^3.1.2", diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 343ef47..1f371f4 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -15,10 +15,12 @@ export default function RootLayout({ }>) { return ( - + - -
{children}
+
+ +
{children}
+
diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index 651c105..8ed17fd 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -70,64 +70,62 @@ export default function LoginPage() { }; return ( -
-
- - - 로그인 - - - {/* Name */} -
- - ( - - - - - - - )} - /> +
+ + + 로그인 + + + {/* Name */} + + + ( + + + + + + + )} + /> - {/* Password */} - ( - - - - - - )} - /> + {/* Password */} + ( + + + + + + )} + /> - + - {error &&

{error}

} + {error &&

{error}

} -

- 아직 계정이 없으신가요?{' '} - - 회원가입 - -

- - -
-
-
+

+ 아직 계정이 없으신가요?{' '} + + 회원가입 + +

+ + +
+
); } diff --git a/web/src/app/me/page.tsx b/web/src/app/me/page.tsx index 1f9a8a2..b016562 100644 --- a/web/src/app/me/page.tsx +++ b/web/src/app/me/page.tsx @@ -13,28 +13,26 @@ export default function MePage() { {error &&

조회 실패

} {!isLoading && !error && data && ( -
- - -
- {data.name?.[0]} -
-
- {data.name ?? '이름 없음'} -
ID: {data.id}
-
-
+ + +
+ {data.name?.[0]} +
+
+ {data.name ?? '이름 없음'} +
ID: {data.id}
+
+
- - {data.email && ( -
- 이메일 - {data.email} -
- )} -
-
-
+ + {data.email && ( +
+ 이메일 + {data.email} +
+ )} +
+ )} ); diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 215f581..e36355f 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -31,7 +31,7 @@ export default function Home() { const localNow = timezone ? now.setZone(timezone) : now; return ( -
+
DateTime UI Sample @@ -54,7 +54,7 @@ export default function Home() {

Current time: {localNow.toFormat('yyyy-MM-dd HH:mm:ss (ZZZZ)')}

Now: {now.toFormat('yyyy-MM-dd HH:mm:ss (ZZZZ)')}

-

Default date now: {defNow.toISOString()}

+

Default date now: {defNow.toISOString().split('.')[0] + 'Z'}

(Luxon: realtime rendering, local timezone support) @@ -85,6 +85,6 @@ export default function Home() { (Luxon: realtime rendering, local timezone support)
-
+
); } diff --git a/web/src/app/sensor-groups/[groupId]/page.tsx b/web/src/app/sensor-groups/[groupId]/page.tsx index d6d9451..6490fa7 100644 --- a/web/src/app/sensor-groups/[groupId]/page.tsx +++ b/web/src/app/sensor-groups/[groupId]/page.tsx @@ -22,56 +22,54 @@ export default function GroupSensorsPage() { return ( -
-
-
- -

센서 그룹

-
- +
+
+ +

센서 그룹

+ +
- {isLoading &&

불러오는 중...

} - {isError &&

불러오지 못했습니다.

} - {!isLoading && !isError && ( -
- - - - ID - 이름 - 단위 - 데이터 - - - - {data && data.length > 0 ? ( - data.map((s) => ( - - {s.id} - {s.name} - {s.unit ?? '-'} - - - - - )) - ) : ( - - - 데이터가 없습니다. + {isLoading &&

불러오는 중...

} + {isError &&

불러오지 못했습니다.

} + {!isLoading && !isError && ( +
+
+ + + ID + 이름 + 단위 + 데이터 + + + + {data && data.length > 0 ? ( + data.map((s) => ( + + {s.id} + {s.name} + {s.unit ?? '-'} + + - )} - -
-
- )} -
+ )) + ) : ( + + + 데이터가 없습니다. + + + )} + + + + )}
); } diff --git a/web/src/app/sensor-groups/page.tsx b/web/src/app/sensor-groups/page.tsx index 8c66d8b..7dbb77a 100644 --- a/web/src/app/sensor-groups/page.tsx +++ b/web/src/app/sensor-groups/page.tsx @@ -18,53 +18,49 @@ export default function SensorGroupPage() { return ( -
-
-

센서 그룹

- -
+
+

센서 그룹

+ +
- {isLoading &&

불러오는 중...

} - {isError &&

불러오지 못했습니다.

} - {!isLoading && !isError && ( -
- - - - ID - 이름 - 설명 - 상세 - - - - {data && data.length > 0 ? ( - data.map((g) => ( - - {g.id} - {g.name} - - {g.description ?? '-'} - - - - - - )) - ) : ( - - - 데이터가 없습니다. + {isLoading &&

불러오는 중...

} + {isError &&

불러오지 못했습니다.

} + {!isLoading && !isError && ( +
+
+ + + ID + 이름 + 설명 + 상세 + + + + {data && data.length > 0 ? ( + data.map((g) => ( + + {g.id} + {g.name} + {g.description ?? '-'} + + - )} - -
-
- )} -
+ )) + ) : ( + + + 데이터가 없습니다. + + + )} + + + + )}
); } diff --git a/web/src/app/shadcn/alert-dialog/page.tsx b/web/src/app/shadcn/alert-dialog/page.tsx new file mode 100644 index 0000000..e839ac6 --- /dev/null +++ b/web/src/app/shadcn/alert-dialog/page.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogContent, + AlertDialogTitle, + AlertDialogTrigger, + AlertDialogFooter, + AlertDialogCancel, +} from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import { useState } from 'react'; + +export default function AlertDialogPage() { + const [dialogResult, setDialogResult] = useState(''); + + function handleContinue() { + setDialogResult('계속을 눌렀습니다.'); + } + + function handleCancel() { + setDialogResult('취소를 눌렀습니다.'); + } + + return ( + <> + + + + + + + 작업을 확인하세요 + + 이 작업은 되돌릴 수 없습니다. 진행하시겠습니까? + + + + 취소 + 계속 + + + + {dialogResult &&

{dialogResult}

} + + ); +} diff --git a/web/src/app/shadcn/badge/page.tsx b/web/src/app/shadcn/badge/page.tsx new file mode 100644 index 0000000..2e93678 --- /dev/null +++ b/web/src/app/shadcn/badge/page.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Badge } from '@/components/ui/badge'; +import { BadgeCheckIcon } from 'lucide-react'; + +export default function BadgePage() { + return ( + <> +
+
+ Badge + Secondary + Outline + Destructive +
+
+ + + Varified + + + 99 + + + 100+ + +
+
+ + ); +} diff --git a/web/src/app/shadcn/calendar/page.tsx b/web/src/app/shadcn/calendar/page.tsx new file mode 100644 index 0000000..bc0c58c --- /dev/null +++ b/web/src/app/shadcn/calendar/page.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { Calendar } from '@/components/ui/calendar'; +import { DateTime } from 'luxon'; +import { useState } from 'react'; + +export default function CalendarPage() { + const [date, setDate] = useState(new Date()); + const [luxonDate, setLuxonDate] = useState(DateTime.now()); + + return ( + <> +
+
+ +

{date?.toLocaleDateString()}

+
+ +
+ setLuxonDate(d ? DateTime.fromJSDate(d) : undefined)} + className="rounded-md border shadow-sm" + captionLayout="dropdown" + /> +

+ {luxonDate?.toJSDate().toLocaleDateString()} +

+
+
+ + ); +} diff --git a/web/src/app/signup/page.tsx b/web/src/app/signup/page.tsx index 2b1b613..92350d9 100644 --- a/web/src/app/signup/page.tsx +++ b/web/src/app/signup/page.tsx @@ -73,104 +73,102 @@ export default function SignupPage() { }; return ( -
-
- - - 회원가입 - - -
- - {/* name */} - ( - - - - - - - )} - /> +
+ + + 회원가입 + + + + + {/* name */} + ( + + + + + + + )} + /> - {/* email */} - ( - - - - - - - )} - /> + {/* email */} + ( + + + + + + + )} + /> - {/* password */} - ( - - - - - - - )} - /> + {/* password */} + ( + + + + + + + )} + /> - {/* confirmPassword */} - ( - - - - - - - )} - /> + {/* confirmPassword */} + ( + + + + + + + )} + /> - + - {error &&

{error}

} - {ok &&

{ok}

} + {error &&

{error}

} + {ok &&

{ok}

} -

- 이미 계정이 있으신가요?{' '} - - 로그인 - -

- - -
-
-
+

+ 이미 계정이 있으신가요?{' '} + + 로그인 + +

+ + +
+
); } diff --git a/web/src/components/app-header.tsx b/web/src/components/app-header.tsx index 2a69368..43a56b0 100644 --- a/web/src/components/app-header.tsx +++ b/web/src/components/app-header.tsx @@ -8,6 +8,14 @@ import { useQueryClient } from '@tanstack/react-query'; import { hasToken, setToken } from '@/lib/token'; import { useMe } from '@/hooks/useMe'; import { Button } from './ui/button'; +import { + NavigationMenu, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, +} from './ui/navigation-menu'; +import { NavigationMenuContent } from '@radix-ui/react-navigation-menu'; const nav = [ { href: '/', label: '홈' }, @@ -57,6 +65,40 @@ export default function AppHeader() { + + + + + shadcn + + + +
    +
  • + + + Alert Dialog + + + + + Badge + + + + + Calendar + + +
  • +
+
+
+
+
+ + + {authed ? (
{/* 아바타 + 이름: 로딩 중이면 스켈레톤 */} diff --git a/web/src/components/ui/alert-dialog.tsx b/web/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..0863e40 --- /dev/null +++ b/web/src/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/web/src/components/ui/alert.tsx b/web/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/web/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/web/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx index b579275..a2df8dc 100644 --- a/web/src/components/ui/button.tsx +++ b/web/src/components/ui/button.tsx @@ -1,36 +1,39 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from '@/lib/utils'; +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', + 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', + "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', + "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', + 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', + variant: "default", + size: "default", }, - }, -); + } +) function Button({ className, @@ -38,11 +41,11 @@ function Button({ size, asChild = false, ...props -}: React.ComponentProps<'button'> & +}: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean; + asChild?: boolean }) { - const Comp = asChild ? Slot : 'button'; + const Comp = asChild ? Slot : "button" return ( - ); + ) } -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/web/src/components/ui/calendar.tsx b/web/src/components/ui/calendar.tsx new file mode 100644 index 0000000..4d7c46a --- /dev/null +++ b/web/src/components/ui/calendar.tsx @@ -0,0 +1,213 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { DayButton, 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 +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + 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( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute bg-popover inset-0 opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", 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 ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( +