feat(ui): add new ui
This commit is contained in:
@ -1,188 +1,294 @@
|
||||
import { useAuthStore } from "../../stores/authStore";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "../../components/ui/card";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
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";
|
||||
import { BookOpen, Calculator } from "lucide-react";
|
||||
|
||||
const DOTS = [
|
||||
{ size: 10, color: "#f97316", top: "6%", left: "4%", delay: "0s" },
|
||||
{ size: 7, color: "#a855f7", top: "25%", left: "2%", delay: "1.2s" },
|
||||
{ size: 9, color: "#22c55e", top: "60%", left: "3%", delay: "0.6s" },
|
||||
{ size: 12, color: "#3b82f6", top: "10%", right: "4%", delay: "1.8s" },
|
||||
{ size: 7, color: "#f43f5e", top: "45%", right: "2%", delay: "0.9s" },
|
||||
{ size: 9, color: "#eab308", top: "75%", right: "5%", delay: "0.4s" },
|
||||
];
|
||||
|
||||
const STYLES = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||
|
||||
.ls-screen {
|
||||
min-height: 100vh;
|
||||
background: #fffbf4;
|
||||
font-family: 'Nunito', sans-serif;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ls-blob { position: fixed; pointer-events: none; z-index: 0; filter: blur(48px); opacity: 0.35; }
|
||||
.ls-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:lsWobble1 14s ease-in-out infinite; }
|
||||
.ls-blob-2 { width:190px;height:190px;background:#a5f3c0;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:lsWobble2 16s ease-in-out infinite; }
|
||||
.ls-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:lsWobble1 18s ease-in-out infinite reverse; }
|
||||
.ls-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:lsWobble2 12s ease-in-out infinite; }
|
||||
|
||||
@keyframes lsWobble1 {
|
||||
0%,100%{border-radius:60% 40% 70% 30%/50% 60% 40% 50%;transform:translate(0,0) rotate(0deg);}
|
||||
50%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(12px,16px) rotate(8deg);}
|
||||
}
|
||||
@keyframes lsWobble2 {
|
||||
0%,100%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(0,0) rotate(0deg);}
|
||||
50%{border-radius:60% 40% 70% 30%/40% 60% 40% 60%;transform:translate(-10px,12px) rotate(-6deg);}
|
||||
}
|
||||
|
||||
.ls-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:lsFloat 7s ease-in-out infinite; }
|
||||
@keyframes lsFloat {
|
||||
0%,100%{transform:translateY(0) rotate(0deg);}
|
||||
50%{transform:translateY(-12px) rotate(180deg);}
|
||||
}
|
||||
|
||||
.ls-inner {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 680px; margin: 0 auto;
|
||||
padding: 2rem 1.25rem 4rem;
|
||||
display: flex; flex-direction: column; gap: 1.25rem;
|
||||
}
|
||||
|
||||
@keyframes lsPopIn {
|
||||
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
||||
to { opacity:1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
.ls-anim { animation: lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
|
||||
.ls-anim-1 { animation-delay: 0.05s; }
|
||||
.ls-anim-2 { animation-delay: 0.1s; }
|
||||
.ls-anim-3 { animation-delay: 0.15s; }
|
||||
|
||||
/* Header */
|
||||
.ls-header { animation: lsPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
|
||||
.ls-title { font-size: 1.8rem; font-weight: 900; color: #1e1b4b; letter-spacing: -0.02em; }
|
||||
.ls-sub {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.85rem; font-weight: 600; color: #9ca3af; margin-top: 0.25rem;
|
||||
line-height: 1.5; max-width: 420px;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.ls-tabs-list {
|
||||
display: flex; gap: 0.5rem; margin-bottom: 1.25rem;
|
||||
}
|
||||
.ls-tab-btn {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.55rem 1.1rem; border-radius: 100px; cursor: pointer; border: none;
|
||||
font-family: 'Nunito', sans-serif; font-size: 0.82rem; font-weight: 800;
|
||||
transition: all 0.2s ease;
|
||||
background: white; border: 2.5px solid #f3f4f6; color: #9ca3af;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
.ls-tab-btn.active {
|
||||
background: #1e1b4b; border-color: #1e1b4b; color: white;
|
||||
box-shadow: 0 4px 0 #1e1b4b66;
|
||||
}
|
||||
.ls-tab-btn:not(.active):hover { border-color: #c4b5fd; color: #7c3aed; }
|
||||
|
||||
/* Lesson grid */
|
||||
.ls-grid {
|
||||
display: grid; gap: 0.85rem; grid-template-columns: 1fr;
|
||||
}
|
||||
@media(min-width: 480px) { .ls-grid { grid-template-columns: 1fr 1fr; } }
|
||||
|
||||
/* Lesson card */
|
||||
.ls-card {
|
||||
background: white; border: 2.5px solid #f3f4f6; border-radius: 22px;
|
||||
overflow: hidden; cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.04);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.ls-card:hover { transform: translateY(-3px); box-shadow: 0 10px 24px rgba(0,0,0,0.08); }
|
||||
.ls-card:active { transform: translateY(1px); box-shadow: 0 3px 8px rgba(0,0,0,0.05); }
|
||||
|
||||
.ls-card-thumb {
|
||||
width: 100%; aspect-ratio: 16/9; object-fit: cover;
|
||||
display: block; background: #f3f4f6;
|
||||
}
|
||||
.ls-card-body { padding: 0.9rem 1rem 1rem; display: flex; flex-direction: column; gap: 0.25rem; flex: 1; }
|
||||
.ls-card-title { font-size: 0.92rem; font-weight: 900; color: #1e1b4b; line-height: 1.3; }
|
||||
.ls-card-topic {
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
display: flex; align-items: center; gap: 0.35rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.ls-card-topic.rw { color: #a855f7; }
|
||||
.ls-card-topic.math { color: #0891b2; }
|
||||
.ls-topic-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||||
.ls-topic-dot.rw { background: #a855f7; }
|
||||
.ls-topic-dot.math { background: #0891b2; }
|
||||
|
||||
/* Empty / error */
|
||||
.ls-empty {
|
||||
grid-column: 1 / -1; text-align: center; padding: 3rem 1rem;
|
||||
background: white; border: 2.5px dashed #e5e7eb; border-radius: 22px;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
|
||||
}
|
||||
.ls-empty-emoji { font-size: 2.5rem; }
|
||||
.ls-empty-text { font-size: 0.9rem; font-weight: 700; color: #9ca3af; }
|
||||
|
||||
/* Skeleton shimmer override */
|
||||
.ls-skeleton-grid { display: grid; gap: 0.85rem; grid-template-columns: 1fr; }
|
||||
@media(min-width: 480px) { .ls-skeleton-grid { grid-template-columns: 1fr 1fr; } }
|
||||
`;
|
||||
|
||||
export const Lessons = () => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const [lessons, setLessons] = useState<Lesson[]>([]);
|
||||
const [lessonLoading, setLessonlLoading] = useState(true);
|
||||
|
||||
const [lessonLoading, setLessonLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<"rw" | "math">("rw");
|
||||
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleLessonClick = (lessonId: string) => {
|
||||
setSelectedLessonId(lessonId);
|
||||
const handleLessonClick = (id: string) => {
|
||||
setSelectedLessonId(id);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAllLessons = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
setLessonlLoading(true);
|
||||
setLessonLoading(true);
|
||||
const authStorage = localStorage.getItem("auth-storage");
|
||||
if (!authStorage) return;
|
||||
|
||||
const parsed = JSON.parse(authStorage) as {
|
||||
state?: { token?: string };
|
||||
};
|
||||
|
||||
const token = parsed.state?.token;
|
||||
const {
|
||||
state: { token },
|
||||
} = JSON.parse(authStorage) as { state?: { token?: string } };
|
||||
if (!token) return;
|
||||
|
||||
const response = await api.fetchAllLessons(token);
|
||||
|
||||
setLessonlLoading(false);
|
||||
setLessons(response.data);
|
||||
} catch (error) {
|
||||
setLessonlLoading(false);
|
||||
console.error("Error fetching lessons:", error);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLessonLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<header className="space-y-2">
|
||||
<h1 className="font-satoshi-black text-2xl">Lessons</h1>
|
||||
<p className="font-satoshi-medium text-sm text-gray-500">
|
||||
Browse step-by-step lessons from expert Edbridge tutors and pick up
|
||||
tips to tackle similar questions with confidence.
|
||||
</p>
|
||||
</header>
|
||||
<section>
|
||||
<Tabs defaultValue="rw">
|
||||
<TabsList className="bg-transparent space-x-4">
|
||||
<TabsTrigger
|
||||
value="rw"
|
||||
className="font-satoshi-bold px-2 tracking-wide text-md rounded-none border-b-2 data-[state=active]:font-satoshi-medium data-[state=active]:border-b-purple-800 data-[state=active]:bg-transparent data-[state=active]:text-purple-800"
|
||||
>
|
||||
Reading & Writing
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="math"
|
||||
className="font-satoshi-bold px-2 tracking-wide text-md rounded-none border-b-2 data-[state=active]:border-b-purple-800 data-[state=active]:text-purple-800"
|
||||
>
|
||||
Math
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="rw" className="pt-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{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 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">
|
||||
{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);
|
||||
}}
|
||||
const renderGrid = (variant: "rw" | "math") => {
|
||||
if (lessonLoading) {
|
||||
return (
|
||||
<div className="ls-skeleton-grid">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<LessonSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!lessons.length) {
|
||||
return (
|
||||
<div className="ls-grid">
|
||||
<div className="ls-empty">
|
||||
<span className="ls-empty-emoji">📭</span>
|
||||
<p className="ls-empty-text">No lessons available yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="ls-grid">
|
||||
{lessons.map((lesson) => (
|
||||
<div
|
||||
key={lesson.id}
|
||||
className="ls-card"
|
||||
onClick={() => handleLessonClick(lesson.id)}
|
||||
>
|
||||
<img
|
||||
src={lesson.thumbnail_url}
|
||||
alt={lesson.title}
|
||||
className="ls-card-thumb"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
</main>
|
||||
<div className="ls-card-body">
|
||||
<p className="ls-card-title">{lesson.title}</p>
|
||||
<p className={`ls-card-topic ${variant}`}>
|
||||
<span className={`ls-topic-dot ${variant}`} />
|
||||
{lesson.topic.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ls-screen">
|
||||
<style>{STYLES}</style>
|
||||
|
||||
{/* Blobs */}
|
||||
<div className="ls-blob ls-blob-1" />
|
||||
<div className="ls-blob ls-blob-2" />
|
||||
<div className="ls-blob ls-blob-3" />
|
||||
<div className="ls-blob ls-blob-4" />
|
||||
|
||||
{/* Dots */}
|
||||
{DOTS.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="ls-dot"
|
||||
style={
|
||||
{
|
||||
width: d.size,
|
||||
height: d.size,
|
||||
background: d.color,
|
||||
top: d.top,
|
||||
left: d.left,
|
||||
right: d.right,
|
||||
animationDelay: d.delay,
|
||||
animationDuration: `${5 + i * 0.5}s`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="ls-inner">
|
||||
{/* Header */}
|
||||
<header className="ls-header">
|
||||
<h1 className="ls-title">📚 Lessons</h1>
|
||||
<p className="ls-sub">
|
||||
Step-by-step lessons from expert Edbridge tutors — pick up tips to
|
||||
tackle similar questions with confidence.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Tabs + content */}
|
||||
<section className="ls-anim ls-anim-1">
|
||||
<div className="ls-tabs-list">
|
||||
<button
|
||||
className={`ls-tab-btn${activeTab === "rw" ? " active" : ""}`}
|
||||
onClick={() => setActiveTab("rw")}
|
||||
>
|
||||
<BookOpen size={15} /> Reading & Writing
|
||||
</button>
|
||||
<button
|
||||
className={`ls-tab-btn${activeTab === "math" ? " active" : ""}`}
|
||||
onClick={() => setActiveTab("math")}
|
||||
>
|
||||
<Calculator size={15} /> Math
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{renderGrid(activeTab)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<LessonModal
|
||||
open={isModalOpen}
|
||||
lessonId={selectedLessonId}
|
||||
onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) setSelectedLessonId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user