From 00b4101aa1435780031e2f7b9942336624c7e738 Mon Sep 17 00:00:00 2001 From: "Jessada.n" Date: Mon, 17 Feb 2025 01:25:27 +0700 Subject: [PATCH] fix: update import paths for utility functions and add new layout components --- frontend/package-lock.json | 93 ++++++++++- frontend/package.json | 6 +- frontend/src/App.tsx | 54 +------ frontend/src/components/Navbar.tsx | 9 +- frontend/src/components/RequireAuth.tsx | 10 +- .../src/components/animata/card/flip-card.tsx | 38 +++++ .../src/components/layouts/AuthLayout.tsx | 15 ++ .../src/components/layouts/PageLayout.tsx | 15 ++ frontend/src/components/ui/alert-dialog.tsx | 2 +- frontend/src/components/ui/alert.tsx | 59 +++++++ frontend/src/components/ui/badge.tsx | 2 +- frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/calendar.tsx | 2 +- frontend/src/components/ui/card.tsx | 2 +- frontend/src/components/ui/chart.tsx | 2 +- .../src/components/ui/datetime-picker.tsx | 2 +- frontend/src/components/ui/dropdown-menu.tsx | 2 +- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/label.tsx | 2 +- frontend/src/components/ui/popover.tsx | 2 +- frontend/src/components/ui/progress.tsx | 2 +- frontend/src/components/ui/scroll-area.tsx | 2 +- frontend/src/components/ui/select.tsx | 2 +- frontend/src/components/ui/skeleton.tsx | 2 +- frontend/src/components/ui/tabs.tsx | 53 ++++++ frontend/src/components/ui/textarea.tsx | 2 +- frontend/src/config/config.ts | 6 +- frontend/src/hooks/UseAuth.tsx | 153 ++++++++++++++++++ frontend/src/lib/Constants.ts | 21 +++ frontend/src/lib/Validation.ts | 14 ++ frontend/src/lib/utils.ts | 8 +- frontend/src/main.tsx | 2 +- frontend/src/pages/Home.tsx | 15 +- frontend/src/pages/SignIn.tsx | 122 -------------- frontend/src/pages/auth/Callback.tsx | 35 ++++ frontend/src/pages/auth/LogIn.tsx | 47 ++++++ frontend/src/routes/Index.tsx | 68 ++++++++ frontend/src/routes/PrivateRoute.tsx | 30 ++++ frontend/src/routes/PubilcRoute.tsx | 24 +++ 39 files changed, 713 insertions(+), 216 deletions(-) create mode 100644 frontend/src/components/animata/card/flip-card.tsx create mode 100644 frontend/src/components/layouts/AuthLayout.tsx create mode 100644 frontend/src/components/layouts/PageLayout.tsx create mode 100644 frontend/src/components/ui/alert.tsx create mode 100644 frontend/src/components/ui/tabs.tsx create mode 100644 frontend/src/hooks/UseAuth.tsx create mode 100644 frontend/src/lib/Constants.ts create mode 100644 frontend/src/lib/Validation.ts delete mode 100644 frontend/src/pages/SignIn.tsx create mode 100644 frontend/src/pages/auth/Callback.tsx create mode 100644 frontend/src/pages/auth/LogIn.tsx create mode 100644 frontend/src/routes/Index.tsx create mode 100644 frontend/src/routes/PrivateRoute.tsx create mode 100644 frontend/src/routes/PubilcRoute.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dcc9857..6b51ae2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "vite-react-typescript-starter", "version": "0.0.0", "dependencies": { + "@hookform/resolvers": "^4.1.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-icons": "^1.3.2", @@ -18,6 +19,7 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -29,10 +31,12 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", + "react-icons": "^5.4.0", "react-router-dom": "^6.22.2", "recharts": "^2.15.1", "tailwind-merge": "^3.0.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -911,6 +915,18 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@hookform/resolvers": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.0.tgz", + "integrity": "sha512-fX/uHKb+OOCpACLc6enuTQsf0ZpRrKbeBBPETg5PCPLCIYV6osP2Bw6ezuclM61lH+wBF9eXcuC0+BFh9XOEnQ==", + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001698" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", @@ -1725,6 +1741,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "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-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -2745,10 +2791,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", - "dev": true, + "version": "1.0.30001699", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", + "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", "funding": [ { "type": "opencollective", @@ -2762,7 +2807,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", @@ -4631,6 +4677,23 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-hot-toast": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.1.tgz", @@ -4647,6 +4710,15 @@ "react-dom": ">=16" } }, + "node_modules/react-icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5652,6 +5724,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/frontend/package.json b/frontend/package.json index ae07aec..ba78e94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^4.1.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-icons": "^1.3.2", @@ -20,6 +21,7 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -31,10 +33,12 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", + "react-icons": "^5.4.0", "react-router-dom": "^6.22.2", "recharts": "^2.15.1", "tailwind-merge": "^3.0.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" }, "devDependencies": { "@eslint/js": "^9.9.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d1fa3d..f172ddb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,60 +1,14 @@ -import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { Toaster } from "react-hot-toast"; -import Navbar from "./components/Navbar"; -import RequireAuth from "./components/RequireAuth"; -import Home from "./pages/Home"; -import PollDetails from "./pages/PollDetails"; -import CreatePoll from "./pages/CreatePoll"; -import SignIn from "./pages/SignIn"; -import { AuthProvider } from "./contexts/AuthContext"; -import { Dashboard } from "./pages/Dashboard"; +import { AuthProvider } from "./hooks/UseAuth"; +import { AppRoutes } from "./routes/Index"; function App() { return ( -
- -
- - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - -
- -
+ +
); diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 5439434..4236738 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,10 +1,11 @@ import { Link } from "react-router-dom"; import { Vote, LogOut, LogIn } from "lucide-react"; -import { useAuth } from "../contexts/AuthContext"; +// import { useAuth } from "../hook/AuthContext"; +import { useAuth } from "@/hooks/UseAuth"; import { Button } from "./ui/button"; export default function Navbar() { - const { user, isAdmin, signOut } = useAuth(); + const { user } = useAuth(); const appName = import.meta.env.VITE_APP_NAME; return ( @@ -17,7 +18,7 @@ export default function Navbar() {
- {user ? ( + {/* {user ? ( <> {isAdmin && ( - )} + )} */}
diff --git a/frontend/src/components/RequireAuth.tsx b/frontend/src/components/RequireAuth.tsx index 3e689a1..020e5ea 100644 --- a/frontend/src/components/RequireAuth.tsx +++ b/frontend/src/components/RequireAuth.tsx @@ -1,5 +1,5 @@ import { Navigate, useLocation } from "react-router-dom"; -import { useAuth } from "../contexts/AuthContext"; +import { useAuth } from "@/hooks/UseAuth"; import { SkeletonCard } from "./Skeleton"; interface RequireAuthProps { @@ -11,18 +11,18 @@ export default function RequireAuth({ children, requireAdmin = false, }: RequireAuthProps) { - const { user, isAdmin, loading } = useAuth(); + const { user, isLoading, isAuthenticated } = useAuth(); const location = useLocation(); - if (loading) { + if (isLoading) { return ; } if (!user) { - return ; + return ; } - if (requireAdmin && !isAdmin) { + if (requireAdmin && !isAuthenticated) { return ; } diff --git a/frontend/src/components/animata/card/flip-card.tsx b/frontend/src/components/animata/card/flip-card.tsx new file mode 100644 index 0000000..17d128c --- /dev/null +++ b/frontend/src/components/animata/card/flip-card.tsx @@ -0,0 +1,38 @@ +import { FC, ReactNode } from "react"; +import { cn } from "@/lib/Utils"; + +interface FlipCardProps extends React.HTMLAttributes { + flip?: boolean; // ควบคุมการ Flip + frontContent: ReactNode; + backContent: ReactNode; +} + +const FlipCard: FC = ({ flip = false, frontContent, backContent, className, ...props }) => { + return ( +
+
+ {/* Front - Google Auth */} +
+ {frontContent} +
+ + {/* Back - Guest Auth */} +
+ {backContent} +
+
+
+ ); +}; + +export default FlipCard; diff --git a/frontend/src/components/layouts/AuthLayout.tsx b/frontend/src/components/layouts/AuthLayout.tsx new file mode 100644 index 0000000..9af2322 --- /dev/null +++ b/frontend/src/components/layouts/AuthLayout.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from "react"; + +interface AuthLayoutProps { + children: ReactNode; +} + +export function AuthLayout({ children }: AuthLayoutProps) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/frontend/src/components/layouts/PageLayout.tsx b/frontend/src/components/layouts/PageLayout.tsx new file mode 100644 index 0000000..744d11d --- /dev/null +++ b/frontend/src/components/layouts/PageLayout.tsx @@ -0,0 +1,15 @@ +import Navbar from "@/components/Navbar"; + +interface PageLayoutProps { + children: React.ReactNode; +} +export function PageLayout({ children }: PageLayoutProps) { + // const [breadcrumbs] = useBreadcrumb(); + + return ( + <> + +
{children}
+ + ); +} diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index fa2b442..d202394 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -1,7 +1,7 @@ import * as React from "react" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" import { buttonVariants } from "@/components/ui/button" const AlertDialog = AlertDialogPrimitive.Root diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..2343756 --- /dev/null +++ b/frontend/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +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 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index e87d62b..c0c8383 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -1,7 +1,7 @@ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const badgeVariants = cva( "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 65d4fcd..9cf42bb 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -2,7 +2,7 @@ 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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", diff --git a/frontend/src/components/ui/calendar.tsx b/frontend/src/components/ui/calendar.tsx index 804fe21..b835d5a 100644 --- a/frontend/src/components/ui/calendar.tsx +++ b/frontend/src/components/ui/calendar.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { ChevronLeft, ChevronRight } from "lucide-react" import { DayPicker } from "react-day-picker" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" import { buttonVariants } from "@/components/ui/button" export type CalendarProps = React.ComponentProps diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index cabfbfc..ca65f23 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -1,6 +1,6 @@ import * as React from "react" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const Card = React.forwardRef< HTMLDivElement, diff --git a/frontend/src/components/ui/chart.tsx b/frontend/src/components/ui/chart.tsx index aedf2b2..3da6b52 100644 --- a/frontend/src/components/ui/chart.tsx +++ b/frontend/src/components/ui/chart.tsx @@ -1,7 +1,7 @@ import * as React from "react" import * as RechartsPrimitive from "recharts" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const diff --git a/frontend/src/components/ui/datetime-picker.tsx b/frontend/src/components/ui/datetime-picker.tsx index 29d6228..4952338 100644 --- a/frontend/src/components/ui/datetime-picker.tsx +++ b/frontend/src/components/ui/datetime-picker.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { CalendarIcon } from "@radix-ui/react-icons"; import { format } from "date-fns"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/Utils"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx index 9ff6568..e35b7f2 100644 --- a/frontend/src/components/ui/dropdown-menu.tsx +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -2,7 +2,7 @@ import * as React from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const DropdownMenu = DropdownMenuPrimitive.Root diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 69b64fb..1d0055f 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -1,6 +1,6 @@ import * as React from "react" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx index 683faa7..654890f 100644 --- a/frontend/src/components/ui/label.tsx +++ b/frontend/src/components/ui/label.tsx @@ -2,7 +2,7 @@ import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const labelVariants = cva( "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" diff --git a/frontend/src/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx index d82e714..9fe8d04 100644 --- a/frontend/src/components/ui/popover.tsx +++ b/frontend/src/components/ui/popover.tsx @@ -1,7 +1,7 @@ import * as React from "react" import * as PopoverPrimitive from "@radix-ui/react-popover" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const Popover = PopoverPrimitive.Root diff --git a/frontend/src/components/ui/progress.tsx b/frontend/src/components/ui/progress.tsx index 3fd47ad..852e7d4 100644 --- a/frontend/src/components/ui/progress.tsx +++ b/frontend/src/components/ui/progress.tsx @@ -1,7 +1,7 @@ import * as React from "react" import * as ProgressPrimitive from "@radix-ui/react-progress" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const Progress = React.forwardRef< React.ElementRef, diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx index cf253cf..1f13de7 100644 --- a/frontend/src/components/ui/scroll-area.tsx +++ b/frontend/src/components/ui/scroll-area.tsx @@ -1,7 +1,7 @@ import * as React from "react" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const ScrollArea = React.forwardRef< React.ElementRef, diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx index a84242c..aa2caf2 100644 --- a/frontend/src/components/ui/select.tsx +++ b/frontend/src/components/ui/select.tsx @@ -2,7 +2,7 @@ import * as React from "react" import * as SelectPrimitive from "@radix-ui/react-select" import { Check, ChevronDown, ChevronUp } from "lucide-react" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const Select = SelectPrimitive.Root diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx index d7e45f7..f5cd6f5 100644 --- a/frontend/src/components/ui/skeleton.tsx +++ b/frontend/src/components/ui/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" function Skeleton({ className, diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..85d83be --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/frontend/src/components/ui/textarea.tsx b/frontend/src/components/ui/textarea.tsx index e56b0af..717d596 100644 --- a/frontend/src/components/ui/textarea.tsx +++ b/frontend/src/components/ui/textarea.tsx @@ -1,6 +1,6 @@ import * as React from "react" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/Utils" const Textarea = React.forwardRef< HTMLTextAreaElement, diff --git a/frontend/src/config/config.ts b/frontend/src/config/config.ts index 68a4fe7..3d1c7fd 100644 --- a/frontend/src/config/config.ts +++ b/frontend/src/config/config.ts @@ -1,5 +1,9 @@ -export default { +export const config = { port: import.meta.env.PORT || 3000, apiUrl: import.meta.env.VITE_APP_API_URL || "http://localhost:5000", jwtSecret: import.meta.env.JWT_SECRET || "your-secret-key", + appUrlCallback: import.meta.env.VITE_APP_URL_CALLBACK || "http://localhost:3000/callback", + apiOpenIdConnectUrl: import.meta.env.VITE_APP_API_OPENID_CONNECT_URL || "http://localhost:5000", + appLogo: import.meta.env.VITE_APP_LOGO || "https://cdn-icons-png.flaticon.com/512/5968/5968292.png", + appName: import.meta.env.VITE_APP_NAME || "My App", }; diff --git a/frontend/src/hooks/UseAuth.tsx b/frontend/src/hooks/UseAuth.tsx new file mode 100644 index 0000000..b83681f --- /dev/null +++ b/frontend/src/hooks/UseAuth.tsx @@ -0,0 +1,153 @@ +import React, { + createContext, + useContext, + useState, + ReactNode, + useEffect + } from 'react' + import { axiosInstance } from '@/lib/Utils' + import { API_ENDPOINTS } from '@/lib/Constants' + import { config } from '@/config/Config' + import { IUser } from '@/interfaces/Interfaces' + + interface User extends IUser { + isGuest: boolean + } + + interface AuthContextProps { + user: User | null + isAuthenticated: boolean + isLoading: boolean + accessToken: string | null + refreshToken: string | null + // login: (email: string, password: string) => Promise + logout: () => Promise + getProfile: () => Promise + oauthLogin: (provider: 'discord' | 'github' | 'google') => Promise + } + + // ✅ สร้าง Context ให้ถูกต้อง + export const AuthContext = createContext(undefined); + + export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(null) + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [accessToken, setAccessToken] = useState(null) + const [refreshToken, setRefreshToken] = useState(null) + + // const login = async (email: string, password: string) => { + // setIsLoading(true) + // try { + // const response = await axiosInstance.post( + // `${config.apiUrl}${API_ENDPOINTS.AUTH.LOGIN}`, + // { email, password } + // ) + + // const data = response.data as { + // user: User + // credentials: { + // accessToken: string + // refreshToken: string + // } + // } + + // setUser({ + // id: data.user.id, + // firstName: data.user.firstName, + // lastName: data.user.lastName, + // email: data.user.email, + // avatar: data.user.avatar, + // createdAt: data.user.createdAt, + // updatedAt: data.user.updatedAt, + // deletedAt: data.user.deletedAt, + // events: data.user.events, + // polls: data.user.polls, + // whitelist: data.user.whitelist, + // votes: data.user.votes, + // userVotes: data.user.userVotes, + // dataLogs: data.user.dataLogs, + // isGuest: data.user + // }) + + // setIsAuthenticated(true) + // setAccessToken(data.credentials.accessToken) + // setRefreshToken(data.credentials.refreshToken) + + // localStorage.setItem('accessToken', data.credentials.accessToken) + // localStorage.setItem('refreshToken', data.credentials.refreshToken) + // } finally { + // setIsLoading(false) + // } + // } + + const oauthLogin = async (provider: 'discord' | 'github' | 'google') => { + const service = 'vote' + window.location.href = `${config.apiOpenIdConnectUrl}/auth/${provider}?service=${service}&redirect=${config.appUrlCallback}` + } + + const logout = async () => { + setIsLoading(true) + try { + localStorage.clear() + window.location.reload() + } finally { + setIsLoading(false) + } + } + + const getProfile = async () => { + setIsLoading(true) + try { + const response = await axiosInstance.get( + `${config.apiUrl}${API_ENDPOINTS.AUTH.ME}` + ) + const data = response.data as User + setUser({ + id: data.id, + firstName: data.firstName, + lastName: data.lastName, + email: data.email, + avatar: data.avatar, + createdAt: data.createdAt, + updatedAt: data.updatedAt, + isGuest: data.isGuest || false, + }) + setIsAuthenticated(true) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + getProfile() + }, []) + + return ( + + {children} + + ) + } + + // ✅ ใช้ useContext ให้ถูกต้อง + export const useAuth = (): AuthContextProps => { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context + } + \ No newline at end of file diff --git a/frontend/src/lib/Constants.ts b/frontend/src/lib/Constants.ts new file mode 100644 index 0000000..2980061 --- /dev/null +++ b/frontend/src/lib/Constants.ts @@ -0,0 +1,21 @@ +export const APP_NAME = 'React Enterprise App'; + +export const API_ENDPOINTS = { + AUTH: { + LOGIN: '/auth/login', + REGISTER: '/auth/register', + LOGOUT: '/auth/logout', + ME: '/auth/me', + }, + USERS: { + BASE: '/user', + PROFILE: '/user/profile', + }, +} as const; + +export const ROUTES = { + HOME: '/', // หน้าหลัก + LOGIN: '/login', + CALLBACK:'/callback', // เข้าสู่ระบบ + PROFILE: '/profile', // โปรไฟล์ AT03.1.2 +} as const; \ No newline at end of file diff --git a/frontend/src/lib/Validation.ts b/frontend/src/lib/Validation.ts new file mode 100644 index 0000000..36d4ea0 --- /dev/null +++ b/frontend/src/lib/Validation.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const loginSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string().min(8, 'Password must be at least 8 characters'), +}); + +export const registerSchema = loginSchema.extend({ + name: z.string().min(2, 'Name must be at least 2 characters'), + confirmPassword: z.string(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); \ No newline at end of file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index f0288c8..0cc4675 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,18 +1,20 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" import axios from 'axios'; -import config from "@/config/config"; +import { config } from "@/config/Config"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -// // // สร้าง axios instance +const getAccessToken = localStorage.getItem('accessToken') + +// สร้าง axios instance export const axiosInstance = axios.create({ baseURL: `${config.apiUrl}/api`, // URL ของ API ที่ต้องการ timeout: 10000, // กำหนด timeout ในการร้องขอ (10 วินาที) headers: { 'Content-Type': 'application/json', // ตั้งค่า headers เริ่มต้น - 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' // ถ้ามีการใช้ token + 'Authorization': `Bearer ${getAccessToken}` // ถ้ามีการใช้ token } }); \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ea9e363..a46835a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; +import App from './App'; import './index.css'; createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 9dc11f5..1b3cdec 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { Calendar, Clock, MoreHorizontal } from "lucide-react"; import { mockPolls } from "../lib/mockData"; -import { useAuth } from "../contexts/AuthContext"; +import { useAuth } from "@/hooks/UseAuth"; +// import { useAuth } from "../contexts/AuthContext"; import { Card, CardContent, @@ -20,10 +21,10 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ConfirmDialog } from "@/components/AlertDialog"; -import { axiosInstance } from "@/lib/utils"; +import { axiosInstance } from "@/lib/Utils"; -export default function Home() { - const { user, isAdmin } = useAuth(); +export default function HomePage() { + const { user } = useAuth(); const [polls, setPolls] = useState(mockPolls); const [selectedPollId, setSelectedPollId] = useState(null); const [isConfirming, setIsConfirming] = useState(false); @@ -74,7 +75,7 @@ export default function Home() { return ( - {(isAdmin || isPollActive) && ( + {( isPollActive) && ( View Poll - {isAdmin && ( + {/* {( */} -
- - or - -
- - - - -
- - ); -} diff --git a/frontend/src/pages/auth/Callback.tsx b/frontend/src/pages/auth/Callback.tsx new file mode 100644 index 0000000..23f2ac4 --- /dev/null +++ b/frontend/src/pages/auth/Callback.tsx @@ -0,0 +1,35 @@ +import { Loader2 } from 'lucide-react' + +function Callback () { + // get access token and refresh token from url query parameters + const urlParams = new URLSearchParams(window.location.search) + + const accessToken = urlParams.get('accessToken') + const refreshToken = urlParams.get('refreshToken') + + console.log(accessToken) + console.log(refreshToken) + + + // set access token to local storage + if (accessToken) { + localStorage.setItem('accessToken', accessToken) + } + if (refreshToken) { + localStorage.setItem('refreshToken', refreshToken) + } + + if(!accessToken || !refreshToken){ + window.location.href = '/login' + } + + window.location.href = '/' + + return ( +
+ +
+ ) +} + +export default Callback \ No newline at end of file diff --git a/frontend/src/pages/auth/LogIn.tsx b/frontend/src/pages/auth/LogIn.tsx new file mode 100644 index 0000000..50dd15f --- /dev/null +++ b/frontend/src/pages/auth/LogIn.tsx @@ -0,0 +1,47 @@ +import FlipCard from "@/components/animata/card/flip-card"; +import { useState } from "react"; + +export function LoginPage() { + const [flip, setFlip] = useState(false); + + return ( +
+ {/* Tabs */} +
+ + +
+ + {/* Flip Card */} + +

Sign in with Google

+ +
+ } + backContent={ +
+

Login as Guest

+ +
+ } + /> +
+ ); +} diff --git a/frontend/src/routes/Index.tsx b/frontend/src/routes/Index.tsx new file mode 100644 index 0000000..899c37f --- /dev/null +++ b/frontend/src/routes/Index.tsx @@ -0,0 +1,68 @@ +import { Routes, Route } from 'react-router-dom' +import { ROUTES } from '@/lib/Constants' +import { PrivateRoute } from './PrivateRoute' +import { PublicRoute } from './PubilcRoute' +import { LoginPage} from '@/pages/auth/LogIn' +import HomePage from '@/pages/Home' +import Callback from '@/pages/auth/Callback' + +const publicRoutes = [ + { path: ROUTES.LOGIN, element: }, + { path: ROUTES.CALLBACK, element: } +// { path: ROUTES.CALLBACK, element: } +] + +const privateRoutes = [ + {path: ROUTES.HOME, element: }, +// { path: ROUTES.HOME, element: }, +// { path: ROUTES.PROFILE, element: }, +// { path: ROUTES.CALENDAR, element: }, +// { path: ROUTES.SCAN, element: }, +// { path: ROUTES.ANNOUNCEMENT.BASE, element: }, + +// // Activities +// { path: ROUTES.ACTIVITY.BASE, element: }, +// { path: ROUTES.ACTIVITY.VIEW, element: }, +// { path: ROUTES.ACTIVITY.ME, element: }, + +// // Projects +// { path: ROUTES.PROJECT.BASE, element: }, +// { path: ROUTES.PROJECT.VIEW, element: }, +// { path: ROUTES.PROJECT.CREATE, element: }, +// { path: ROUTES.PROJECT.EDIT, element: }, +// { path: ROUTES.PROJECT.RESTORE, element: }, + +// // Locations +// { path: ROUTES.LOCATION.BASE, element: }, + +// // Users +// { path: ROUTES.USER.BASE, element: }, +// { path: ROUTES.USER.VIEW, element: }, +// { path: ROUTES.USER.CREATE, element: }, +// { path: ROUTES.USER.EDIT, element: }, + +// // Groups +// { path: ROUTES.GROUP.BASE, element: }, +// { path: ROUTES.GROUP.VIEW, element: }, +] + +export function AppRoutes () { + return ( + + {/* Public Routes */} + }> + {publicRoutes.map(({ path, element }) => ( + + ))} + + + {/* Private Routes */} + }> + {privateRoutes.map(({ path, element }) => ( + + ))} + + + // + ) +} diff --git a/frontend/src/routes/PrivateRoute.tsx b/frontend/src/routes/PrivateRoute.tsx new file mode 100644 index 0000000..90b5309 --- /dev/null +++ b/frontend/src/routes/PrivateRoute.tsx @@ -0,0 +1,30 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { ROUTES } from '@/lib/Constants'; +import { useAuth } from '@/hooks/UseAuth'; +import { PageLayout } from '@/components/layouts/PageLayout'; +import { useMemo } from 'react'; +import { Loader2 } from 'lucide-react'; + +export function PrivateRoute() { + const { isAuthenticated, isLoading } = useAuth(); + + const shouldRedirect = useMemo(() => !isAuthenticated && !isLoading, [isAuthenticated, isLoading]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (shouldRedirect) { + return ; + } + + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/src/routes/PubilcRoute.tsx b/frontend/src/routes/PubilcRoute.tsx new file mode 100644 index 0000000..91a5a53 --- /dev/null +++ b/frontend/src/routes/PubilcRoute.tsx @@ -0,0 +1,24 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { ROUTES } from '@/lib/Constants'; +import { useAuth } from '@/hooks/UseAuth'; +import { AuthLayout } from '@/components/layouts/AuthLayout'; +import { useEffect } from 'react'; + +export function PublicRoute() { + const { isAuthenticated } = useAuth(); + + useEffect(() =>{ + console.log(isAuthenticated); + }, []) + + + if (isAuthenticated) { + return ; + } + + return ( + + + + ); +} \ No newline at end of file