From b5645b8cc0758ea103a490d671561ba8b6c34b03 Mon Sep 17 00:00:00 2001 From: Aya Date: Wed, 25 Feb 2026 14:37:57 +0800 Subject: [PATCH 1/4] feat: partial auth page --- app/auth/layout.tsx | 7 + app/auth/page.tsx | 125 ++++++++++++++++++ app/globals.css | 3 +- app/layout.tsx | 10 +- components/ui/button.tsx | 64 ++++++++++ components/ui/card.tsx | 92 +++++++++++++ components/ui/field.tsx | 248 ++++++++++++++++++++++++++++++++++++ components/ui/input.tsx | 21 +++ components/ui/label.tsx | 24 ++++ components/ui/separator.tsx | 28 ++++ eslint.config.mjs | 1 + hooks/api/use-login.ts | 5 + images/Colors.jpg | Bin 0 -> 360144 bytes images/Login.jpg | Bin 0 -> 307178 bytes 14 files changed, 625 insertions(+), 3 deletions(-) create mode 100644 app/auth/layout.tsx create mode 100644 app/auth/page.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/field.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/separator.tsx create mode 100644 hooks/api/use-login.ts create mode 100644 images/Colors.jpg create mode 100644 images/Login.jpg diff --git a/app/auth/layout.tsx b/app/auth/layout.tsx new file mode 100644 index 0000000..0d9347a --- /dev/null +++ b/app/auth/layout.tsx @@ -0,0 +1,7 @@ +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx new file mode 100644 index 0000000..3bdacd5 --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useLogin } from "@/hooks/api/use-login" +import { LoginRequest, loginRequestSchema } from "@/types/kubb/gen"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; + +export default function AuthPage() { + const { mutate, isPending } = useLogin(); + + const form = useForm({ + resolver: zodResolver(loginRequestSchema), + defaultValues: { + username: "", + password: "" + } + }); + + function onSubmit(values: LoginRequest) { + mutate({ + body: values, + }); + } + + return ( +
+ + {/* Hero Section */} +
+ +
+ + {/* Form Section */} +
+
+

Faculytics 2.0

+
+ +
+
+
+

Welcome

+

+ Sign in using your student portal account +

+
+ +
+ + ( + + + Username + + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> + + ( + +
+ + Password + + + Forgot Password? + +
+ + {fieldState.error && ( + + {fieldState.error.message} + + )} +
+ )} + /> +
+ + +
+
+

Need an account?

+ Contact your administrator +
+
+
+
+
+ ) +} diff --git a/app/globals.css b/app/globals.css index 382ca14..5dd97f5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -9,6 +9,7 @@ --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --font-playfair: var(--font-playfair); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -123,4 +124,4 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/layout.tsx index ea3f6ad..7754769 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Geist, Geist_Mono, Playfair_Display } from "next/font/google"; import "./globals.css"; import AppProvider from "@/components/providers/app-provider"; @@ -13,6 +13,12 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); +const playfair = Playfair_Display({ + variable: "--font-playfair", + subsets: ["latin"], + weight: ["400", "500", "600", "700"], +}); + export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", @@ -26,7 +32,7 @@ export default function RootLayout({ return ( {children} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..b5ea4ab --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +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 hover:bg-primary/90", + destructive: + "bg-destructive text-white 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 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", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-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", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/components/ui/field.tsx b/components/ui/field.tsx new file mode 100644 index 0000000..235d00e --- /dev/null +++ b/components/ui/field.tsx @@ -0,0 +1,248 @@ +"use client" + +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + responsive: [ + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +