Skip to content
Merged
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
37 changes: 36 additions & 1 deletion apps/web/app/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
import { DegreeComposition } from "@/ui/dashboard/degree-composition";
import { AppShell } from "@/ui/shell";
import type { PlanCatalogue } from "@/lib/coursemap/plan-catalogue";
import type { OnboardingCatalogue } from "@/lib/coursemap/onboarding-catalogue";
import { degreeComposition } from "@/lib/coursemap/degree-composition";
import {
cumulativeDashboardUnits,
currentDashboardTermId,
Expand Down Expand Up @@ -70,7 +72,13 @@ function finishLabelFor(
return `${term?.name ?? last.label} ${last.year}`;
}

export function Dashboard({ catalogue }: { catalogue: PlanCatalogue }) {
export function Dashboard({
catalogue,
choices,
}: {
catalogue: PlanCatalogue;
choices: OnboardingCatalogue;
}) {
const { state } = useCoursemap();
const previewMetrics = process.env.NODE_ENV === "development";
const degree = catalogue.degrees.find(
Expand Down Expand Up @@ -159,6 +167,31 @@ export function Dashboard({ catalogue }: { catalogue: PlanCatalogue }) {
[catalogue.structureRequirements, planningCatalogue, state.attempts],
);

const composition = useMemo(() => {
const inYear = <T extends { catalogueYear: number }>(items: T[]) =>
items.filter((item) => item.catalogueYear === catalogue.academicYear);
return degreeComposition({
degreeUnits: unitTarget,
profile: state.profile,
programme:
inYear(choices.degrees).find(
(item) => item.code === state.profile.degreeCode,
) ?? null,
structureOptions: [...inYear(choices.majors), ...inYear(choices.minors)],
requirements: catalogue.structureRequirements,
attempts: state.attempts,
catalogue: planningCatalogue,
});
}, [
catalogue.academicYear,
catalogue.structureRequirements,
choices,
planningCatalogue,
state.attempts,
state.profile,
unitTarget,
]);

const academicInputs = useMemo(
() => ({ ...planningCatalogue, attempts: state.attempts }),
[planningCatalogue, state.attempts],
Expand Down Expand Up @@ -313,6 +346,8 @@ export function Dashboard({ catalogue }: { catalogue: PlanCatalogue }) {

<div className="grid items-stretch gap-4 lg:grid-cols-[minmax(0,1fr)_19rem]">
<DegreeComposition
sections={composition}
academicYear={catalogue.academicYear}
courseLinks={Object.fromEntries(
catalogue.courses.map((course) => [
course.code,
Expand Down
11 changes: 8 additions & 3 deletions apps/web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PlanningCatalogueError } from "@/ui/plan/planning-catalogue-error";
import { loadCurrentUserPlanCatalogue } from "@/lib/coursemap/plan-catalogue";
import { loadOnboardingCatalogue } from "@/lib/coursemap/onboarding-catalogue";
import { Dashboard } from "./dashboard";

export const dynamic = "force-dynamic";
Expand All @@ -9,13 +10,17 @@ export const dynamic = "force-dynamic";
* which offers onboarding, rather than being redirected into it.
*/
export default async function DashboardPage() {
let catalogue;
let data;
try {
catalogue = await loadCurrentUserPlanCatalogue();
const [catalogue, choices] = await Promise.all([
loadCurrentUserPlanCatalogue(),
loadOnboardingCatalogue(),
]);
data = { catalogue, choices };
} catch {
return (
<PlanningCatalogueError pageTitle="Dashboard" retryHref="/dashboard" />
);
}
return <Dashboard catalogue={catalogue} />;
return <Dashboard {...data} />;
}
15 changes: 13 additions & 2 deletions apps/web/app/requirements/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PlanningCatalogueError } from "@/ui/plan/planning-catalogue-error";
import {
isPlanStructureKind,
loadCurrentUserPlanCatalogue,
planCourseFromDetails,
} from "@/lib/coursemap/plan-catalogue";
Expand All @@ -10,7 +11,12 @@ import { Requirements } from "./requirements";

export const dynamic = "force-dynamic";

export default async function RequirementsPage() {
export default async function RequirementsPage({
searchParams,
}: {
searchParams: Promise<{ tab?: string }>;
}) {
const { tab } = await searchParams;
let data;
try {
const [catalogue, choices] = await Promise.all([
Expand Down Expand Up @@ -50,5 +56,10 @@ export default async function RequirementsPage() {
);
}

return <Requirements {...data} />;
return (
<Requirements
{...data}
initialTab={tab && isPlanStructureKind(tab) ? tab : "programme"}
/>
);
}
5 changes: 4 additions & 1 deletion apps/web/app/requirements/requirements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ const sections = [
export function Requirements({
catalogue,
choices,
initialTab = "programme",
}: {
catalogue: PlanCatalogue;
choices: OnboardingCatalogue;
/** The section to open first, such as the major when adding one. */
initialTab?: PlanStructureKind;
}) {
const { state, updateProfile, notify } = useCoursemap();
const router = useRouter();
const [tab, setTab] = useState<PlanStructureKind>("programme");
const [tab, setTab] = useState<PlanStructureKind>(initialTab);
const [choosing, setChoosing] = useState(false);
const [pending, setPending] = useState(false);
const [addingCourse, setAddingCourse] = useState<Course | null>(null);
Expand Down
29 changes: 29 additions & 0 deletions apps/web/hooks/use-element-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";
import { useEffect, useRef, useState } from "react";

/**
* The rendered size of an element, following resizes. Until the first
* measurement it reports `fallback`, so server and first client render agree.
*/
export function useElementSize<T extends HTMLElement>(fallback: {
width: number;
height: number;
}) {
const ref = useRef<T | null>(null);
const [size, setSize] = useState(fallback);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setSize((current) =>
current.width === width && current.height === height
? current
: { width, height },
);
});
observer.observe(element);
return () => observer.disconnect();
}, []);
return [ref, size] as const;
}
Loading
Loading