diff --git a/package.json b/package.json index cbb8c1c..f398c8a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "embla-carousel-react": "^8.6.0", "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1589690..aca1ec2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.2.3) lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.3) @@ -1181,6 +1184,19 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} @@ -2771,6 +2787,18 @@ snapshots: electron-to-chromium@1.5.267: {} + embla-carousel-react@8.6.0(react@19.2.3): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.2.3 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 diff --git a/src/App.tsx b/src/App.tsx index a735c6b..dcc78ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,12 +5,15 @@ import { } from "react-router-dom"; import { Login } from "./pages/auth/Login"; import { Home } from "./pages/student/Home"; -import { Drills } from "./pages/student/Drills"; +import { Practice } from "./pages/student/Practice"; import { Rewards } from "./pages/student/Rewards"; import { Profile } from "./pages/student/Profile"; import { Lessons } from "./pages/student/Lessons"; import { ProtectedRoute } from "./components/ProtectedRoute"; import { StudentLayout } from "./pages/student/StudentLayout"; +import { Test } from "./pages/student/practice/Test"; +import { Results } from "./pages/student/practice/Results"; +import { Pretest } from "./pages/student/practice/Pretest"; function App() { const router = createBrowserRouter([ @@ -30,8 +33,8 @@ function App() { element: , }, { - path: "drills", - element: , + path: "practice", + element: , }, { path: "lessons", @@ -45,6 +48,20 @@ function App() { path: "profile", element: , }, + { + path: "practice/:sheetId", + element: , + children: [ + { + path: "test", + element: , + }, + { + path: "results", + element: , + }, + ], + }, // more student subroutes here ], }, diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 0000000..71cff4c --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,239 @@ +import * as React from "react" +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react" +import { ArrowLeft, ArrowRight } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +function Carousel({ + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props +}: React.ComponentProps<"div"> & CarouselProps) { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) return + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) return + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) return + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) +} + +function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +} + +function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { + const { orientation } = useCarousel() + + return ( +
+ ) +} + +function CarouselPrevious({ + className, + variant = "outline", + size = "icon", + ...props +}: React.ComponentProps) { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +} + +function CarouselNext({ + className, + variant = "outline", + size = "icon", + ...props +}: React.ComponentProps) { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +} + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/src/pages/student/Home.tsx b/src/pages/student/Home.tsx index 0c17df2..ceba9f0 100644 --- a/src/pages/student/Home.tsx +++ b/src/pages/student/Home.tsx @@ -20,13 +20,14 @@ import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import type { PracticeSheet } from "../../types/sheet"; import { formatStatus } from "../../lib/utils"; +import { useNavigate } from "react-router-dom"; export const Home = () => { const user = useAuthStore((state) => state.user); + const navigate = useNavigate(); // const logout = useAuthStore((state) => state.logout); // const navigate = useNavigate(); - const [practiceSheets, setPracticeSheets] = useState([]); const [notStartedSheets, setNotStartedSheets] = useState([]); const [inProgressSheets, setInProgressSheets] = useState([]); @@ -77,6 +78,10 @@ export const Home = () => { fetchPracticeSheets(); }, [user]); + const handleStartPractice = (sheetId: string) => { + navigate(`/student/practice/${sheetId}`); + }; + return (
@@ -124,7 +129,7 @@ export const Home = () => {
{practiceSheets.length > 0 ? ( practiceSheets.map((sheet) => ( - + {sheet?.title} @@ -151,6 +156,7 @@ export const Home = () => { +
+ ); +}; diff --git a/src/pages/student/practice/Results.tsx b/src/pages/student/practice/Results.tsx new file mode 100644 index 0000000..ec57a20 --- /dev/null +++ b/src/pages/student/practice/Results.tsx @@ -0,0 +1,3 @@ +export const Results = () => { + return
Results
; +}; diff --git a/src/pages/student/practice/Test.tsx b/src/pages/student/practice/Test.tsx new file mode 100644 index 0000000..b6fcbc7 --- /dev/null +++ b/src/pages/student/practice/Test.tsx @@ -0,0 +1,3 @@ +export const Test = () => { + return
Test
; +}; diff --git a/src/types/sheet.ts b/src/types/sheet.ts index 317ab56..3a85211 100644 --- a/src/types/sheet.ts +++ b/src/types/sheet.ts @@ -4,6 +4,49 @@ interface CreatedBy { email: string; } +export interface Subject { + name: string; + section: string; + parent_id: string; + id: string; + slug: string; + parent_name: string; +} + +export interface Question { + text: string; + context: string; + context_image_url: string; + type: string; + section: string; + image_url: string; + index: number; + id: string; + options: any[]; + topics: Topic[]; + correct_answer: string; + explanation: string; +} + +export interface Topic { + id: string; + name: string; +} + +export interface Module { + title: string; + duration: number; + section: string; + difficulty: string; + description: string; + sequence_order: number; + id: string; + practice_sheet_id: string; + subject: Subject; + questions: Question[]; + questions_count: number; +} + export interface PracticeSheet { title: string; difficulty: string; @@ -15,9 +58,9 @@ export interface PracticeSheet { created_at: string; updated_at: string; questions_count: number; - topics: string[]; + topics: Topic[]; created_by: CreatedBy; - modules: string[]; + modules: Module[]; user_status: string; modules_count: number; } diff --git a/src/utils/api.ts b/src/utils/api.ts index 8afb9b5..a219b62 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,3 +1,5 @@ +import type { PracticeSheet } from "../types/sheet"; + const API_URL = "https://ed-dev-api.omukk.dev"; export interface LoginRequest { @@ -36,7 +38,7 @@ class ApiClient { private async request( endpoint: string, - options: RequestInit = {} + options: RequestInit = {}, ): Promise { const url = `${this.baseURL}${endpoint}`; @@ -79,7 +81,7 @@ class ApiClient { async authenticatedRequest( endpoint: string, token: string, - options: RequestInit = {} + options: RequestInit = {}, ): Promise { return this.request(endpoint, { ...options, @@ -98,7 +100,7 @@ class ApiClient { async getPracticeSheets( token: string, page: number, - limit: number + limit: number, ): Promise { const queryParams = new URLSearchParams({ page: page.toString(), @@ -106,12 +108,18 @@ class ApiClient { }).toString(); return this.authenticatedRequest( `/practice-sheets/?${queryParams}`, - token + token, ); } - async getSatDates(token: string): Promise { - return this.authenticatedRequest(`/sat-dates/`, token); + async getPracticeSheetById( + token: string, + sheetId: string, + ): Promise { + return this.authenticatedRequest( + `/practice-sheets/${sheetId}`, + token, + ); } }