feat(lesson): add lesson modal
This commit is contained in:
90
src/components/LessonModal.tsx
Normal file
90
src/components/LessonModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
11
src/components/LessonSkeleton.tsx
Normal file
11
src/components/LessonSkeleton.tsx
Normal 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>
|
||||
);
|
||||
@ -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">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
{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={lesson.thumbnail_url}
|
||||
alt={lesson.title}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2">
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="math" className="pt-4">
|
||||
<Card className="py-0 pb-8 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>
|
||||
{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={lesson.thumbnail_url}
|
||||
alt={lesson.title}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2">
|
||||
<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
35
src/types/lesson.ts
Normal 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[];
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user