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 {
Card,
CardHeader,
@ -12,9 +13,52 @@ import {
TabsList,
TabsTrigger,
} 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 = () => {
// 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 (
<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>
<TabsContent value="rw" className="pt-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">
<img
src="https://placehold.co/600x400"
alt="Video Thumbnail"
className="w-full h-auto rounded"
src={lesson.thumbnail_url}
alt={lesson.title}
className="w-full h-auto"
/>
</CardHeader>
<CardContent className="space-y-2">
<CardTitle>Video Title</CardTitle>
<CardDescription>Video Description</CardDescription>
<CardTitle>{lesson.title}</CardTitle>
<CardDescription>{lesson.topic.name}</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"
))}
</div>
)}
<LessonModal
open={isModalOpen}
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>
</TabsContent>
<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">
<img
src="https://placehold.co/600x400"
alt="Video Thumbnail"
className="w-full h-auto rounded"
src={lesson.thumbnail_url}
alt={lesson.title}
className="w-full h-auto"
/>
</CardHeader>
<CardContent className="space-y-2">
<CardTitle>Video Title</CardTitle>
<CardDescription>Video Description</CardDescription>
<CardTitle>{lesson.title}</CardTitle>
<CardDescription>{lesson.topic.name}</CardDescription>
</CardContent>
</Card>
))}
</div>
)}
<LessonModal
open={isModalOpen}
lessonId={selectedLessonId}
onOpenChange={(open) => {
setIsModalOpen(open);
if (!open) setSelectedLessonId(null);
}}
/>
</TabsContent>
</Tabs>
</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 {
SessionAnswerResponse,
SessionQuestionsResponse,
@ -183,5 +184,12 @@ class ApiClient {
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);