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 {
|
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
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 {
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user