{article.title}
diff --git a/apps/web/app/help/page.tsx b/apps/web/app/help/page.tsx index 126df47a..5f0464ff 100644 --- a/apps/web/app/help/page.tsx +++ b/apps/web/app/help/page.tsx @@ -5,7 +5,7 @@ import { AppShell } from "@/ui/shell"; export default function HelpPage() { return (Help centre
Roadmap
Loading {noun}
{labels.plural}
{title}
{description}
@@ -135,10 +135,10 @@ export async function CatalogueRecordPage({
{`Sync from ANU or write the ${kindLabel} content, and this is what students will read.`}
diff --git a/apps/web/ui/admin/catalogue/sync-button.tsx b/apps/web/ui/admin/catalogue/sync-button.tsx
index 2192dedf..856cd499 100644
--- a/apps/web/ui/admin/catalogue/sync-button.tsx
+++ b/apps/web/ui/admin/catalogue/sync-button.tsx
@@ -22,20 +22,19 @@ const SYNC_PROGRESS: Record<
running: { percent: 50, ceiling: 88, detail: "Reading the ANU page." },
};
-const SYNC_OUTCOMES = {
- applied: {
- title: "ANU changes applied",
- detail: "The record is up to date.",
- },
- review_required: {
- title: "ANU changes need review",
- detail: "Open the changes to accept or reject them.",
- },
- unchanged: {
- title: "No ANU changes",
- detail: "ANU has not changed this record since the last sync.",
- },
-} as const;
+function syncOutcome(code: string, status: string) {
+ if (status === "review_required")
+ return {
+ title: `${code} has ANU changes to review`,
+ detail: "Open Changes to accept or reject them.",
+ };
+ if (status === "unchanged")
+ return {
+ title: `${code} is up to date`,
+ detail: "Nothing has changed on ANU since the last sync.",
+ };
+ return { title: `${code} updated from ANU` };
+}
export function CatalogueSyncButton({
recordId,
@@ -78,8 +77,8 @@ export function CatalogueSyncButton({
reportedStatus.current = null;
task.current = startTask({
id: `sync:${recordId}`,
- title: `Syncing ${code} from ANU`,
- detail: "Asking ANU for the latest version.",
+ title: `Syncing ${code}`,
+ detail: "Contacting ANU.",
ceiling: 12,
});
startTransition(async () => {
@@ -94,8 +93,8 @@ export function CatalogueSyncButton({
};
if (!response.ok || !result.syncId) {
task.current?.fail({
- title: `Syncing ${code} from ANU could not start`,
- detail: result.error ?? "The sync did not return an identifier.",
+ title: `Couldn't start the ${code} sync`,
+ detail: result.error ?? "The server did not return a sync.",
retry: retrySync,
});
task.current = null;
@@ -117,8 +116,8 @@ export function CatalogueSyncButton({
useEffect(
() => () =>
task.current?.abandon({
- title: "The ANU sync is still running",
- detail: "Open the record again to see how it finished.",
+ title: "Sync still running",
+ detail: "Reopen the record to see the result.",
}),
[],
);
@@ -137,20 +136,16 @@ export function CatalogueSyncButton({
}
if (status === "failed") {
task.current?.fail({
- title: `Syncing ${code} from ANU failed`,
+ title: `${code} sync failed`,
detail: latestSync.errorMessage ?? "The sync did not finish.",
retry: retrySync,
});
} else if (status === "cancelled") {
task.current?.note({
- title: `Syncing ${code} from ANU was cancelled`,
+ title: `${code} sync cancelled`,
});
} else {
- const outcome = SYNC_OUTCOMES[status as keyof typeof SYNC_OUTCOMES];
- task.current?.done({
- title: outcome?.title ?? `${code} synced from ANU`,
- detail: outcome?.detail,
- });
+ task.current?.done(syncOutcome(code, status));
}
task.current = null;
}, [code, latestSync, retrySync, startedSyncId]);
diff --git a/apps/web/ui/admin/imports/import-model-card.tsx b/apps/web/ui/admin/imports/import-model-card.tsx
index deaa41b8..cfdc1985 100644
--- a/apps/web/ui/admin/imports/import-model-card.tsx
+++ b/apps/web/ui/admin/imports/import-model-card.tsx
@@ -3,7 +3,6 @@
import { useRef, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { ChevronsUpDown, Cpu, Plus, Settings2 } from "lucide-react";
-import { toast } from "sonner";
import { Button } from "@coursemap/ui/primitives/button";
import { Card } from "@coursemap/ui/primitives/card";
import {
@@ -19,6 +18,7 @@ import { cn } from "@/lib/cn";
import { ImportModelLogo } from "./import-model-logo";
import { ImportModelPrice } from "./import-model-price";
import { ImportModelManager } from "./import-model-manager";
+import { showToast } from "@/ui/common/toast";
const dateFormatter = new Intl.DateTimeFormat("en-AU", {
day: "numeric",
@@ -50,13 +50,13 @@ export function ImportModelCard({
try {
const result = await setImportModel(next);
if (!result.ok) {
- toast.error(result.message);
+ showToast(result.message, "error");
return;
}
- toast.success(result.message);
+ showToast(result.message);
router.refresh();
} catch {
- toast.error("The default model could not be saved. Try again.");
+ showToast("Couldn't save the default model. Try again.", "error");
}
});
}
diff --git a/apps/web/ui/admin/imports/import-model-manager.tsx b/apps/web/ui/admin/imports/import-model-manager.tsx
index 738ee670..e5f5b37a 100644
--- a/apps/web/ui/admin/imports/import-model-manager.tsx
+++ b/apps/web/ui/admin/imports/import-model-manager.tsx
@@ -3,7 +3,6 @@
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { ChevronDown } from "lucide-react";
-import { toast } from "sonner";
import { Button } from "@coursemap/ui/primitives/button";
import { Input } from "@coursemap/ui/primitives/input";
import { Label } from "@coursemap/ui/primitives/label";
@@ -21,6 +20,7 @@ import {
import type { ImportModel } from "@/lib/admin/import-model";
import { ImportModelRow } from "./import-model-row";
import type { ImportModelRowAction } from "./import-model-row";
+import { showToast } from "@/ui/common/toast";
export function ImportModelManager({
open,
@@ -55,7 +55,7 @@ export function ImportModelManager({
return;
}
if (action === "add" && model === id) setId("");
- toast.success(result.message);
+ showToast(result.message);
router.refresh();
} catch {
setError("The model could not be saved. Try again.");
diff --git a/apps/web/ui/admin/key-dates/key-date-dialog.tsx b/apps/web/ui/admin/key-dates/key-date-dialog.tsx
index ad3dfe09..03cc43fc 100644
--- a/apps/web/ui/admin/key-dates/key-date-dialog.tsx
+++ b/apps/web/ui/admin/key-dates/key-date-dialog.tsx
@@ -2,7 +2,6 @@
import { useId, useState, type FormEvent, type ReactNode } from "react";
import { Info } from "lucide-react";
-import { toast } from "sonner";
import { Button } from "@coursemap/ui/primitives/button";
import {
Dialog,
@@ -21,6 +20,7 @@ import {
} from "@/lib/admin/key-dates-actions";
import { DatePicker } from "@/ui/common/date-picker";
import { Hint } from "@/ui/common/hint";
+import { showToast } from "@/ui/common/toast";
export type KeyDateDraft = { id?: number; date: string; title: string };
@@ -98,7 +98,7 @@ export function KeyDateDialog({
setError(result.message);
return;
}
- toast.success(result.message);
+ showToast(result.message);
changeOpen(false);
} catch {
setError("The key date could not be saved. Try again.");
diff --git a/apps/web/ui/admin/key-dates/key-date-row-menu.tsx b/apps/web/ui/admin/key-dates/key-date-row-menu.tsx
index 6a1e6cfe..e54df80c 100644
--- a/apps/web/ui/admin/key-dates/key-date-row-menu.tsx
+++ b/apps/web/ui/admin/key-dates/key-date-row-menu.tsx
@@ -2,7 +2,6 @@
import { useRef, useState } from "react";
import { EllipsisVertical, Pencil, Trash2 } from "lucide-react";
-import { toast } from "sonner";
import { Button } from "@coursemap/ui/primitives/button";
import {
DropdownMenu,
@@ -19,6 +18,7 @@ import type { UniversityCalendarReviewEvent } from "@/lib/coursemap/university-c
import { ConfirmDialog } from "@/ui/common/confirm-dialog";
import { KeyDateDialog } from "@/ui/admin/key-dates/key-date-dialog";
import { calendarDateLabel } from "@/ui/key-dates/category-badge";
+import { showToast } from "@/ui/common/toast";
/**
* Edit and remove for one row. A published date is changed directly; a date
@@ -46,7 +46,7 @@ export function KeyDateRowMenu({
? await reviseKeyDatesReviewAction(reviewId!, year, event, null)
: await removeKeyDateAction(year, event.eventId!);
if (!result.ok) throw new Error(result.message);
- toast.success(result.message);
+ showToast(result.message);
}
return (
diff --git a/apps/web/ui/admin/key-dates/key-dates-review.tsx b/apps/web/ui/admin/key-dates/key-dates-review.tsx
index ca6f4768..1ae7a214 100644
--- a/apps/web/ui/admin/key-dates/key-dates-review.tsx
+++ b/apps/web/ui/admin/key-dates/key-dates-review.tsx
@@ -9,7 +9,6 @@ import {
ExternalLink,
TriangleAlert,
} from "lucide-react";
-import { toast } from "sonner";
import {
Alert,
AlertDescription,
@@ -34,6 +33,7 @@ import type { ImportDiagnostic } from "@/lib/catalogue-import/import-source";
import { ConfirmDialog } from "@/ui/common/confirm-dialog";
import { OutlinedTabsList } from "@/ui/common/outlined-tabs-list";
import { KeyDatesMonthList } from "@/ui/admin/key-dates/key-dates-month-list";
+import { showToast } from "@/ui/common/toast";
const timestampFormat = new Intl.DateTimeFormat("en-AU", {
dateStyle: "medium",
@@ -120,14 +120,14 @@ export function KeyDatesReviewPanel({
async function approve() {
const result = await approveKeyDatesReviewAction(review.id, year);
if (!result.ok) throw new Error(result.message);
- toast.success(result.message);
+ showToast(result.message);
router.push(`/admin/key-dates/${year}`);
}
async function discard() {
const result = await discardKeyDatesReviewAction(review.id, year);
if (!result.ok) throw new Error(result.message);
- toast.success(result.message);
+ showToast(result.message);
}
return (
diff --git a/apps/web/ui/admin/key-dates/key-dates-sync-button.tsx b/apps/web/ui/admin/key-dates/key-dates-sync-button.tsx
index b7c93fac..0d67b3fd 100644
--- a/apps/web/ui/admin/key-dates/key-dates-sync-button.tsx
+++ b/apps/web/ui/admin/key-dates/key-dates-sync-button.tsx
@@ -3,10 +3,10 @@
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import { RefreshCw } from "lucide-react";
-import { toast } from "sonner";
import { Button } from "@coursemap/ui/primitives/button";
import { cn } from "@/lib/cn";
import { syncKeyDatesAction } from "@/lib/admin/key-dates-actions";
+import { showToast } from "@/ui/common/toast";
/** Fetches the year from the ANU calendar and stages it for review. */
export function KeyDatesSyncButton({
@@ -25,12 +25,12 @@ export function KeyDatesSyncButton({
startTransition(async () => {
try {
const result = await syncKeyDatesAction(year);
- if (result.ok) toast.success(result.message);
- else toast.error(result.message);
+ if (result.ok) showToast(result.message);
+ else showToast(result.message, "error");
if (result.staged)
router.push(`/admin/key-dates/${year}/sync`, { scroll: false });
} catch {
- toast.error(`The ${year} calendar could not be synced. Try again.`);
+ showToast(`Couldn't sync the ${year} calendar. Try again.`, "error");
}
});
}
diff --git a/apps/web/ui/admin/rooms/indoor-editor.tsx b/apps/web/ui/admin/rooms/indoor-editor.tsx
index 177914ef..d2fdbb4a 100644
--- a/apps/web/ui/admin/rooms/indoor-editor.tsx
+++ b/apps/web/ui/admin/rooms/indoor-editor.tsx
@@ -1,6 +1,5 @@
"use client";
import { buildingDrawingAngle } from "@/lib/rooms/indoor-orientation";
-import { toast } from "sonner";
import { useSearchParams } from "next/navigation";
import {
@@ -80,6 +79,7 @@ import {
type CampusMapData,
type CampusMapPlace,
} from "@/lib/rooms/campus-map";
+import { showToast } from "@/ui/common/toast";
/** Old links used `?tab=`; the 3D views map onto the perspective toggle. */
function perspectiveFromSearch(value: string | null) {
@@ -361,7 +361,7 @@ export function IndoorEditor({
status,
});
if (result.ok) {
- toast.success(result.message);
+ showToast(result.message);
setSavedRecord((current) => ({
...current,
name,
@@ -377,13 +377,14 @@ export function IndoorEditor({
sourceName,
});
} else {
- toast.error(result.message);
+ showToast(result.message, "error");
}
} catch (error) {
- toast.error(
+ showToast(
error instanceof Error
? error.message
- : "This floor plan could not be saved.",
+ : "Couldn't save this floor plan",
+ "error",
);
} finally {
setSavingStatus(null);
diff --git a/apps/web/ui/common/filter-bar.tsx b/apps/web/ui/common/filter-bar.tsx
index 58346258..5e17d686 100644
--- a/apps/web/ui/common/filter-bar.tsx
+++ b/apps/web/ui/common/filter-bar.tsx
@@ -64,9 +64,19 @@ export function FilterBar({
const urlQuery = searchParams.get("q") ?? "";
const [localQuery, setLocalQuery] = useState(urlQuery);
const [previousUrlQuery, setPreviousUrlQuery] = useState(urlQuery);
+ // Searches sent to the URL that have not landed yet. An earlier one landing
+ // while the reader keeps typing must not overwrite the box, so only a
+ // change from elsewhere (back, a clear link) resets the input.
+ const [sentQueries, setSentQueries] = useStateNothing to preview yet