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
10 changes: 4 additions & 6 deletions apps/web/app/academic/academic-record.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client";
import { toast } from "sonner";
import { useMemo, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import {
Expand All @@ -11,6 +10,7 @@ import {
} from "@coursemap/ui/primitives/dialog";
import { useCoursemap } from "@/app/providers";
import { AppShell } from "@/ui/shell";
import { showToast } from "@/ui/common/toast";
import { OnboardingPrompt } from "@/ui/common/onboarding-prompt";
import { ResultsEmptyState } from "@/ui/academic/results-empty-state";
import { PreviewLayout } from "@/ui/academic/previews/preview-layout";
Expand Down Expand Up @@ -97,10 +97,8 @@ export function AcademicRecord({ catalogue }: { catalogue: PlanCatalogue }) {
);
if (!response.ok) {
setSelected(null);
toast.error(response.message, {
description: response.detail ? (
<span className="line-clamp-1 break-all">{response.detail}</span>
) : undefined,
showToast(response.message, "error", {
detail: response.detail,
});
return;
}
Expand All @@ -109,7 +107,7 @@ export function AcademicRecord({ catalogue }: { catalogue: PlanCatalogue }) {
router.refresh();
} catch {
setSelected(null);
notify("We couldn't save that result. Try again.", "error");
notify("Couldn't save that result. Try again.", "error");
}
});
}
Expand Down
4 changes: 1 addition & 3 deletions apps/web/app/admin/courses/[year]/(directory)/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading";

export default function Loading() {
return (
<CatalogueLoading noun="courses" layout="directory" hideAcademicYear />
);
return <CatalogueLoading noun="courses" layout="directory" />;
}
2 changes: 1 addition & 1 deletion apps/web/app/admin/majors/[year]/(directory)/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading";

export default function Loading() {
return <CatalogueLoading noun="majors" layout="directory" hideAcademicYear />;
return <CatalogueLoading noun="majors" layout="directory" />;
}
2 changes: 1 addition & 1 deletion apps/web/app/admin/minors/[year]/(directory)/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading";

export default function Loading() {
return <CatalogueLoading noun="minors" layout="directory" hideAcademicYear />;
return <CatalogueLoading noun="minors" layout="directory" />;
}
4 changes: 1 addition & 3 deletions apps/web/app/admin/programmes/[year]/(directory)/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading";

export default function Loading() {
return (
<CatalogueLoading noun="programmes" layout="directory" hideAcademicYear />
);
return <CatalogueLoading noun="programmes" layout="directory" />;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading";

export default function Loading() {
return (
<CatalogueLoading
noun="specialisations"
layout="directory"
hideAcademicYear
/>
);
return <CatalogueLoading noun="specialisations" layout="directory" />;
}
1 change: 0 additions & 1 deletion apps/web/app/courses/(directory)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export default async function CoursesPage({
<h1 className="sr-only">Explore courses</h1>
<div className="mx-auto flex min-h-0 w-full flex-1 flex-col gap-5">
<FilterBar
key={query}
searchPlaceholder="Search by course code, name or school"
filters={[
{
Expand Down
39 changes: 0 additions & 39 deletions apps/web/app/courses/course-directory.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Link from "next/link";
import { LinkedTableRow } from "@/ui/common/linked-table-row";
import { CatalogueIdentity } from "@/ui/admin/catalogue-table/catalogue-table";
import { CatalogueEmpty } from "@/ui/admin/catalogue-table/catalogue-empty";
Expand All @@ -13,14 +12,10 @@ import {
TableRow,
} from "@/ui/admin/catalogue-table/catalogue-table";
import type { CourseDetails } from "@/lib/coursemap/course-types";
import { cn } from "@/lib/cn";
import { Pagination } from "@/ui/common/pagination";
import { CourseAvailability } from "@/ui/courses/course-availability";
import { CourseRowActions } from "./course-row-actions";

const chipClasses =
"rounded-md bg-muted/50 px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground ring-1 ring-border ring-inset";

export function CourseDirectory({
academicYear,
courses,
Expand Down Expand Up @@ -68,7 +63,6 @@ export function CourseDirectory({
<TableRow className="hover:bg-transparent">
<TableHead>Course</TableHead>
<TableHead>Year</TableHead>
<TableHead>Requisites</TableHead>
<TableHead>Available</TableHead>
<TableHead>Units</TableHead>
<TableHead>
Expand All @@ -89,39 +83,6 @@ export function CourseDirectory({
/>
</TableCell>
<TableCell>{academicYear}</TableCell>
<TableCell>
<div className="flex min-h-10 flex-wrap items-center gap-1">
{course.prerequisiteCodes.length === 0 ? (
<span className="text-[13px] text-muted-foreground/80">
None
</span>
) : (
course.prerequisiteCodes.map((prerequisite) =>
course.availableCourseCodes.includes(prerequisite) ? (
<Link
key={prerequisite}
href={`/courses/${academicYear}/${prerequisite.toLowerCase()}`}
aria-label={`View prerequisite ${prerequisite}`}
className={cn(
chipClasses,
"font-mono transition-colors hover:bg-card hover:text-primary hover:ring-primary/25 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring motion-reduce:transition-none",
)}
>
{prerequisite}
</Link>
) : (
<span
key={prerequisite}
className={cn(chipClasses, "font-mono")}
title={`${prerequisite} is not published for ${academicYear}`}
>
{prerequisite}
</span>
),
)
)}
</div>
</TableCell>
<TableCell>
<CourseAvailability
courseCode={course.code}
Expand Down
5 changes: 1 addition & 4 deletions apps/web/app/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,7 @@ export function Dashboard({

return (
<AppShell>
<div
className="mx-auto flex flex-col gap-6"
data-entered={entered || undefined}
>
<div className="flex flex-col gap-6" data-entered={entered || undefined}>
<h1 className="sr-only">Dashboard</h1>

<section aria-label="Your metrics" className="flex flex-col gap-4">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/help/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default async function HelpArticlePage({
<AppShell currentBreadcrumbLabel={article.title}>
<div
key={article.slug}
className="mx-auto grid animate-fade-in items-start gap-10 py-2 sm:py-4 lg:grid-cols-[minmax(0,1fr)_18rem] lg:gap-10"
className="mx-auto grid w-full animate-fade-in items-start gap-10 py-2 sm:py-4 lg:grid-cols-[minmax(0,1fr)_18rem] lg:gap-10"
>
<article className="max-w-3xl min-w-0">
<h1 className="sr-only">{article.title}</h1>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/help/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AppShell } from "@/ui/shell";
export default function HelpPage() {
return (
<AppShell>
<div className="mx-auto animate-fade-in space-y-12 py-2 sm:py-4">
<div className="mx-auto w-full animate-fade-in space-y-12 py-2 sm:py-4">
<h1 className="sr-only">Help centre</h1>
<HelpGuides />
<HelpContact />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/profile/profile-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export function ProfileEditor({
async function save(event: FormEvent) {
event.preventDefault();
if (nameError || !draft.degreeCode) {
notify("Add your name and select a degree before saving", "warning");
notify("Add your name and degree before saving", "warning");
return;
}
if (studentNumberError) {
Expand Down
19 changes: 8 additions & 11 deletions apps/web/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useInputModality } from "@/lib/browser/use-input-modality";
import { Toaster } from "@coursemap/ui/primitives/sonner";
import type { CSSProperties } from "react";
import { toast } from "sonner";
import { showToast, type ToastTone } from "@/ui/common/toast";
import { useTheme } from "next-themes";
import {
createContext,
Expand All @@ -27,8 +27,6 @@ import {
type CoursemapActionResult,
} from "@/lib/coursemap/actions";

type ToastTone = "success" | "warning" | "info" | "error";

type AppContextValue = {
state: AppState;
ready: boolean;
Expand Down Expand Up @@ -129,10 +127,7 @@ export function AppProvider({
}, [viewer]);

const notify = useCallback((message: string, tone: ToastTone = "success") => {
if (tone === "error") toast.error(message);
else if (tone === "warning") toast.warning(message);
else if (tone === "info") toast.info(message);
else toast.success(message);
showToast(message, tone);
}, []);

const updateProfile = useCallback(
Expand All @@ -142,7 +137,7 @@ export function AppProvider({
const result = await saveProfileAndPlan(nextProfile);
if (!result.ok) return result;
setState((current) => ({ ...current, profile: nextProfile }));
return { ok: true, message: "Profile and academic plan saved" };
return { ok: true, message: "Profile saved" };
},
[state.profile],
);
Expand Down Expand Up @@ -247,7 +242,8 @@ export function AppProvider({
attemptedUnits?: number,
) => {
const attempt = state.attempts.find((item) => item.id === attemptId);
if (!attempt) return { ok: false, message: "Course was not found" };
if (!attempt)
return { ok: false, message: "That course is no longer in your plan" };
if (attempt.status !== "planned") {
return {
ok: false,
Expand Down Expand Up @@ -312,7 +308,8 @@ export function AppProvider({
message: "Recorded attempts stay in your academic history",
};
}
if (!attempt) return { ok: false, message: "Course was not found" };
if (!attempt)
return { ok: false, message: "That course is no longer in your plan" };
const result = await removePlanCourse(attemptId);
if (!result.ok) return result;
setState((current) => ({
Expand Down Expand Up @@ -387,7 +384,7 @@ export function AppProvider({
: "system"
}
position="top-center"
style={{ "--width": "560px" } as CSSProperties}
style={{ "--width": "420px" } as CSSProperties}
closeButton
visibleToasts={3}
/>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/requirements/requirements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function Requirements({
router.refresh();
}
} catch {
notify("Your selection could not be saved. Try again.", "warning");
notify("Couldn't save your selection. Try again.", "warning");
} finally {
setPending(false);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/roadmap/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ const stages: RoadmapStage[] = [
export default function RoadmapPage() {
return (
<AppShell>
<div className="py-2 sm:py-4">
<div className="flex flex-1 flex-col py-2 sm:py-4">
<h1 className="sr-only">Roadmap</h1>
<RoadmapExplorer stages={stages} />
</div>
Expand Down
27 changes: 27 additions & 0 deletions apps/web/app/vendor.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@
color: var(--toast-status-colour, inherit);
}

/* A toast is a short title and at most two lines of detail under it. The
title is clamped too, so a long server message can never grow the toast
into a paragraph; toastParts in ui/common/toast.tsx keeps titles short. */
.toaster [data-sonner-toast] [data-title] {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
font-weight: 500;
line-height: 1.35;
}

.toaster [data-sonner-toast] [data-description] {
color: var(--muted-foreground);
line-height: 1.4;
}

.toaster [data-sonner-toast][data-styled="true"] [data-close-button] {
top: 12px;
right: 12px;
Expand Down Expand Up @@ -168,6 +185,16 @@
pointer-events: none;
}

/* Collapsed inset sidebar: the rail already ends 16px past the icons, so the
panel drops its extra left margin to match the 16px on the left. */
@media (width >= 48rem) {
.style-nova
.peer[data-variant="inset"][data-state="collapsed"]
~ .cn-sidebar-inset {
margin-left: 0;
}
}

/* Compact tabs in light keep an outlined track so the white active tab reads
against the muted fill. */
:root:not(.dark-mode) .style-nova .cn-tabs-list[data-variant="default"] {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/academic/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function saveAcademicResult(
const { canAccessAdmin } = await getAuthContext();
return {
ok: false,
message: "An unexpected error occurred",
message: "Couldn't save that result",
...(canAccessAdmin ? { detail } : {}),
};
}
Expand Down
6 changes: 3 additions & 3 deletions apps/web/lib/coursemap/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function failure(error: unknown): CoursemapActionResult {
const rawMessage =
error && typeof error === "object" && "message" in error
? String(error.message)
: "Coursemap could not save that change.";
: "Couldn't save that change. Try again.";
const message = rawMessage.includes("profiles_student_number_format_check")
? "Enter a student number in the format u1234567, or leave it blank."
: rawMessage;
Expand Down Expand Up @@ -65,7 +65,7 @@ export async function saveProfileAndPlan(
);
if (error) throw error;
revalidatePath("/", "layout");
return { ok: true, id: data, message: "Profile and academic plan saved" };
return { ok: true, id: data, message: "Profile saved" };
} catch (error) {
return failure(error);
}
Expand Down Expand Up @@ -148,7 +148,7 @@ export async function removePlanCourse(
if (data) revalidatePath("/plan");
return data
? { ok: true, message: "Course removed from the plan" }
: { ok: false, message: "Course was not found in your plan" };
: { ok: false, message: "That course is no longer in your plan" };
} catch (error) {
return failure(error);
}
Expand Down
21 changes: 14 additions & 7 deletions apps/web/lib/coursemap/admin-catalogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,14 +299,21 @@ export async function loadCatalogueDirectoryPage({
: null,
} satisfies CatalogueDirectoryRecord;
});
const needle = query.trim().toUpperCase();
// Every word must appear somewhere in the code or title, in any order and
// ignoring punctuation, so "Human Centred Computing" finds
// "Human-Centred and Creative Computing".
const words = query
.toUpperCase()
.split(/[^A-Z0-9]+/u)
.filter(Boolean);
const filtered = rows
.filter(
(row) =>
!needle ||
row.code.includes(needle) ||
(row.title ?? "").toUpperCase().includes(needle),
)
.filter((row) => {
if (!words.length) return true;
const haystack = `${row.code} ${row.title ?? ""}`
.toUpperCase()
.replace(/[^A-Z0-9]+/gu, " ");
return words.every((word) => haystack.includes(word));
})
// The whole year is already in memory, so narrowing by state costs a pass
// rather than a query, and it agrees with the badge by construction.
.filter((row) => !state || catalogueRecordState(row) === state)
Expand Down
Loading
Loading