diff --git a/components.json b/components.json new file mode 100644 index 0000000..2b0833f --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/package.json b/package.json index bedd93f..68818d0 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,13 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.12.0", + "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "zustand": "^5.0.9" }, @@ -27,6 +31,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83d5380..3603858 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.562.0 + version: 0.562.0(react@19.2.3) react: specifier: ^19.2.0 version: 19.2.3 @@ -20,6 +29,9 @@ importers: react-router-dom: specifier: ^7.12.0 version: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tailwind-merge: + specifier: ^3.4.0 + version: 3.4.0 tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -54,6 +66,9 @@ importers: globals: specifier: ^16.5.0 version: 16.5.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: ~5.9.3 version: 5.9.3 @@ -736,6 +751,13 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1060,6 +1082,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.562.0: + resolution: {integrity: sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1201,6 +1228,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} @@ -1218,6 +1248,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1931,6 +1964,12 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2236,6 +2275,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.562.0(react@19.2.3): + dependencies: + react: 19.2.3 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2373,6 +2416,8 @@ snapshots: dependencies: has-flag: 4.0.0 + tailwind-merge@3.4.0: {} + tailwindcss@4.1.18: {} tapable@2.3.0: {} @@ -2386,6 +2431,8 @@ snapshots: dependencies: typescript: 5.9.3 + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/src/App.tsx b/src/App.tsx index f4f8af2..6f84961 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,28 +1,67 @@ -import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; -import { Login } from "./pages/login"; -import { StudentDashboard } from "./pages/StudentDashboard"; +import { + createBrowserRouter, + Navigate, + RouterProvider, +} from "react-router-dom"; +import { Login } from "./pages/auth/Login"; +import { Home } from "./pages/student/Home"; +import { Practice } from "./pages/student/Practice"; +import { Rewards } from "./pages/student/Rewards"; +import { Profile } from "./pages/student/Profile"; +import { Progress } from "./pages/student/Progress"; import { ProtectedRoute } from "./components/ProtectedRoute"; +import { StudentLayout } from "./pages/student/StudentLayout"; function App() { - return ( - - - } /> + const router = createBrowserRouter([ + { + path: "/login", + element: , + }, + { + path: "/student", + element: , + children: [ + { + element: , + children: [ + { + path: "home", + element: , + }, + { + path: "practice", + element: , + }, + { + path: "progress", + element: , + }, + { + path: "rewards", + element: , + }, + { + path: "profile", + element: , + }, + // more student subroutes here + ], + }, + // Add more subroutes here as needed + ], + }, + { + path: "/", + element: , + }, + { + path: "*", + element: , + }, + ]); - {/* Protected Routes */} - }> - } /> - {/* Add more subroutes here as needed */} - - - {/* Redirect root to student */} - } /> - - {/* Catch all - redirect to student */} - } /> - - - ); + return ; } export default App; diff --git a/src/index.css b/src/index.css index bd23856..37a196a 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,7 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); /* ================================ Satoshi Font Family @@ -159,3 +162,122 @@ font-style: italic; } } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/pages/StudentDashboard.tsx b/src/pages/StudentDashboard.tsx deleted file mode 100644 index cd677a7..0000000 --- a/src/pages/StudentDashboard.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { useAuthStore } from "../stores/authStore"; - -export const StudentDashboard = () => { - const user = useAuthStore((state) => state.user); - const logout = useAuthStore((state) => state.logout); - const navigate = useNavigate(); - - const handleLogout = () => { - logout(); - navigate("/login"); - }; - - return ( -
- - -
-
-

Dashboard

-
-

Email: {user?.email}

-

Role: {user?.role}

-

Status: {user?.status}

-

- Member since:{" "} - {user?.joined_at - ? new Date(user.joined_at).toLocaleDateString() - : "N/A"} -

-
-
-
-
- ); -}; diff --git a/src/pages/Login.tsx b/src/pages/auth/Login.tsx similarity index 98% rename from src/pages/Login.tsx rename to src/pages/auth/Login.tsx index 04b30f5..89d56b7 100644 --- a/src/pages/Login.tsx +++ b/src/pages/auth/Login.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import type { FormEvent } from "react"; import { useNavigate, useLocation } from "react-router-dom"; -import { useAuthStore } from "../stores/authStore"; +import { useAuthStore } from "../../stores/authStore"; interface LocationState { from?: { diff --git a/src/pages/student/Home.tsx b/src/pages/student/Home.tsx new file mode 100644 index 0000000..639f2bf --- /dev/null +++ b/src/pages/student/Home.tsx @@ -0,0 +1,29 @@ +import { useNavigate } from "react-router-dom"; +import { useAuthStore } from "../../stores/authStore"; + +export const Home = () => { + const user = useAuthStore((state) => state.user); + // const logout = useAuthStore((state) => state.logout); + // const navigate = useNavigate(); + + return ( +
+
+
+

Dashboard

+
+

Email: {user?.email}

+

Role: {user?.role}

+

Status: {user?.status}

+

+ Member since:{" "} + {user?.joined_at + ? new Date(user.joined_at).toLocaleDateString() + : "N/A"} +

+
+
+
+
+ ); +}; diff --git a/src/pages/student/Practice.tsx b/src/pages/student/Practice.tsx new file mode 100644 index 0000000..e7898a0 --- /dev/null +++ b/src/pages/student/Practice.tsx @@ -0,0 +1,26 @@ +import { useAuthStore } from "../../stores/authStore"; + +export const Practice = () => { + const user = useAuthStore((state) => state.user); + + return ( +
+
+
+

Practice

+
+

Email: {user?.email}

+

Role: {user?.role}

+

Status: {user?.status}

+

+ Member since:{" "} + {user?.joined_at + ? new Date(user.joined_at).toLocaleDateString() + : "N/A"} +

+
+
+
+
+ ); +}; diff --git a/src/pages/student/Profile.tsx b/src/pages/student/Profile.tsx new file mode 100644 index 0000000..14a448c --- /dev/null +++ b/src/pages/student/Profile.tsx @@ -0,0 +1,26 @@ +import { useAuthStore } from "../../stores/authStore"; + +export const Profile = () => { + const user = useAuthStore((state) => state.user); + + return ( +
+
+
+

Profile

+
+

Email: {user?.email}

+

Role: {user?.role}

+

Status: {user?.status}

+

+ Member since:{" "} + {user?.joined_at + ? new Date(user.joined_at).toLocaleDateString() + : "N/A"} +

+
+
+
+
+ ); +}; diff --git a/src/pages/student/Progress.tsx b/src/pages/student/Progress.tsx new file mode 100644 index 0000000..85a9cea --- /dev/null +++ b/src/pages/student/Progress.tsx @@ -0,0 +1,26 @@ +import { useAuthStore } from "../../stores/authStore"; + +export const Progress = () => { + const user = useAuthStore((state) => state.user); + + return ( +
+
+
+

Progress

+
+

Email: {user?.email}

+

Role: {user?.role}

+

Status: {user?.status}

+

+ Member since:{" "} + {user?.joined_at + ? new Date(user.joined_at).toLocaleDateString() + : "N/A"} +

+
+
+
+
+ ); +}; diff --git a/src/pages/student/Rewards.tsx b/src/pages/student/Rewards.tsx new file mode 100644 index 0000000..dbcc372 --- /dev/null +++ b/src/pages/student/Rewards.tsx @@ -0,0 +1,26 @@ +import { useAuthStore } from "../../stores/authStore"; + +export const Rewards = () => { + const user = useAuthStore((state) => state.user); + + return ( +
+
+
+

Rewards

+
+

Email: {user?.email}

+

Role: {user?.role}

+

Status: {user?.status}

+

+ Member since:{" "} + {user?.joined_at + ? new Date(user.joined_at).toLocaleDateString() + : "N/A"} +

+
+
+
+
+ ); +}; diff --git a/src/pages/student/StudentLayout.tsx b/src/pages/student/StudentLayout.tsx new file mode 100644 index 0000000..bf82d12 --- /dev/null +++ b/src/pages/student/StudentLayout.tsx @@ -0,0 +1,96 @@ +import { Outlet, NavLink, useNavigate } from "react-router-dom"; +import { Home, BookOpen, TrendingUp, Award, User, Menu } from "lucide-react"; +import { useAuthStore } from "../../stores/authStore"; + +export function StudentLayout() { + // const user = useAuthStore((state) => state.user); + const logout = useAuthStore((state) => state.logout); + const navigate = useNavigate(); + + const handleLogout = () => { + logout(); + navigate("/login"); + }; + + const navItems = [ + { to: "/student/home", icon: Home, label: "Home" }, + { to: "/student/practice", icon: BookOpen, label: "Practice" }, + { to: "/student/progress", icon: TrendingUp, label: "Progress" }, + { to: "/student/rewards", icon: Award, label: "Rewards" }, + { to: "/student/profile", icon: User, label: "Profile" }, + ]; + + return ( +
+ {/* Top Header */} +
+
+
+
+ EdBridge logo +
+ +
+
+
+ + {/* Main Content */} +
+ +
+ + {/* Bottom Tab Navigation */} + +
+ ); +} diff --git a/tsconfig.app.json b/tsconfig.app.json index a9b5a59..87fe896 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -24,5 +24,9 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"] + "include": ["src"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } } diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..fec8c8e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,11 @@ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } - ] + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } } diff --git a/vite.config.ts b/vite.config.ts index 4ff4f8f..41e04e1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,14 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import path from "path"; import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, });