diff --git a/package-lock.json b/package-lock.json index 0103dc5e..b7329a24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "firebase": "^10.13.0", "firebase-admin": "^12.3.1", "highlight.js": "^11.10.0", - "katex": "^0.16.11", + "katex": "^0.16.47", "lucide-react": "^0.525.0", "marked": "^18.0.5", "next": "^14.2.5", @@ -7407,9 +7407,9 @@ } }, "node_modules/katex": { - "version": "0.16.22", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", - "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" diff --git a/package.json b/package.json index 9ba74ca1..ba5cb896 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "firebase": "^10.13.0", "firebase-admin": "^12.3.1", "highlight.js": "^11.10.0", - "katex": "^0.16.11", + "katex": "^0.16.47", "lucide-react": "^0.525.0", "marked": "^18.0.5", "next": "^14.2.5", diff --git a/public/team/lermonade.webp b/public/team/lermonade.webp new file mode 100644 index 00000000..ee2398a5 Binary files /dev/null and b/public/team/lermonade.webp differ diff --git a/public/team/stealth.webp b/public/team/stealth.webp new file mode 100644 index 00000000..ebc5232d Binary files /dev/null and b/public/team/stealth.webp differ diff --git a/public/team/venkata.jpg b/public/team/venkata.jpg new file mode 100644 index 00000000..33410c66 Binary files /dev/null and b/public/team/venkata.jpg differ diff --git a/src/app/admin/subject/[slug]/[unit]/frq/[id]/page.tsx b/src/app/admin/subject/[slug]/[unit]/frq/[id]/page.tsx new file mode 100644 index 00000000..33937718 --- /dev/null +++ b/src/app/admin/subject/[slug]/[unit]/frq/[id]/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import FRQEditorRenderer from "@/components/frq/editorRenderer"; +import { db } from "@/lib/firebase"; +import { doc, getDoc } from "firebase/firestore"; +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; + +const Page = () => { + const pathname = usePathname() ?? ""; + + const pathParts = pathname.split("/").slice(-4); + const subject = pathParts[0] ?? ""; + const unitId = pathParts[1] ?? ""; + const frqId = pathParts[3] ?? ""; + + const [frqFound, setFrqFound] = useState(null); + + useEffect(() => { + if (!subject || !unitId || !frqId) { + setFrqFound(false); + return; + } + + (async () => { + const docRef = doc( + db, + "subjects", + subject, + "units", + unitId, + "frqs", + frqId, + ); + + const docSnap = await getDoc(docRef); + setFrqFound(docSnap.exists()); + })().catch((error) => { + console.error("Error fetching FRQ:", error); + setFrqFound(false); + }); + }, [subject, unitId, frqId]); + + if (frqFound === null) { + return
Loading...
; + } + + return ; +}; + +export default Page; diff --git a/src/app/admin/subject/[slug]/_components/chapterContent.tsx b/src/app/admin/subject/[slug]/_components/chapterContent.tsx index 5e3d5aa7..5697e2d5 100644 --- a/src/app/admin/subject/[slug]/_components/chapterContent.tsx +++ b/src/app/admin/subject/[slug]/_components/chapterContent.tsx @@ -19,6 +19,7 @@ interface ChapterComponentProps { setChapterVisibility: (chapterId: string, isPublic: boolean) => void; moveChapterUp: (chapterId: string) => void; moveChapterDown: (chapterId: string) => void; + hasUnit0?: boolean; } /** @@ -36,6 +37,7 @@ function ChapterComponent({ setChapterVisibility, moveChapterUp, moveChapterDown, + hasUnit0, }: ChapterComponentProps) { const [editing, setEditing] = useState(false); const [localTitle, setLocalTitle] = useState(chapter.title); @@ -81,7 +83,7 @@ function ChapterComponent({ Edit Chapter {index + 1} diff --git a/src/app/admin/subject/[slug]/_components/unit.tsx b/src/app/admin/subject/[slug]/_components/unit.tsx index 4397204b..7b5555c4 100644 --- a/src/app/admin/subject/[slug]/_components/unit.tsx +++ b/src/app/admin/subject/[slug]/_components/unit.tsx @@ -35,6 +35,7 @@ interface UnitComponentProps { onMoveDown: (index: number) => void; subjectSlug: string; subjectTitle: string; + hasUnit0?: boolean; } /** @@ -50,6 +51,7 @@ function UnitComponent({ onMoveDown, subjectSlug, subjectTitle, + hasUnit0, }: UnitComponentProps) { const [expanded, setExpanded] = useState(false); @@ -369,6 +371,7 @@ function UnitComponent({ setChapterVisibility={setChapterVisibility} moveChapterUp={moveChapterUp} moveChapterDown={moveChapterDown} + hasUnit0={hasUnit0} /> ))} diff --git a/src/app/admin/subject/[slug]/page.tsx b/src/app/admin/subject/[slug]/page.tsx index 374ac4c4..a51d4a71 100644 --- a/src/app/admin/subject/[slug]/page.tsx +++ b/src/app/admin/subject/[slug]/page.tsx @@ -69,6 +69,7 @@ export default function Page({ params }: { params: { slug: string } }) { const { user, error, setError, setLoading } = useUser(); const [subjectTitle, setSubjectTitle] = useState(""); const [units, setUnits] = useState([]); + const [hasUnit0, setHasUnit0] = useState(false); const [resetting, setResetting] = useState(false); const [subjectLoading, setSubjectLoading] = useState(true); @@ -88,6 +89,7 @@ export default function Page({ params }: { params: { slug: string } }) { const fetched = docSnap.data() as Subject; setSubjectTitle(fetched.title); setUnits(fetched.units || []); + setHasUnit0(fetched.hasUnit0 ?? false); } else { // Subject not found -> create new with default template // Also try to fill in subject title from `apClasses` if found @@ -259,6 +261,7 @@ export default function Page({ params }: { params: { slug: string } }) { const subjectToSave: Subject = { title: subjectTitle, units: units, + hasUnit0: hasUnit0, }; try { @@ -463,21 +466,37 @@ export default function Page({ params }: { params: { slug: string } }) { {subjectTitle || "Untitled Subject"} + {/* hasUnit0 Toggle */} + + {/* Render each Unit */}
{units.map((unit, index) => ( - - ))} + + ))}
{/* Add new Unit */} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 170b52c4..f6392b1e 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -32,7 +32,8 @@ export default async function sitemap(): Promise { }); subject.units.forEach((unit, unitIndex) => { - const unitSegment = `unit-${unitIndex + 1}-${unit.id}`; + const uNum = subject.hasUnit0 ? unitIndex : unitIndex + 1; + const unitSegment = `unit-${uNum}-${unit.id}`; for (const chapter of unit.chapters ?? []) { // Only list publicly readable chapters — gated content can't be // crawled or cited anyway, and listing it invites thin-page penalties. diff --git a/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterClient.tsx b/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterClient.tsx index 5f3801ce..41cbb21f 100644 --- a/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterClient.tsx +++ b/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterClient.tsx @@ -26,13 +26,15 @@ const ChapterClient = ({ useEffect(() => { if (subject && content) { - const unitIndex = Number(params.unit.split("-")[1]) - 1; + const unitNum = Number(params.unit.split("-")[1]); + const unitIndex = subject.hasUnit0 ? unitNum : unitNum - 1; const chapterIndex = subject.units[unitIndex]!.chapters.findIndex( (ch) => ch.id === params.id, ); const chapter = subject.units[unitIndex]!.chapters[chapterIndex]; + const displayUnit = subject.hasUnit0 ? unitIndex : unitIndex + 1; - document.title = `FiveHive - ${subject.title} ${unitIndex + 1}.${chapterIndex + 1} - ${chapter?.title}`; + document.title = `FiveHive - ${subject.title} ${displayUnit}.${chapterIndex + 1} - ${chapter?.title}`; } }, [subject, content, params.unit, params.id]); @@ -64,7 +66,8 @@ const ChapterClient = ({ } if (subject && content) { - const unitIndex = Number(params.unit.split("-")[1]) - 1; + const unitNum = Number(params.unit.split("-")[1]); + const unitIndex = subject.hasUnit0 ? unitNum : unitNum - 1; const chapterIndex = subject.units[unitIndex]!.chapters.findIndex( (ch) => ch.id === params.id, ); @@ -85,6 +88,7 @@ const ChapterClient = ({ chapterTitle={chapter.title} chapterId={params.id} author={content.author} + hasUnit0={subject.hasUnit0} > diff --git a/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterScaffold.tsx b/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterScaffold.tsx index eab96e6e..ef886fe3 100644 --- a/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterScaffold.tsx +++ b/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/ChapterScaffold.tsx @@ -24,6 +24,7 @@ export default function ChapterScaffold({ chapterId, author, children, + hasUnit0, }: { subjectTitle: string; units: Unit[]; @@ -34,6 +35,7 @@ export default function ChapterScaffold({ chapterId: string; author: string; children: React.ReactNode; + hasUnit0?: boolean; }) { return (
@@ -46,7 +48,7 @@ export default function ChapterScaffold({ />

- {unitIndex + 1}.{chapterIndex + 1} - {chapterTitle} + {hasUnit0 ? unitIndex : unitIndex + 1}.{chapterIndex + 1} - {chapterTitle}

{author}

@@ -63,12 +65,14 @@ export default function ChapterScaffold({ units={units} unitIndex={unitIndex} chapterIndex={chapterIndex} + hasUnit0={hasUnit0} />
@@ -83,11 +87,13 @@ function PreviousArticle({ units, unitIndex, chapterIndex, + hasUnit0, }: { subjectTitle: string; units: Unit[]; unitIndex: number; chapterIndex: number; + hasUnit0?: boolean; }) { let unit = units[unitIndex]; let newUnitIndex = unitIndex; @@ -106,10 +112,11 @@ function PreviousArticle({ if (!unit) return null; const subjectSlug = formatSlug(subjectTitle.replace(/AP /g, "")); + const uNum = hasUnit0 ? newUnitIndex : newUnitIndex + 1; return ( @@ -123,11 +130,13 @@ function NextArticle({ units, unitIndex, chapterIndex, + hasUnit0, }: { subjectTitle: string; units: Unit[]; unitIndex: number; chapterIndex: number; + hasUnit0?: boolean; }) { let unit = units[unitIndex]; let newUnitIndex = unitIndex; @@ -147,10 +156,11 @@ function NextArticle({ if (!unit) return null; const subjectSlug = formatSlug(subjectTitle.replace(/AP /g, "")); + const uNum = hasUnit0 ? newUnitIndex : newUnitIndex + 1; return ( Next Chapter diff --git a/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/page.tsx b/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/page.tsx index c3f8ba03..a1487ea1 100644 --- a/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/page.tsx +++ b/src/app/subject/[slug]/(sidebar)/[unit]/chapter/[id]/[title]/page.tsx @@ -20,7 +20,8 @@ type PageParams = { slug: string; unit: string; id: string; title: string }; */ export default async function Page({ params }: { params: PageParams }) { const subject = await getSubject(params.slug); - const unitIndex = Number(params.unit.split("-")[1]) - 1; + const unitNum = Number(params.unit.split("-")[1]); + const unitIndex = subject?.hasUnit0 ? unitNum : unitNum - 1; const unit = subject?.units[unitIndex]; const chapterIndex = unit?.chapters.findIndex((ch) => ch.id === params.id) ?? -1; @@ -49,6 +50,7 @@ export default async function Page({ params }: { params: PageParams }) { chapterTitle={chapter.title} chapterId={params.id} author={content.author} + hasUnit0={subject.hasUnit0} > diff --git a/src/app/subject/[slug]/(sidebar)/page.tsx b/src/app/subject/[slug]/(sidebar)/page.tsx index e41dc81b..a317dd00 100644 --- a/src/app/subject/[slug]/(sidebar)/page.tsx +++ b/src/app/subject/[slug]/(sidebar)/page.tsx @@ -93,6 +93,7 @@ const Page = ({ params }: { params: { slug: string } }) => { key={unitIndex} pathname={pathname} preview={user?.access === "member" || user?.access === "admin"} + hasUnit0={subject.hasUnit0} /> ))} diff --git a/src/components/frq/editorRenderer.tsx b/src/components/frq/editorRenderer.tsx new file mode 100644 index 00000000..0b93bd48 --- /dev/null +++ b/src/components/frq/editorRenderer.tsx @@ -0,0 +1,17 @@ +interface FRQEditorRendererProps { + frqFound: boolean; +} + +const FRQEditorRenderer = ({ + frqFound, +}: FRQEditorRendererProps) => { + return ( +
+ {frqFound + ? "FRQ loaded successfully." + : "Failed to load FRQ."} +
+ ); +}; + +export default FRQEditorRenderer; diff --git a/src/components/subject/subject-sidebar.tsx b/src/components/subject/subject-sidebar.tsx index 1226138a..80ec007e 100644 --- a/src/components/subject/subject-sidebar.tsx +++ b/src/components/subject/subject-sidebar.tsx @@ -57,7 +57,13 @@ const SubjectSidebar = (props: Props) => { type="multiple" // defaultValue={props.subject.units.map((unit) => unit.title)} > - {props.subject.units.map((unit, unitIndex) => ( + {props.subject.units.map((unit, unitIndex) => { + // uNum is the display number shown in the UI and used in URLs. + // When hasUnit0 is true the first array element (index 0) maps to + // "Unit 0", so the display number equals the array index directly. + // Otherwise display numbers start at 1 (classic behaviour). + const uNum = props.subject.hasUnit0 ? unitIndex : unitIndex + 1; + return ( { !props.preview && !chapter.isPublic ? -1 : undefined } key={chapterIndex} - href={`${pathname.split("/").slice(0, 3).join("/")}/unit-${unitIndex + 1}-${unit.id}/chapter/${chapter.id}/${formatSlug(chapter.title)}`} + href={`${pathname.split("/").slice(0, 3).join("/")}/unit-${uNum}-${unit.id}/chapter/${chapter.id}/${formatSlug(chapter.title)}`} >
- {unitIndex + 1}.{chapterIndex + 1} + {uNum}.{chapterIndex + 1}
{chapter.title} @@ -112,7 +118,7 @@ const SubjectSidebar = (props: Props) => { tabIndex={ !props.preview && !test.isPublic ? -1 : undefined } - href={`${pathname.split("/").slice(0, 3).join("/")}/unit-${unitIndex + 1}-${unit.id}/test/${test.id}`} + href={`${pathname.split("/").slice(0, 3).join("/")}/unit-${uNum}-${unit.id}/test/${test.id}`} key={test.id} > {test.isPublic ? ( @@ -124,7 +130,7 @@ const SubjectSidebar = (props: Props) => { {test.name ? test.name : // unit.tests cuz typescript doesn't recognize I checked for unit.tests already - `Unit ${unitIndex + 1} Test ${unit.tests && unit.tests.length > 1 ? ` ${testIndex + 1}` : ""}`} + `Unit ${uNum} Test ${unit.tests && unit.tests.length > 1 ? ` ${testIndex + 1}` : ""}`}

{ - ))} + ); + })} diff --git a/src/components/subject/unit-accordion.tsx b/src/components/subject/unit-accordion.tsx index de3e8524..01857a62 100644 --- a/src/components/subject/unit-accordion.tsx +++ b/src/components/subject/unit-accordion.tsx @@ -14,9 +14,13 @@ type Props = { pathname: string; unitIndex: number; preview: boolean; + hasUnit0?: boolean; }; -const UnitAccordion = ({ unit, pathname, unitIndex, preview }: Props) => { +const UnitAccordion = ({ unit, pathname, unitIndex, preview, hasUnit0 }: Props) => { + // uNum: display number used in URLs and badges. + // 0-indexed when hasUnit0 is true (first array element = Unit 0). + const uNum = hasUnit0 ? unitIndex : unitIndex + 1; return ( { chapter.isPublic ? (

- {unitIndex + 1}.{chapterIndex + 1} + {uNum}.{chapterIndex + 1}
@@ -54,7 +58,7 @@ const UnitAccordion = ({ unit, pathname, unitIndex, preview }: Props) => { key={chapterIndex} >
- {unitIndex + 1}.{chapterIndex + 1} + {uNum}.{chapterIndex + 1}
@@ -63,7 +67,7 @@ const UnitAccordion = ({ unit, pathname, unitIndex, preview }: Props) => { { test.isPublic ? ( {test.name ? test.name : // unit.tests cuz typescript doesn't recognize I checked for unit.tests already - `Unit ${unitIndex + 1} Test ${unit.tests && unit.tests.length > 1 ? ` ${testIndex + 1}` : ""}`} + `Unit ${uNum} Test ${unit.tests && unit.tests.length > 1 ? ` ${testIndex + 1}` : ""}`} ) : (
{ {test.name ? test.name : // unit.tests cuz typescript doesn't recognize I checked for unit.tests already - `Unit ${unitIndex + 1} Test ${unit.tests && unit.tests.length > 1 ? ` ${testIndex + 1}` : ""}`} + `Unit ${uNum} Test ${unit.tests && unit.tests.length > 1 ? ` ${testIndex + 1}` : ""}`} { - const { subjectSlug, unitIndex, unitId, chapterId, chapterTitle } = input; - const unitSegment = `unit-${unitIndex + 1}-${unitId}`; + const { subjectSlug, unitIndex, unitId, chapterId, chapterTitle, hasUnit0 } = input; + const uNum = hasUnit0 ? unitIndex : unitIndex + 1; + const unitSegment = `unit-${uNum}-${unitId}`; return `/subject/${subjectSlug}/${unitSegment}/chapter/${chapterId}/${formatSlug(chapterTitle)}`; }; @@ -237,6 +239,7 @@ const mapSubjectToSearchItems = ( unitId: unit.id, chapterId: chapter.id, chapterTitle: chapter.title, + hasUnit0: subject.hasUnit0, }), normalizedSearchableText: buildNormalizedSearchableText({ subjectTitle: subject.title, diff --git a/src/lib/sitemap-data.ts b/src/lib/sitemap-data.ts index 75aa960e..5f1055ce 100644 --- a/src/lib/sitemap-data.ts +++ b/src/lib/sitemap-data.ts @@ -69,6 +69,7 @@ export interface SitemapSubject { slug: string; title: string; units: SitemapUnit[]; + hasUnit0?: boolean; } /** @@ -92,6 +93,7 @@ export async function getAllSubjects(): Promise { units: (Array.isArray(fields.units) ? fields.units : []) as SitemapUnit[], + hasUnit0: typeof fields.hasUnit0 === "boolean" ? fields.hasUnit0 : false, }; }); } catch { @@ -112,6 +114,7 @@ export async function getSubject(slug: string): Promise { slug, title: typeof fields.title === "string" ? fields.title : slug, units: (Array.isArray(fields.units) ? fields.units : []) as SitemapUnit[], + hasUnit0: typeof fields.hasUnit0 === "boolean" ? fields.hasUnit0 : false, }; } catch { return null; diff --git a/src/types/firestore.ts b/src/types/firestore.ts index 954a1ea8..7319e6e9 100644 --- a/src/types/firestore.ts +++ b/src/types/firestore.ts @@ -4,6 +4,7 @@ import type { QuestionFormat } from "./questions"; export type Subject = { title: string; units: Unit[]; + hasUnit0?: boolean; }; export type Unit = {