feat(lesson): add lesson modal

This commit is contained in:
shafin-r
2026-02-01 18:20:03 +06:00
parent 62238cbf8f
commit 2ac88835f9
5 changed files with 280 additions and 54 deletions

View File

@ -0,0 +1,90 @@
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "../components/ui/dialog";
import { api } from "../utils/api";
import { useAuthStore } from "../stores/authStore";
interface LessonModalProps {
lessonId: string | null;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const LessonModal = ({
lessonId,
open,
onOpenChange,
}: LessonModalProps) => {
const user = useAuthStore((state) => state.user);
const [loading, setLoading] = useState(false);
const [lesson, setLesson] = useState<any>(null);
useEffect(() => {
if (!open || !lessonId || !user) return;
const fetchLesson = async () => {
try {
setLoading(true);
const authStorage = localStorage.getItem("auth-storage");
if (!authStorage) return;
const parsed = JSON.parse(authStorage) as {
state?: { token?: string };
};
const token = parsed.state?.token;
if (!token) return;
const response = await api.fetchLessonById(token, lessonId);
setLesson(response);
} catch (err) {
console.error("Failed to fetch lesson", err);
} finally {
setLoading(false);
}
};
fetchLesson();
}, [open, lessonId, user]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
{loading && (
<div className="py-12 text-center text-muted-foreground">
Loading lesson...
</div>
)}
<DialogHeader>
<DialogTitle>{lesson ? lesson.title : "Lesson details"}</DialogTitle>
</DialogHeader>
{!loading && lesson && (
<div className="space-y-4">
{lesson.video_url && (
<video
src={lesson.video_url}
controls
className="w-full rounded-lg"
/>
)}
<h2 className="font-satoshi-bold text-xl">
{lesson ? lesson.title : "Lesson details"}
</h2>
<p className="text-sm text-muted-foreground">
{lesson.description}
</p>
<p className="text-sm text-muted-foreground">{lesson.content}</p>
</div>
)}
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,11 @@
import { Card, CardContent } from "./ui/card";
export const LessonSkeleton = () => (
<Card className="py-0 pb-5 rounded-4xl overflow-hidden animate-pulse">
<div className="w-full h-48 bg-muted" />
<CardContent className="space-y-2 pt-4">
<div className="h-5 w-2/3 bg-muted rounded" />
<div className="h-4 w-1/2 bg-muted rounded" />
</CardContent>
</Card>
);

View File

@ -1,4 +1,5 @@
// import { useAuthStore } from "../../stores/authStore"; import { useAuthStore } from "../../stores/authStore";
import { useEffect, useState } from "react";
import { import {
Card, Card,
CardHeader, CardHeader,
@ -12,9 +13,52 @@ import {
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from "../../components/ui/tabs"; } from "../../components/ui/tabs";
import { api } from "../../utils/api";
import { type Lesson } from "../../types/lesson";
import { LessonSkeleton } from "../../components/LessonSkeleton";
import { LessonModal } from "../../components/LessonModal";
export const Lessons = () => { export const Lessons = () => {
// const user = useAuthStore((state) => state.user); const user = useAuthStore((state) => state.user);
const [lessons, setLessons] = useState<Lesson[]>([]);
const [lessonLoading, setLessonlLoading] = useState(true);
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleLessonClick = (lessonId: string) => {
setSelectedLessonId(lessonId);
setIsModalOpen(true);
};
useEffect(() => {
const fetchAllLessons = async () => {
if (!user) return;
try {
setLessonlLoading(true);
const authStorage = localStorage.getItem("auth-storage");
if (!authStorage) return;
const parsed = JSON.parse(authStorage) as {
state?: { token?: string };
};
const token = parsed.state?.token;
if (!token) return;
const response = await api.fetchAllLessons(token);
setLessonlLoading(false);
setLessons(response.data);
} catch (error) {
setLessonlLoading(false);
console.error("Error fetching lessons:", error);
}
};
fetchAllLessons();
}, [user]);
return ( return (
<main className="min-h-screen space-y-6 max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8"> <main className="min-h-screen space-y-6 max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8">
@ -43,61 +87,99 @@ export const Lessons = () => {
</TabsList> </TabsList>
<TabsContent value="rw" className="pt-4"> <TabsContent value="rw" className="pt-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="py-0 pb-5 rounded-4xl overflow-hidden"> {lessonLoading && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<LessonSkeleton key={i} />
))}
</div>
)}
{!lessonLoading && lessons.length === 0 && (
<div className="text-center text-muted-foreground py-12">
No lessons available
</div>
)}
{!lessonLoading && lessons.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{lessons.map((lesson) => (
<Card
key={lesson.id}
onClick={() => handleLessonClick(lesson.id)}
className="py-0 pb-5 rounded-4xl overflow-hidden"
>
<CardHeader className="w-full py-0 px-0"> <CardHeader className="w-full py-0 px-0">
<img <img
src="https://placehold.co/600x400" src={lesson.thumbnail_url}
alt="Video Thumbnail" alt={lesson.title}
className="w-full h-auto rounded" className="w-full h-auto"
/> />
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
<CardTitle>Video Title</CardTitle> <CardTitle>{lesson.title}</CardTitle>
<CardDescription>Video Description</CardDescription> <CardDescription>{lesson.topic.name}</CardDescription>
</CardContent> </CardContent>
</Card> </Card>
<Card className="py-0 pb-5 rounded-4xl overflow-hidden"> ))}
<CardHeader className="w-full py-0 px-0"> </div>
<img )}
src="https://placehold.co/600x400" <LessonModal
alt="Video Thumbnail" open={isModalOpen}
className="w-full h-auto rounded" lessonId={selectedLessonId}
onOpenChange={(open) => {
setIsModalOpen(open);
if (!open) setSelectedLessonId(null);
}}
/> />
</CardHeader>
<CardContent className="space-y-2">
<CardTitle>Video Title</CardTitle>
<CardDescription>Video Description</CardDescription>
</CardContent>
</Card>
<Card className="py-0 pb-5 rounded-4xl overflow-hidden">
<CardHeader className="w-full py-0 px-0">
<img
src="https://placehold.co/600x400"
alt="Video Thumbnail"
className="w-full h-auto rounded"
/>
</CardHeader>
<CardContent className="space-y-2">
<CardTitle>Video Title</CardTitle>
<CardDescription>Video Description</CardDescription>
</CardContent>
</Card>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="math" className="pt-4"> <TabsContent value="math" className="pt-4">
<Card className="py-0 pb-8 rounded-4xl overflow-hidden"> {lessonLoading && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<LessonSkeleton key={i} />
))}
</div>
)}
{!lessonLoading && lessons.length === 0 && (
<div className="text-center text-muted-foreground py-12">
No lessons available
</div>
)}
{!lessonLoading && lessons.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{lessons.map((lesson) => (
<Card
key={lesson.id}
onClick={() => handleLessonClick(lesson.id)}
className="py-0 pb-5 rounded-4xl overflow-hidden"
>
<CardHeader className="w-full py-0 px-0"> <CardHeader className="w-full py-0 px-0">
<img <img
src="https://placehold.co/600x400" src={lesson.thumbnail_url}
alt="Video Thumbnail" alt={lesson.title}
className="w-full h-auto rounded" className="w-full h-auto"
/> />
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
<CardTitle>Video Title</CardTitle> <CardTitle>{lesson.title}</CardTitle>
<CardDescription>Video Description</CardDescription> <CardDescription>{lesson.topic.name}</CardDescription>
</CardContent> </CardContent>
</Card> </Card>
))}
</div>
)}
<LessonModal
open={isModalOpen}
lessonId={selectedLessonId}
onOpenChange={(open) => {
setIsModalOpen(open);
if (!open) setSelectedLessonId(null);
}}
/>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</section> </section>

35
src/types/lesson.ts Normal file
View File

@ -0,0 +1,35 @@
import type { Topic } from "./sheet";
export type Lesson = {
id: string;
title: string;
thumbnail_url: string;
topic: Topic;
};
export interface LessonsResponse {
data: Lesson[];
pagination: {
page: number;
limit: number;
total: number;
total_pages: number;
};
}
export interface LessonDetails {
title: string;
topic_id: string;
video_url: string;
content: string;
description: string;
thumbnail_url: string;
resources: any[];
id: string;
created_by: {
id: string;
name: string;
email: string;
};
topic: Topic[];
}

View File

@ -1,3 +1,4 @@
import type { Lesson, LessonsResponse } from "../types/lesson";
import type { import type {
SessionAnswerResponse, SessionAnswerResponse,
SessionQuestionsResponse, SessionQuestionsResponse,
@ -183,5 +184,12 @@ class ApiClient {
token, token,
); );
} }
async fetchAllLessons(token: string): Promise<LessonsResponse> {
return this.authenticatedRequest<LessonsResponse>(`/lessons/`, token);
}
async fetchLessonById(token: string, lessonId: string): Promise<Lesson> {
return this.authenticatedRequest<Lesson>(`/lessons/${lessonId}`, token);
}
} }
export const api = new ApiClient(API_URL); export const api = new ApiClient(API_URL);