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 */}
+
+
+
+
+

+
+
+
+
+
+
+ {/* 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"),
+ },
+ },
});