Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/(app)/past_papers/[code]/[exam]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getCourseSyllabusPath,
} from "@/lib/seo";
import CourseHeader from "@/app/components/past_papers/course-header";
import StudyPlanCta from "@/app/components/study-plan/plan-cta";
import CoursePaperGrid from "@/app/components/past_papers/course-paper-grid";
import {
DESKTOP_SELECT_ALL_HOST_ID,
Expand Down Expand Up @@ -191,7 +192,9 @@ async function CourseExamContent({
]}
/>

<section className="rounded-md border border-black/10 bg-white p-4 dark:border-[#D5D5D5]/10 dark:bg-[#0C1222]">
<StudyPlanCta courseCode={course.code} examType={examType} />

<section className="rounded-md border border-black/10 bg-white p-4 dark:border-[#D5D5D5]/10 dark:bg-[#0C1222]">
<p className="sr-only">
Open the dedicated {label} collection for {course.code} when you want a
focused set of papers for one exam pattern instead of the full course list.
Expand Down
4 changes: 4 additions & 0 deletions app/(app)/past_papers/[code]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from "@/app/components/past_papers/course-paper-grid-controls";
import CoursePagination from "@/app/components/past_papers/course-pagination";
import CourseVisitTracker from "@/app/components/past_papers/course-visit-tracker";
import StudyPlanCta from "@/app/components/study-plan/plan-cta";
import {
campusValues,
course as courseTable,
Expand Down Expand Up @@ -602,6 +603,7 @@ async function CoursePastPapersPageContent({
noteCount={course.noteCount}
syllabusId={null}
>
<StudyPlanCta courseCode={course.code} variant="compact" />
<Suspense fallback={null}>
<CourseHeaderSyllabusAction
code={course.code}
Expand All @@ -610,6 +612,8 @@ async function CoursePastPapersPageContent({
</Suspense>
</CourseHeader>

<StudyPlanCta courseCode={course.code} />

<Suspense fallback={<CoursePastPapersSectionsShell />}>
<CoursePastPapersContent
course={course}
Expand Down
92 changes: 92 additions & 0 deletions app/(app)/study-plan/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { Metadata } from "next";
import DirectionalTransition from "@/app/components/common/directional-transition";
import StudyPlanExperience from "@/app/components/study-plan/study-plan-experience";
import StructuredData from "@/app/components/seo/structured-data";
import {
getCoursePickerRecords,
getUpcomingExamsCourseGrid,
type CourseSearchRecord,
} from "@/lib/data/course-catalog";
import { buildBreadcrumbList } from "@/lib/structured-data";

type CoursePick = {
id: string;
code: string;
title: string;
paperCount: number;
};

type SearchParams = {
course?: string;
exam?: string;
slot?: string;
};

export const metadata: Metadata = {
title: "Study plan | ExamCooker",
description:
"Build a syllabus-grounded study queue from ExamCooker past papers, slot reports, resources, and web research.",
alternates: { canonical: "/study-plan" },
};

export default async function StudyPlanPage({
searchParams,
}: {
searchParams?: Promise<SearchParams>;
}) {
const [params, courses, upcoming] = await Promise.all([
searchParams ?? Promise.resolve({} as SearchParams),
getCoursePickerRecords(),
getUpcomingExamsCourseGrid(),
]);

// Resolve the curated upcoming-exam courses to full search records (keeping
// the curated order) so they can seed the planner as one-tap entry points.
const byCode = new Map(courses.map((course) => [course.code, course]));
const suggested: CoursePick[] = upcoming
.map((item) => byCode.get(item.code))
.filter((course): course is CourseSearchRecord => Boolean(course));

// A second band of real entry points: the courses with the most material that
// aren't already shown as upcoming exams. Always full, always useful.
const upcomingCodes = new Set(upcoming.map((item) => item.code));
const popularPicks: CoursePick[] = courses
.filter((course) => !upcomingCodes.has(course.code) && course.paperCount > 0)
.sort((a, b) => b.paperCount - a.paperCount)
.slice(0, 12)
.map((course) => ({
id: course.id,
code: course.code,
title: course.title,
paperCount: course.paperCount,
}));

return (
<DirectionalTransition>
<main className="relative min-h-dvh overflow-hidden bg-[#C2E6EC] text-black dark:bg-[hsl(224,48%,9%)] dark:text-[#D5D5D5]">
<StructuredData
data={[
buildBreadcrumbList([
{ name: "Home", path: "/" },
{ name: "Study plan", path: "/study-plan" },
]),
]}
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-0 h-[520px] bg-[radial-gradient(72%_100%_at_22%_-10%,rgba(39,186,236,0.10),transparent_62%)] dark:bg-[radial-gradient(72%_100%_at_22%_-10%,rgba(39,186,236,0.14),transparent_60%)]"
/>
<div className="relative z-10 mx-auto flex w-full max-w-7xl flex-col gap-6 px-3 py-6 sm:px-6 sm:py-8 lg:px-10 lg:py-12">
<StudyPlanExperience
courses={courses}
suggested={suggested}
popular={popularPicks}
initialCourse={params.course ?? ""}
initialExam={params.exam ?? ""}
initialSlot={params.slot ?? ""}
/>
</div>
</main>
</DirectionalTransition>
);
}
3 changes: 3 additions & 0 deletions app/(app)/syllabus/course/[code]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ViewTracker from "@/app/components/view-tracker";
import { LazySyllabusInlineEditor } from "@/app/components/moderation/lazy-editors";
import StructuredData from "@/app/components/seo/structured-data";
import DirectionalTransition from "@/app/components/common/directional-transition";
import StudyPlanCta from "@/app/components/study-plan/plan-cta";
import { getCourseByCodeAny } from "@/lib/data/courses";
import { getCourseDetailByCode } from "@/lib/data/course-catalog";
import { getSubjectByCourseCode } from "@/lib/data/resources";
Expand Down Expand Up @@ -189,6 +190,8 @@ async function CourseSyllabusContent({
</div>
</header>

<StudyPlanCta courseCode={context.code} />

<div className="overflow-hidden border border-black/15 bg-white shadow-[0_4px_28px_-14px_rgba(0,0,0,0.25)] dark:border-[#D5D5D5]/15 dark:bg-[#0C1222] dark:shadow-[0_4px_28px_-14px_rgba(0,0,0,0.6)]">
<div className="h-[70dvh] sm:h-[78dvh] lg:h-[84dvh] xl:h-[86dvh]">
<PDFViewerClient
Expand Down
Loading