diff --git a/.gitignore b/.gitignore index 5ef6a52..15158a8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# gemini +/images 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..a0bb829 --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,151 @@ +"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"; +import { BackgroundGradientAnimation } from "@/components/ui/background-gradient-animation"; + +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 */} +
+ +
+
+

+ Data-driven decisions for better education +

+

+ Powered by artificial intelligence to deliver real-time insights and smart recommendations. +

+
+
+ 2026 Faculytics. All rights reserved. +
+
+
+
+ + {/* 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..bdfe4ca 100644 --- a/app/globals.css +++ b/app/globals.css @@ -9,6 +9,10 @@ --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --font-playfair: var(--font-playfair); + --color-brand-blue: var(--brand-blue); + --color-brand-yellow: var(--brand-yellow); + --color-brand-neutral: var(--brand-neutral); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -45,10 +49,57 @@ --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); + + + /* Gradient Animation */ + --animate-first: moveVertical 60s ease infinite; + --animate-second: moveInCircle 40s reverse infinite; + --animate-third: moveInCircle 80s linear infinite; + --animate-fourth: moveHorizontal 80s ease infinite; + --animate-fifth: moveInCircle 40s ease infinite; + + @keyframes moveHorizontal { + 0% { + transform: translateX(-50%) translateY(-10%); + } + 50% { + transform: translateX(50%) translateY(10%); + } + 100% { + transform: translateX(-50%) translateY(-10%); + } + } + @keyframes moveInCircle { + 0% { + transform: rotate(0deg); + } + 50% { + transform: rotate(180deg); + } + 100% { + transform: rotate(360deg); + } + } + @keyframes moveVertical { + 0% { + transform: translateY(-50%); + } + 50% { + transform: translateY(50%); + } + 100% { + transform: translateY(-50%); + } + } } +/* End of Gradient Animation */ + :root { --radius: 0.625rem; + --brand-blue: oklch(0.428 0.25 264.53); + --brand-yellow: oklch(0.898 0.196 91.13); + --brand-neutral: oklch(0.982 0.005 84.15); --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); @@ -123,4 +174,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/bun.lock b/bun.lock index f89702e..fbeb9c1 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@tanstack/react-query": "^5.90.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.34.3", "lucide-react": "^0.564.0", "next": "16.1.6", "openapi-fetch": "^0.17.0", @@ -1186,6 +1187,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "framer-motion": ["framer-motion@12.34.3", "", { "dependencies": { "motion-dom": "^12.34.3", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], @@ -1568,6 +1571,10 @@ "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + "motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], + + "motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="], diff --git a/components.json b/components.json index f87021e..da782e7 100644 --- a/components.json +++ b/components.json @@ -19,5 +19,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": {} + "registries": { + "@aceternity": "https://ui.aceternity.com/registry/{name}.json" + } } diff --git a/components/ui/background-gradient-animation.tsx b/components/ui/background-gradient-animation.tsx new file mode 100644 index 0000000..cea6632 --- /dev/null +++ b/components/ui/background-gradient-animation.tsx @@ -0,0 +1,181 @@ +"use client"; +import { cn } from "@/lib/utils"; +import { useEffect, useRef, useState } from "react"; + +export const BackgroundGradientAnimation = ({ + gradientBackgroundStart = "rgb(108, 0, 162)", + gradientBackgroundEnd = "rgb(0, 17, 82)", + firstColor = "18, 113, 255", + secondColor = "221, 74, 255", + thirdColor = "100, 220, 255", + fourthColor = "200, 50, 50", + fifthColor = "180, 180, 50", + pointerColor = "140, 100, 255", + size = "80%", + blendingValue = "hard-light", + children, + className, + interactive = true, + containerClassName, +}: { + gradientBackgroundStart?: string; + gradientBackgroundEnd?: string; + firstColor?: string; + secondColor?: string; + thirdColor?: string; + fourthColor?: string; + fifthColor?: string; + pointerColor?: string; + size?: string; + blendingValue?: string; + children?: React.ReactNode; + className?: string; + interactive?: boolean; + containerClassName?: string; +}) => { + const interactiveRef = useRef(null); + + const [curX, setCurX] = useState(0); + const [curY, setCurY] = useState(0); + const [tgX, setTgX] = useState(0); + const [tgY, setTgY] = useState(0); + useEffect(() => { + document.body.style.setProperty( + "--gradient-background-start", + gradientBackgroundStart + ); + document.body.style.setProperty( + "--gradient-background-end", + gradientBackgroundEnd + ); + document.body.style.setProperty("--first-color", firstColor); + document.body.style.setProperty("--second-color", secondColor); + document.body.style.setProperty("--third-color", thirdColor); + document.body.style.setProperty("--fourth-color", fourthColor); + document.body.style.setProperty("--fifth-color", fifthColor); + document.body.style.setProperty("--pointer-color", pointerColor); + document.body.style.setProperty("--size", size); + document.body.style.setProperty("--blending-value", blendingValue); + }, []); + + useEffect(() => { + function move() { + if (!interactiveRef.current) { + return; + } + setCurX(curX + (tgX - curX) / 20); + setCurY(curY + (tgY - curY) / 20); + interactiveRef.current.style.transform = `translate(${Math.round( + curX + )}px, ${Math.round(curY)}px)`; + } + + move(); + }, [tgX, tgY]); + + const handleMouseMove = (event: React.MouseEvent) => { + if (interactiveRef.current) { + const rect = interactiveRef.current.getBoundingClientRect(); + setTgX(event.clientX - rect.left); + setTgY(event.clientY - rect.top); + } + }; + + const [isSafari, setIsSafari] = useState(false); + useEffect(() => { + setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)); + }, []); + + return ( +
+ + + + + + + + + +
{children}
+
+
+
+
+
+
+ + {interactive && ( +
+ )} +
+
+ ); +}; 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 ( +