mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
202 lines
9.5 KiB
React
202 lines
9.5 KiB
React
import { useEffect, useMemo, useState } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { House, ChevronLeft, ChevronRight, Layers } from "lucide-react";
|
||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||
import { Input } from "@/components/ui/input";
|
||
import {
|
||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Button } from "@/components/ui/button";
|
||
import { useLibrary } from "@/contexts/ClientLibraryContext";
|
||
import { useClientTiers } from "@/contexts/ClientTiersProvider";
|
||
import LessonUpsellModal from "../components/LessonUpsellModal";
|
||
import { LessonCard, LessonCardSkeleton } from "../components/LessonCard";
|
||
import { PageMeta } from "@/contexts/MetadataContext";
|
||
|
||
const ITEMS_PER_PAGE = 10;
|
||
|
||
// ─── Pagination ───────────────────────────────────────────────────────────────
|
||
|
||
const Pagination = ({ currentPage, totalPages, totalItems, itemsPerPage, onPageChange }) => {
|
||
const start = (currentPage - 1) * itemsPerPage + 1;
|
||
const end = Math.min(currentPage * itemsPerPage, totalItems);
|
||
|
||
const getPages = () => {
|
||
const pages = [];
|
||
if (totalPages <= 5) {
|
||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||
} else {
|
||
pages.push(1);
|
||
if (currentPage > 3) pages.push("...");
|
||
for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) pages.push(i);
|
||
if (currentPage < totalPages - 2) pages.push("...");
|
||
pages.push(totalPages);
|
||
}
|
||
return pages;
|
||
};
|
||
|
||
return (
|
||
<div className="flex items-center justify-between w-full pt-4 border-t">
|
||
<div className="text-sm text-muted-foreground">
|
||
Showing <span className="font-medium text-foreground">{start}–{end}</span> of{" "}
|
||
<span className="font-medium text-foreground">{totalItems}</span> lessons
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<Button onClick={() => onPageChange(currentPage - 1)} disabled={currentPage === 1} variant="ghost" size="sm">
|
||
<ChevronLeft />
|
||
</Button>
|
||
{getPages().map((page, i) =>
|
||
page === "..." ? (
|
||
<span key={`ellipsis-${i}`} className="w-7 h-7 flex items-center justify-center text-xs text-muted-foreground">···</span>
|
||
) : (
|
||
<Button key={page} size="sm" variant={currentPage === page ? "default" : "outline"} onClick={() => onPageChange(page)}>
|
||
{page}
|
||
</Button>
|
||
)
|
||
)}
|
||
<Button onClick={() => onPageChange(currentPage + 1)} disabled={currentPage === totalPages} variant="ghost" size="sm">
|
||
<ChevronRight />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||
|
||
const LessonsList = () => {
|
||
const navigate = useNavigate();
|
||
const { lessons, lessonsLoading, getLessons } = useLibrary();
|
||
const { tierMap, getTierCategories } = useClientTiers();
|
||
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [search, setSearch] = useState("");
|
||
const [lockFilter, setLockFilter] = useState("All");
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [selectedLesson, setSelectedLesson] = useState(null);
|
||
|
||
useEffect(() => {
|
||
getLessons();
|
||
getTierCategories();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
const filtered = useMemo(() =>
|
||
lessons
|
||
.filter((l) => {
|
||
const matchSearch = l.title.toLowerCase().includes(search.toLowerCase()) ||
|
||
(l.description ?? "").toLowerCase().includes(search.toLowerCase());
|
||
const matchLock = lockFilter === "All"
|
||
|| (lockFilter === "Unlocked" && !l.is_locked)
|
||
|| (lockFilter === "Locked" && l.is_locked);
|
||
return matchSearch && matchLock;
|
||
})
|
||
.sort((a, b) => a.title.localeCompare(b.title)),
|
||
[lessons, search, lockFilter]
|
||
);
|
||
|
||
const totalPages = Math.max(1, Math.ceil(filtered.length / ITEMS_PER_PAGE));
|
||
const paginated = filtered.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
|
||
|
||
const handleViewDetails = (lesson) => {
|
||
if (lesson.is_locked) {
|
||
setSelectedLesson(lesson);
|
||
setModalOpen(true);
|
||
} else {
|
||
navigate(`/lessons/${lesson.uuid}`);
|
||
}
|
||
};
|
||
|
||
const items = [
|
||
{ label: "Home", icon: <House className="size-4" />, to: `/dashboard` },
|
||
{ label: "Lessons" },
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<PageMeta title="Lessons - STARR" description="Browse standalone lessons you can start right away." />
|
||
<div className="py-24 bg-accent/70 min-h-screen">
|
||
<div className="flex flex-col gap-4 justify-between lg:container lg:mx-auto pt-2">
|
||
<AppBreadcrumb items={items} />
|
||
|
||
{/* Search & Filters */}
|
||
<div className="fixed top-[67px] left-0 right-0 z-10 bg-card border-b lg:static lg:bg-transparent lg:border-none py-3 xs:px-4 md:px-6 lg:px-0">
|
||
<div className="flex items-center xs:flex-col lg:flex-row gap-4">
|
||
<Input
|
||
placeholder="Search lessons..."
|
||
className="w-full bg-card lg:max-w-64 text-sm"
|
||
value={search}
|
||
onChange={(e) => { setSearch(e.target.value); setCurrentPage(1); }}
|
||
/>
|
||
<Select value={lockFilter} onValueChange={(v) => { setLockFilter(v); setCurrentPage(1); }}>
|
||
<SelectTrigger className="w-full lg:w-48 bg-card">
|
||
<SelectValue placeholder="Access" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="All">All Lessons</SelectItem>
|
||
<SelectItem value="Unlocked">Unlocked</SelectItem>
|
||
<SelectItem value="Locked">Locked</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<Select value="lessons" onValueChange={(v) => {
|
||
if (v === "courses") navigate("/course");
|
||
if (v === "units") navigate("/units");
|
||
}}>
|
||
<SelectTrigger className="w-full lg:w-48 bg-card">
|
||
<SelectValue placeholder="Browse" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="courses">Courses</SelectItem>
|
||
<SelectItem value="units">Units</SelectItem>
|
||
<SelectItem value="lessons">Lessons</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Lesson Grid */}
|
||
{lessonsLoading ? (
|
||
<div className="xs:pt-12 lg:pt-0 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||
{Array.from({ length: 8 }).map((_, i) => <LessonCardSkeleton key={i} />)}
|
||
</div>
|
||
) : paginated.length === 0 ? (
|
||
<div className="flex flex-col items-center justify-center py-20">
|
||
<Layers className="size-40 text-primary" />
|
||
<p className="text-md">No lessons found</p>
|
||
</div>
|
||
) : (
|
||
<div className="xs:pt-12 lg:pt-0 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-4 xs:px-4 lg:px-0">
|
||
{paginated.map((lesson) => (
|
||
<LessonCard
|
||
key={lesson.lesson_id}
|
||
lesson={lesson}
|
||
onViewDetails={handleViewDetails}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{!lessonsLoading && filtered.length > ITEMS_PER_PAGE && (
|
||
<Pagination
|
||
currentPage={currentPage}
|
||
totalPages={totalPages}
|
||
totalItems={filtered.length}
|
||
itemsPerPage={ITEMS_PER_PAGE}
|
||
onPageChange={setCurrentPage}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<LessonUpsellModal
|
||
open={modalOpen}
|
||
onOpenChange={setModalOpen}
|
||
lesson={selectedLesson}
|
||
tierMap={tierMap}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default LessonsList;
|