Files
starr-philproperties/src/modules/client/pages/LessonsList.jsx
T

202 lines
9.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;