From 16dffe6ffd8dfaeed368057c87a4e4e81e369838 Mon Sep 17 00:00:00 2001 From: shafin-r Date: Sun, 11 Jan 2026 21:02:35 +0600 Subject: [PATCH] feat(practice-sheets): add card-based ui for practice sheets --- package.json | 1 + pnpm-lock.yaml | 19 ++++++++ src/components/ui/badge.tsx | 46 ++++++++++++++++++ src/components/ui/button.tsx | 62 ++++++++++++++++++++++++ src/components/ui/card.tsx | 92 ++++++++++++++++++++++++++++++++++++ src/pages/student/Home.tsx | 80 ++++++++++++++++++++++++++++++- src/utils/api.ts | 36 +++++++++++++- 7 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx diff --git a/package.json b/package.json index 8739a4a..fec9eb7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/vite": "^4.1.18", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b73992..15b1d29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.7)(react@19.2.3) '@radix-ui/react-tabs': specifier: ^1.1.13 version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -493,6 +496,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tabs@1.1.13': resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} peerDependencies: @@ -1864,6 +1876,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-slot@1.2.4(@types/react@19.2.7)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..fd3a406 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +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" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..37a7d4b --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,62 @@ +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" + +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", + 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-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 : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/src/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/src/pages/student/Home.tsx b/src/pages/student/Home.tsx index c2b014a..a53656d 100644 --- a/src/pages/student/Home.tsx +++ b/src/pages/student/Home.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { Tabs, TabsTrigger, @@ -6,12 +7,52 @@ import { } from "../../components/ui/tabs"; import { useAuthStore } from "../../stores/authStore"; import { Search } from "lucide-react"; +import { api } from "../../utils/api"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "../../components/ui/card"; +import { Badge } from "../../components/ui/badge"; +import { Button } from "../../components/ui/button"; export const Home = () => { const user = useAuthStore((state) => state.user); + const [practiceSheets, setPracticeSheets] = useState([]); // const logout = useAuthStore((state) => state.logout); // const navigate = useNavigate(); + useEffect(() => { + const fetchPracticeSheets = async () => { + if (!user) return; + + try { + const authStorage = localStorage.getItem("auth-storage"); + if (!authStorage) { + console.error("authStorage not found in local storage"); + return; + } + const { + state: { token }, + } = JSON.parse(authStorage); + if (!token) { + console.error("Token not found in authStorage"); + return; + } + const sheets = await api.getPracticeSheets(token, 1, 10); + setPracticeSheets(sheets.data); + } catch (error) { + console.error("Error fetching practice sheets:", error); + } + }; + + fetchPracticeSheets(); + }, [user]); + return (
@@ -29,7 +70,7 @@ export const Home = () => {
- + { -

All Status

+
+ {practiceSheets.map((sheet) => ( + + + + {sheet?.title} + + + {sheet?.subject} + + + +

Not Started

+ + {sheet?.modules_count} modules + +
+ +

+ {sheet?.time_limit} minutes +

+
+ + + +
+ ))} +

Not Started

diff --git a/src/utils/api.ts b/src/utils/api.ts index 4660407..d93b9b8 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -27,6 +27,25 @@ export interface ApiError { message?: string; } +export interface PracticeSheet { + title: string; + subject: string; + difficulty: string; + time_limit: number; + description: string; + topics: string[]; + is_locked: boolean; + id: string; + created_at: string; + created_by: { + id: string; + name: string; + email: string; + }; + modules: string[]; + modules_count: number; +} + class ApiClient { private baseURL: string; @@ -92,10 +111,23 @@ class ApiClient { // Example: Get user profile (authenticated endpoint) async getUserProfile(token: string): Promise { - return this.authenticatedRequest("/auth/me", token); + return this.authenticatedRequest("/auth/me/", token); } - // Add more API methods here as needed + async getPracticeSheets( + token: string, + page: number, + limit: number + ): Promise { + const queryParams = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }).toString(); + return this.authenticatedRequest( + `/practice-sheets/?${queryParams}`, + token + ); + } } export const api = new ApiClient(API_URL);