Initial Commit
This commit is contained in:
38
components/layout/breadcrumbs.tsx
Normal file
38
components/layout/breadcrumbs.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import Link from "next/link";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export function Breadcrumbs({ items }: BreadcrumbsProps) {
|
||||
return (
|
||||
<nav aria-label="Breadcrumb" className="mb-6">
|
||||
<ol className="flex flex-wrap items-center gap-1.5 text-sm">
|
||||
<li>
|
||||
<Link href="/" className="text-muted transition-colors hover:text-foreground">
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
{items.map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-1.5">
|
||||
<svg className="h-3.5 w-3.5 text-border" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{item.href ? (
|
||||
<Link href={item.href} className="text-muted transition-colors hover:text-foreground">
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="font-medium text-foreground">{item.label}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
32
components/layout/footer.tsx
Normal file
32
components/layout/footer.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border/60 bg-surface/95">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-gradient-to-br from-unit-1 via-unit-2 to-unit-3 shadow-[var(--shadow-sm)]">
|
||||
<span className="text-xs font-extrabold text-white">C</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-extrabold text-[#17367d]">Cabrits Mathematics</p>
|
||||
<p className="text-xs text-[#4c6290]">Learn it. Try it. Master it.</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex items-center gap-5 text-sm font-semibold text-muted">
|
||||
<Link href="/lessons" className="transition-colors hover:text-foreground">
|
||||
Lessons
|
||||
</Link>
|
||||
<Link href="/practice" className="transition-colors hover:text-foreground">
|
||||
Practice
|
||||
</Link>
|
||||
</nav>
|
||||
<p className="text-xs font-medium text-muted">
|
||||
Portsmouth Secondary School
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
42
components/layout/header.tsx
Normal file
42
components/layout/header.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import Link from "next/link";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", label: "Home", className: "bg-[#e0483d] hover:bg-[#cb3a30]" },
|
||||
{ href: "/lessons", label: "Lessons", className: "bg-[#f08b24] hover:bg-[#d97706]" },
|
||||
{ href: "/practice", label: "Practice", className: "bg-[#1faa59] hover:bg-[#168845]" },
|
||||
];
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b-4 border-[#1c4ab7] bg-[#2f65e8] text-white">
|
||||
<div className="mx-auto max-w-6xl playground-frame border-b-0 border-t-0 bg-[#2f65e8] shadow-none">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
|
||||
<Link href="/" className="group flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-[#ffd043] text-[#10308a] shadow-md">
|
||||
<span className="text-lg font-extrabold">C</span>
|
||||
</div>
|
||||
<div className="rounded-md bg-white px-2.5 py-1 leading-none shadow-sm">
|
||||
<p className="text-2xl font-extrabold tracking-tight text-[#17367d]">Cabrits</p>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.14em] text-[#3a5ea8]">Math Playground</p>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 text-xs font-bold sm:text-sm">
|
||||
<span className="rounded-full bg-[#1a49b6] px-3 py-1">Ages 12-16</span>
|
||||
<span className="rounded-full bg-[#1a49b6] px-3 py-1">Form 1 Term 2</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex flex-wrap gap-1 bg-[#1b49b5] px-2 pb-2 pt-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`rounded-sm px-4 py-2 text-sm font-extrabold text-white transition-colors ${item.className}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
89
components/layout/mobile-nav.tsx
Normal file
89
components/layout/mobile-nav.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { curriculum } from "@/lib/curriculum";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
const unitColorMap = {
|
||||
"unit-1": "text-unit-1-dark",
|
||||
"unit-2": "text-unit-2-dark",
|
||||
"unit-3": "text-unit-3-dark",
|
||||
"unit-4": "text-unit-4-dark",
|
||||
};
|
||||
|
||||
const unitDotColor = {
|
||||
"unit-1": "bg-unit-1",
|
||||
"unit-2": "bg-unit-2",
|
||||
"unit-3": "bg-unit-3",
|
||||
"unit-4": "bg-unit-4",
|
||||
};
|
||||
|
||||
export function MobileNav() {
|
||||
const pathname = usePathname();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="lg:hidden">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 rounded-xl border border-border/60 bg-surface px-3.5 py-2 text-sm font-medium shadow-[var(--shadow-sm)] transition-all hover:shadow-[var(--shadow-md)]"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
{isOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
Topics
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute left-0 right-0 top-full z-40 max-h-[70vh] overflow-y-auto border-b border-border/60 bg-surface p-4 shadow-[var(--shadow-lg)]">
|
||||
<Link
|
||||
href="/lessons"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="mb-4 flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium text-muted transition-colors hover:bg-background hover:text-foreground"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
All Topics
|
||||
</Link>
|
||||
<div className="mb-3 h-px bg-border/60" />
|
||||
{curriculum.map((unit) => (
|
||||
<div key={unit.slug} className="mb-4">
|
||||
<p className={cn("mb-1.5 flex items-center gap-2 px-3 text-xs font-bold uppercase tracking-wider", unitColorMap[unit.color])}>
|
||||
<span className={cn("h-2 w-2 rounded-full", unitDotColor[unit.color])} />
|
||||
Unit {unit.number}: {unit.title}
|
||||
</p>
|
||||
<div className="space-y-0.5">
|
||||
{unit.topics.map((topic) => {
|
||||
const href = `/lessons/${unit.slug}/${topic.slug}`;
|
||||
return (
|
||||
<Link
|
||||
key={topic.slug}
|
||||
href={href}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={cn(
|
||||
"block rounded-lg px-3 py-1.5 text-sm transition-colors",
|
||||
pathname === href
|
||||
? "bg-foreground text-background font-medium"
|
||||
: "text-muted hover:bg-background hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{topic.shortTitle}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
components/layout/sidebar.tsx
Normal file
142
components/layout/sidebar.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { curriculum } from "@/lib/curriculum";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
const unitColorMap = {
|
||||
"unit-1": {
|
||||
active: "bg-unit-1-light text-unit-1-dark border-unit-1/20",
|
||||
dot: "bg-unit-1",
|
||||
heading: "text-unit-1-dark",
|
||||
hoverBg: "hover:bg-unit-1-light/50",
|
||||
},
|
||||
"unit-2": {
|
||||
active: "bg-unit-2-light text-unit-2-dark border-unit-2/20",
|
||||
dot: "bg-unit-2",
|
||||
heading: "text-unit-2-dark",
|
||||
hoverBg: "hover:bg-unit-2-light/50",
|
||||
},
|
||||
"unit-3": {
|
||||
active: "bg-unit-3-light text-unit-3-dark border-unit-3/20",
|
||||
dot: "bg-unit-3",
|
||||
heading: "text-unit-3-dark",
|
||||
hoverBg: "hover:bg-unit-3-light/50",
|
||||
},
|
||||
"unit-4": {
|
||||
active: "bg-unit-4-light text-unit-4-dark border-unit-4/20",
|
||||
dot: "bg-unit-4",
|
||||
heading: "text-unit-4-dark",
|
||||
hoverBg: "hover:bg-unit-4-light/50",
|
||||
},
|
||||
};
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const [openUnits, setOpenUnits] = useState<Set<number>>(() => {
|
||||
const open = new Set<number>();
|
||||
for (const unit of curriculum) {
|
||||
if (pathname.includes(unit.slug)) {
|
||||
open.add(unit.number);
|
||||
}
|
||||
}
|
||||
if (open.size === 0) open.add(1);
|
||||
return open;
|
||||
});
|
||||
|
||||
function toggleUnit(num: number) {
|
||||
setOpenUnits((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(num)) next.delete(num);
|
||||
else next.add(num);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="hidden w-64 shrink-0 border-r border-border/60 bg-surface lg:block">
|
||||
<nav className="sticky top-[4.125rem] h-[calc(100vh-4.125rem)] overflow-y-auto p-4">
|
||||
<Link
|
||||
href="/lessons"
|
||||
className={cn(
|
||||
"mb-4 flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200",
|
||||
pathname === "/lessons"
|
||||
? "bg-foreground text-background shadow-[var(--shadow-sm)]"
|
||||
: "text-muted hover:bg-background hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
All Topics
|
||||
</Link>
|
||||
|
||||
<div className="mb-3 h-px bg-border/60" />
|
||||
|
||||
{curriculum.map((unit) => {
|
||||
const colors = unitColorMap[unit.color];
|
||||
const isOpen = openUnits.has(unit.number);
|
||||
|
||||
return (
|
||||
<div key={unit.slug} className="mb-1">
|
||||
<button
|
||||
onClick={() => toggleUnit(unit.number)}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-xl px-3 py-2 text-left text-sm font-semibold transition-all duration-200",
|
||||
colors.heading,
|
||||
colors.hoverBg,
|
||||
)}
|
||||
>
|
||||
<span className="truncate">Unit {unit.number}: {unit.title}</span>
|
||||
<svg
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 transition-transform duration-200",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="ml-2 mt-0.5 space-y-0.5 border-l-2 border-border/40 pl-2">
|
||||
{unit.topics.map((topic) => {
|
||||
const href = `/lessons/${unit.slug}/${topic.slug}`;
|
||||
const isActive = pathname === href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={topic.slug}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm transition-all duration-200",
|
||||
isActive
|
||||
? cn(colors.active, "font-medium shadow-[var(--shadow-sm)]")
|
||||
: "text-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"h-1.5 w-1.5 shrink-0 rounded-full transition-colors",
|
||||
isActive ? colors.dot : "bg-border",
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{topic.shortTitle}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user