+ );
+}
diff --git a/apps/web/ui/requirements/structure-detail-view.tsx b/apps/web/ui/requirements/structure-detail-view.tsx
index f626442c..de48509a 100644
--- a/apps/web/ui/requirements/structure-detail-view.tsx
+++ b/apps/web/ui/requirements/structure-detail-view.tsx
@@ -1,6 +1,5 @@
"use client";
-import Link from "next/link";
import {
Banknote,
BookOpen,
@@ -28,11 +27,7 @@ import {
TabsList,
TabsTrigger,
} from "@coursemap/ui/primitives/tabs";
-import {
- CATALOGUE_KIND_LABELS,
- publicCatalogueRecordPath,
-} from "@/lib/coursemap/catalogue-kinds";
-import { isCatalogueKind } from "@/lib/catalogue/content";
+import { CATALOGUE_KIND_LABELS } from "@/lib/coursemap/catalogue-kinds";
import type {
StructureDetails,
StructureFee,
@@ -42,10 +37,15 @@ import {
STRUCTURE_FEE_BASIS_LABELS,
STRUCTURE_FEE_TYPE_LABELS,
} from "@/lib/coursemap/structure-types";
-import { STRUCTURE_RELATIONSHIP_LABELS } from "@/lib/catalogue/structure-vocabulary";
+import { CatalogueMarkdown } from "@/ui/common/catalogue-markdown";
import { SectionNavigation } from "@/ui/common/section-navigation";
import { RequirementGroupView } from "@/ui/requirements/requirement-tree";
import type { TreeContext } from "@/ui/requirements/requirement-presentation";
+import { StructureRelated } from "@/ui/requirements/structure-related";
+import {
+ StructureSectionCard,
+ structureSectionAnchor,
+} from "@/ui/requirements/structure-section";
export const structureDetailTabs = [
{ id: "overview", label: "Overview", icon: BookOpen },
@@ -86,54 +86,6 @@ function feeAmount(fee: StructureFee) {
return basis ? `${amount} ${basis}` : amount;
}
-const CODE_LINE = /^[A-Z]{4}[0-9]{4}[A-Z]?$|^[A-Z0-9][A-Z0-9-]{1,31}$/u;
-
-/**
- * ANU section bodies arrive as one line per scraped element, so rendering them
- * as pre-wrapped text produced a wall with no rhythm: a course code, its
- * title and its unit value read as three unrelated sentences. Each line is
- * given its own row, and a bare code is set in the monospace face so a study
- * plan scans as a list of courses rather than prose.
- */
-function SectionLines({ markdown }: { markdown: string }) {
- const lines = markdown
- .split(/\r?\n/)
- .map((line) => line.trim())
- .filter(Boolean);
- if (lines.length === 0)
- return (
-
- The ANU page left this section empty.
-
- );
- if (lines.length === 1)
- return (
-
{lines[0]}
- );
- return (
-
- {lines.map((line, index) => (
-
- {line}
-
- ))}
-
- );
-}
-
-function sectionAnchor(sectionKey: string) {
- return `section-${sectionKey}`;
-}
-
/**
* The reader's body of a structure page. The student route and the import
* preview both render this, so a draft preview cannot drift away from what a
@@ -166,7 +118,9 @@ export function StructureDetailView({
["Study as", structure.studyAs],
].filter((entry): entry is [string, string] => Boolean(entry[1]));
- const informationSections = structure.sections;
+ const availableCourseCodes = new Set(
+ treeContext.catalogue.courses.map((course) => course.code),
+ );
return (
+
+
+
+ {children}
+
+
+ );
+}
+
+/**
+ * The records a structure relates to, one card per meaning: the degrees it
+ * can be studied in, the majors, minors and specialisations a programme
+ * offers, and what it cannot be combined with. Each card appears only when
+ * the structure has something to put in it.
+ */
+export function StructureRelated({
+ structure,
+}: {
+ structure: StructureDetails;
+}) {
+ const offeredIn = uniqueTargets(structure.relationships, "offered_in");
+ const optionGroups = OPTION_GROUPS.map((group) => ({
+ ...group,
+ records: uniqueTargets(structure.relationships, "option", group.kind),
+ })).filter((group) => group.records.length);
+ const incompatible = uniqueTargets(structure.relationships, "incompatible");
+
+ return (
+ <>
+ {offeredIn.length ? (
+
+
+
+ ) : null}
+ {optionGroups.length ? (
+
+ {optionGroups.map((group) => (
+
+
+ {group.title}
+
+
+
+ ))}
+
+ ) : null}
+ {incompatible.length ? (
+
+
+
+ ) : null}
+ >
+ );
+}
diff --git a/apps/web/ui/requirements/structure-section.tsx b/apps/web/ui/requirements/structure-section.tsx
new file mode 100644
index 00000000..d3a70972
--- /dev/null
+++ b/apps/web/ui/requirements/structure-section.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@coursemap/ui/primitives/card";
+import type { StructureSection } from "@/lib/coursemap/structure-types";
+import { CatalogueMarkdown } from "@/ui/common/catalogue-markdown";
+
+export function structureSectionAnchor(section: StructureSection) {
+ return `section-${section.sectionKey}`;
+}
+
+/** One of a structure's fixed information sections, as tidied prose. */
+export function StructureSectionCard({
+ section,
+ academicYear,
+ availableCourseCodes,
+}: {
+ section: StructureSection;
+ academicYear: number;
+ availableCourseCodes: ReadonlySet;
+}) {
+ return (
+
+
+
+