From c5d1a2fdc530eeace4b41c48f1d8d4bd7f9ea8c2 Mon Sep 17 00:00:00 2001
From: Andrew January
Date: Sun, 19 Oct 2025 13:22:19 +0100
Subject: [PATCH] Add support for multiple maps
The config `map` property is now a `maps` property, and each has an id
and label. If there is more than one map, it will add a map select
component that allows the user to select the map.
The zoom and pan settings are now persisted per-map, which hopefully
makes it easier to switch back and forth between maps to understand how
to get from one room to another.
The urls now have the map in them. This means upgrading from an old map
will break all the old URLs. However, people don't tend to upgrade old
maps, so that's not really a big deal. Besides, the rooms will likely
have to be changed as a part of the upgrade anyway.
---
app/App.tsx | 72 ++-
app/InfoPanel.tsx | 17 +
app/Map.tsx | 48 +-
app/MapSelect.tsx | 36 ++
app/RoomSelect.tsx | 53 ++-
app/about/page.tsx | 25 +-
app/config.ts | 677 +++++++++++++++--------------
app/config.types.ts | 46 +-
app/map/[map]/page.tsx | 17 +
app/map/[map]/room/[room]/page.tsx | 20 +
app/room/[room]/page.tsx | 17 -
public/first.png | Bin 0 -> 74721 bytes
public/ground.png | Bin 0 -> 75416 bytes
public/map.png | Bin 152491 -> 0 bytes
14 files changed, 624 insertions(+), 404 deletions(-)
create mode 100644 app/MapSelect.tsx
create mode 100644 app/map/[map]/page.tsx
create mode 100644 app/map/[map]/room/[room]/page.tsx
delete mode 100644 app/room/[room]/page.tsx
create mode 100644 public/first.png
create mode 100644 public/ground.png
delete mode 100644 public/map.png
diff --git a/app/App.tsx b/app/App.tsx
index cd44f77..7db29cb 100644
--- a/app/App.tsx
+++ b/app/App.tsx
@@ -1,43 +1,73 @@
"use client";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import Map from "./Map";
import RoomSelect from "./RoomSelect";
+import MapSelect from "./MapSelect";
import InfoPanel from "./InfoPanel";
import config from "./config";
-import { Room } from "./config.types";
-import { set } from "ol/transform";
+import { Map as MapConfig, Room } from "./config.types";
-export default function App({ roomId }: { roomId?: string }) {
- const room = config.map.rooms.find((room) => room.id === roomId);
+export default function App({
+ mapId,
+ roomId,
+}: {
+ mapId?: string;
+ roomId?: string;
+}) {
+ const map = config.maps.find((m) => m.id === mapId) || config.maps[0];
+ const room = map.rooms.find((room) => room.id === roomId);
+ const [selectedMap, setSelectedMap] = useState(map);
const [selectedRoom, setSelectedRoom] = useState(room);
const [focusedRoom, setFocusedRoom] = useState(undefined);
- const [infoPanelExpanded, setInfoPanelExpanded] = useState(() => {
- return Boolean(
- typeof localStorage !== "undefined" &&
- localStorage.getItem("infoPanelExpanded"),
- );
- });
+ useEffect(() => {
+ if (!mapId) {
+ window.history.replaceState(null, "", `/map/${selectedMap.id}`);
+ }
+ }, [mapId, selectedMap.id]);
+
+ const [infoPanelExpanded, setInfoPanelExpanded] = useState(false);
+
+ useEffect(() => {
+ if (typeof localStorage !== "undefined") {
+ const stored = localStorage.getItem("infoPanelExpanded");
+ if (stored) {
+ setInfoPanelExpanded(true);
+ }
+ }
+ }, []);
- const onRoomSelected = (room?: Room) => {
+ const onRoomSelected = (map: MapConfig, room?: Room) => {
+ setSelectedMap(map);
if (!room) {
setSelectedRoom(undefined);
- window.history.replaceState(null, "", "/");
+ window.history.replaceState(null, "", `/map/${map.id}`);
} else {
- history.replaceState(null, "", `/room/${room.id}`);
+ history.replaceState(null, "", `/map/${map.id}/room/${room.id}`);
setSelectedRoom(room);
}
};
+ const onMapSelected = (map: MapConfig) => {
+ setSelectedMap(map);
+ onRoomSelected(map, undefined);
+ window.history.replaceState(null, "", `/map/${map.id}`);
+ };
+
const onRoomSelectedFromMap = (room?: Room) => {
setFocusedRoom(undefined);
- onRoomSelected(room);
+ onRoomSelected(selectedMap, room);
};
- const onRoomSelectedFromDropdown = (room?: Room) => {
+ const onRoomSelectedFromDropdown = (map: MapConfig, room?: Room) => {
setFocusedRoom(room);
- onRoomSelected(room);
+ onRoomSelected(map, room);
+ };
+
+ const onMapSelectedFromDropdown = (map: MapConfig) => {
+ setSelectedMap(map);
+ onMapSelected(map);
};
const onInfoPanelExpandChange = (expanded: boolean) => {
@@ -59,12 +89,20 @@ export default function App({ roomId }: { roomId?: string }) {
+ {config.maps.length > 1 && (
+
+ )}
{
+ // Use Next.js Link for internal links, regular for external
+ if (href && href.startsWith("/")) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+ },
}}
>
{dedent(room.description)}
diff --git a/app/Map.tsx b/app/Map.tsx
index f9819d5..dee00c1 100644
--- a/app/Map.tsx
+++ b/app/Map.tsx
@@ -15,10 +15,11 @@ import React, {
useRef,
useState,
} from "react";
-import { Room, Config } from "./config.types";
+import { Config, Map as MapConfig, Room } from "./config.types";
interface MapProps extends React.HTMLAttributes {
config: Config;
+ selectedMap: MapConfig;
selectedRoom?: Room;
focusedRoom?: Room;
onRoomSelected?: (room?: Room) => void;
@@ -35,6 +36,7 @@ async function decodeAllImages(src: string) {
export default function Map({
config,
+ selectedMap,
selectedRoom,
focusedRoom,
onRoomSelected,
@@ -85,7 +87,10 @@ export default function Map({
return;
}
- decodeAllImages(config.map.src).then((img) => {
+ let map: olMap | null = null;
+
+ // TODO: support multiple maps
+ decodeAllImages(selectedMap.src).then((img) => {
const width = img.naturalWidth;
const height = img.naturalHeight;
@@ -98,13 +103,13 @@ export default function Map({
const imageLayer = new ImageLayer({
source: new Static({
- url: config.map.src,
+ url: selectedMap.src,
projection: projection,
imageExtent: extent,
}),
});
- const markers = config.map.rooms.map((room) => {
+ const markers = selectedMap.rooms.map((room) => {
return new Feature({
geometry: new Polygon([
room.area.map((coords) => [coords[0], height - coords[1]]),
@@ -121,7 +126,7 @@ export default function Map({
style: unselectedStyle,
});
- const map = new olMap({
+ map = new olMap({
target: mapDiv,
interactions: defaults({
altShiftDragRotate: false,
@@ -135,15 +140,19 @@ export default function Map({
if (
typeof localStorage !== "undefined" &&
- localStorage.getItem("map-extent")
+ localStorage.getItem(`map-extent-${selectedMap.id}`)
) {
- map.getView().fit(JSON.parse(localStorage.getItem("map-extent")!));
+ map
+ .getView()
+ .fit(
+ JSON.parse(localStorage.getItem(`map-extent-${selectedMap.id}`)!),
+ );
} else {
map.getView().fit(extent);
}
map.on("pointermove", (e) => {
- const selectable = map.forEachFeatureAtPixel(e.pixel, (f) => f);
+ const selectable = map!.forEachFeatureAtPixel(e.pixel, (f) => f);
if (selectable) {
mapDiv.style.cursor = "pointer";
} else {
@@ -154,15 +163,24 @@ export default function Map({
map.on("moveend", (e) => {
typeof localStorage !== "undefined" &&
localStorage.setItem(
- "map-extent",
- JSON.stringify(map.getView().calculateExtent()),
+ `map-extent-${selectedMap.id}`,
+ JSON.stringify(map!.getView().calculateExtent()),
);
onPanRef.current && onPanRef.current();
});
setMap(map);
});
- }, [config.map, mapDiv, unselectedStyle]);
+
+ return function cleanup() {
+ if (map != null) {
+ map.setTarget(undefined);
+ map.dispose();
+ }
+ setMap(null);
+ setSelectedFeature(null);
+ };
+ }, [selectedMap, mapDiv, unselectedStyle]);
const onMapClick = useCallback(
(e: MapBrowserEvent) => {
@@ -207,17 +225,21 @@ export default function Map({
.getSource()!
.getFeatures()
.find(
- (f: { get: (arg0: string) => Room }) => f.get("room") === selectedRoom,
+ (f: { get: (arg0: string) => Room }) =>
+ f.get("room").id === selectedRoom?.id,
);
if (selected != null) {
setSelectedFeature(selected);
+ } else {
+ setSelectedFeature(null);
}
const focused = vectorLayer
.getSource()!
.getFeatures()
.find(
- (f: { get: (arg0: string) => Room }) => f.get("room") === focusedRoom,
+ (f: { get: (arg0: string) => Room }) =>
+ f.get("room").id === focusedRoom?.id,
);
if (focused != null) {
map.getView().fit(focused.getGeometry().getExtent(), {
diff --git a/app/MapSelect.tsx b/app/MapSelect.tsx
new file mode 100644
index 0000000..18ebd4a
--- /dev/null
+++ b/app/MapSelect.tsx
@@ -0,0 +1,36 @@
+import { Map, Config } from "./config.types";
+import { MapIcon } from "@heroicons/react/24/outline";
+
+export default function MapSelect({
+ config,
+ selectedMap,
+ onMapSelected,
+}: {
+ config: Config;
+ selectedMap: Map;
+ onMapSelected: (map: Map) => void;
+}) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/app/RoomSelect.tsx b/app/RoomSelect.tsx
index 7d955d1..649e086 100644
--- a/app/RoomSelect.tsx
+++ b/app/RoomSelect.tsx
@@ -1,43 +1,61 @@
-import React from "react";
+import { useState, useMemo, MouseEvent } from "react";
import {
ChevronLeftIcon,
MagnifyingGlassIcon,
} from "@heroicons/react/24/solid";
import Fuse from "fuse.js";
-import { Room, Config } from "./config.types";
+import { Map, Room, Config } from "./config.types";
import { useTranslations } from "next-intl";
interface RoomSelectProps {
config: Config;
- onRoomSelected?: (room: Room) => void;
+ onRoomSelected?: (map: Map, room: Room) => void;
}
+type RoomWithMap = Room & { map: Map; mapLabel: string };
+
export default function RoomSelect({
config,
onRoomSelected,
}: RoomSelectProps) {
const t = useTranslations("room-select");
- const [focused, setFocused] = React.useState(false);
+ const [focused, setFocused] = useState(false);
const onFocus = () => setFocused(true);
- const onDismiss = (e: React.MouseEvent) => {
+ const onDismiss = (e: MouseEvent) => {
if (e.target === e.currentTarget || e.currentTarget.tagName === "BUTTON") {
setFocused(false);
}
};
- const [query, setQuery] = React.useState("");
+ const [query, setQuery] = useState("");
- const onRoomClick = (room: Room) => {
+ const onRoomClick = (room: RoomWithMap) => {
setFocused(false);
setQuery("");
- onRoomSelected && onRoomSelected(room);
+ onRoomSelected && onRoomSelected(room.map, room);
};
+ const allRooms = useMemo(() => {
+ return config.maps
+ .flatMap((map) => {
+ return map.rooms.map((room) => ({
+ ...room,
+ map: map,
+ mapLabel: map.label,
+ }));
+ })
+ .sort((a, b) => a.label.localeCompare(b.label));
+ }, [config.maps]);
+
let results;
if (query === "") {
- results = config.map.rooms.sort((a, b) => a.label.localeCompare(b.label));
+ results = allRooms;
} else {
- const fuse = new Fuse(config.map.rooms, {
- keys: ["label", "aliases"],
+ let keys = ["label", "aliases"];
+ if (config.maps.length > 1) {
+ keys.push("mapLabel");
+ }
+ const fuse = new Fuse(allRooms, {
+ keys: keys,
ignoreLocation: true,
});
@@ -82,8 +100,15 @@ export default function RoomSelect({
className={`absolute top-14 right-0 bottom-0 left-0 overflow-y-auto px-4 py-2 ${focused ? "" : "hidden"}`}
>
{results.map((room, i) => {
+ let secondaryText = [];
+ if (config.maps.length > 1) {
+ secondaryText.push(room.mapLabel);
+ }
+ if (room.aliases && room.aliases.length > 0) {
+ secondaryText = secondaryText.concat(room.aliases);
+ }
return (
-
+
{room.label}
- {room.aliases && (
+ {secondaryText.length > 0 && (
- {room.aliases.join(", ")}
+ {secondaryText.join(", ")}
)}
diff --git a/app/about/page.tsx b/app/about/page.tsx
index e7fbac2..fcde99e 100644
--- a/app/about/page.tsx
+++ b/app/about/page.tsx
@@ -3,10 +3,29 @@ import config from "../config";
import Markdown from "react-markdown";
import { dedent } from "../text-utils";
import Link from "next/link";
+import type { Components } from "react-markdown";
export default function About() {
const t = useTranslations("about");
+ const markdownComponents: Components = {
+ a: ({ href, children, ...props }) => {
+ // Use Next.js Link for internal links, regular for external
+ if (href && href.startsWith("/")) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ };
+
let attributions = <>>;
if (config.attributions) {
attributions = (
@@ -15,7 +34,7 @@ export default function About() {
{config.attributions.map((attribution, i) => (
-
- {attribution}
+ {attribution}
))}
@@ -29,7 +48,9 @@ export default function About() {
← {t("back")}
{t("title")}
- {dedent(config.description)}
+
+ {dedent(config.description)}
+
{t.rich("powered-by", {
GitHubLink: () => (
diff --git a/app/config.ts b/app/config.ts
index 43f309c..d1cac65 100644
--- a/app/config.ts
+++ b/app/config.ts
@@ -26,46 +26,49 @@ const config: Config = {
accent: "rgb(8,114,50)",
disabled: "#cbd5e1",
},
- map: {
- src: "/map.png",
- rooms: [
- {
- id: "silent-room",
- label: "Silent room",
- aliases: ["Jackfield Boardroom"],
- description: `
+ maps: [
+ {
+ id: "ground",
+ label: "Ground floor",
+ src: "/ground.png",
+ rooms: [
+ {
+ id: "silent-room",
+ label: "Silent room",
+ aliases: ["Jackfield Boardroom"],
+ description: `
This is a room intended for sitting in silence, where no activities
are allowed (including reading or using your phone). This will be
available each day from 11am. Note, there may be noise leaking from
neighbouring rooms, and the aircon makes some noise, so do bring noise
cancelling headphones if you need complete silence.
`,
- area: [
- [202, 499],
- [255, 499],
- [255, 550],
- [202, 550],
- ],
- },
- {
- id: "pattingham",
- label: "Pattingham",
- aliases: ["Programme"],
- description: `
+ area: [
+ [373, 551],
+ [426, 551],
+ [426, 602],
+ [373, 602],
+ ],
+ },
+ {
+ id: "pattingham",
+ label: "Pattingham",
+ aliases: ["Programme"],
+ description: `
[Programme schedule](https://guide.example.co.uk/pattingham)
`,
- area: [
- [262, 470],
- [363, 470],
- [363, 550],
- [262, 550],
- ],
- },
- {
- id: "ops-help-desk",
- label: "Ops Help Desk",
- aliases: ["Beckbury 1", "Beckbury 2"],
- description: `
+ area: [
+ [433, 522],
+ [534, 522],
+ [534, 602],
+ [433, 602],
+ ],
+ },
+ {
+ id: "ops-help-desk",
+ label: "Ops Help Desk",
+ aliases: ["Beckbury 1", "Beckbury 2"],
+ description: `
If you need help with anything, this is the place to go. We can help
with lost property, lost people, and any other issues you might have.
This is also where the volunteers desk is.
@@ -80,121 +83,128 @@ const config: Config = {
Monday: 9:30am-11pm
`,
- area: [
- [270, 655],
- [363, 655],
- [363, 728],
- [270, 728],
- ],
- },
- {
- id: "newsletter",
- label: "Newsletter",
- aliases: ["Beckbury 3"],
- description: `
+ area: [
+ [441, 707],
+ [534, 707],
+ [534, 780],
+ [441, 780],
+ ],
+ },
+ {
+ id: "newsletter",
+ label: "Newsletter",
+ aliases: ["Beckbury 3"],
+ description: `
Office for the newsletter team. If you want to submit something to the
newsletter, email
[newsletter@example.org](mailto:newsletter@example.org).
`,
- area: [
- [371, 655],
- [414, 655],
- [414, 728],
- [371, 728],
- ],
- },
- {
- id: "quiet-activities",
- label: "Quiet activities",
- aliases: ["Beckbury 4"],
- description: `
+ area: [
+ [542, 707],
+ [585, 707],
+ [585, 780],
+ [542, 780],
+ ],
+ },
+ {
+ id: "quiet-activities",
+ label: "Quiet activities",
+ aliases: ["Beckbury 4"],
+ description: `
A place where you may do a jigsaw or read a book/electronic item, but
may not make phone calls.
`,
- area: [
- [422, 655],
- [462, 655],
- [462, 728],
- [422, 728],
- ],
- },
- {
- id: "games",
- label: "Games",
- aliases: ["Ryton"],
- description: `
+ area: [
+ [593, 707],
+ [633, 707],
+ [633, 780],
+ [593, 780],
+ ],
+ },
+ {
+ id: "games",
+ label: "Games",
+ aliases: ["Ryton"],
+ description: `
Play games with friends, or make new friends by joining a game. There
will be a selection of games available, or you can bring your own.
`,
- area: [
- [884, 500],
- [942, 500],
- [942, 550],
- [884, 550],
- ],
- },
- {
- id: "toilets-ground-floor",
- label: "Toilets (ground floor)",
- aliases: [
- "Male toilets",
- "Female toilets",
- "Accessible toilets",
- "Disabled toilets",
- ],
- description: `
+ area: [
+ [1055, 552],
+ [1113, 552],
+ [1113, 602],
+ [1055, 602],
+ ],
+ },
+ {
+ id: "toilets-ground-floor",
+ label: "Toilets (ground floor)",
+ aliases: [
+ "Male toilets",
+ "Female toilets",
+ "Accessible toilets",
+ "Disabled toilets",
+ ],
+ description: `
Gender neutral toilets are available on the first floor.
`,
- area: [
- [852, 344],
- [1055, 344],
- [1055, 382],
- [852, 382],
- ],
- },
- {
- id: "stairs-ground",
- label: "Stairs (ground floor)",
- area: [
- [311, 585],
- [484, 585],
- [484, 615],
- [311, 615],
- ],
- },
- {
- id: "lift-small-ground",
- label: "Lift (small, ground floor)",
- description: `
+ area: [
+ [1023, 396],
+ [1226, 396],
+ [1226, 434],
+ [1023, 434],
+ ],
+ },
+ {
+ id: "stairs-ground",
+ label: "Stairs (ground floor)",
+ description: `
+ Connects to [first floor](/map/first/room/stairs-first).
+ `,
+ area: [
+ [482, 637],
+ [655, 637],
+ [655, 667],
+ [482, 667],
+ ],
+ },
+ {
+ id: "lift-small-ground",
+ label: "Lift (small, ground floor)",
+ description: `
This lift is small and can only fit one wheelchair user at a time.
If you are able to, please take the stairs. If you need a larger lift,
please use the lift near the E4 entrance.
+
+ Connects to [first floor](/map/first/room/lift-small-first).
`,
- area: [
- [590, 585],
- [621, 585],
- [621, 615],
- [590, 615],
- ],
- },
- {
- id: "lift-large-ground",
- label: "Lift (large, ground floor)",
- description: `
+ area: [
+ [761, 637],
+ [792, 637],
+ [792, 667],
+ [761, 667],
+ ],
+ },
+ {
+ id: "lift-large-ground",
+ label: "Lift (large, ground floor)",
+ description: `
You will need to ask a member of staff to call the lift for you.
+
+ Connects to [first floor](/map/first/room/lift-large-first).
`,
- area: [
- [1123, 149],
- [1193, 149],
- [1193, 209],
- [1123, 209],
- ],
- },
- {
- id: "e1",
- label: "E1",
- aliases: ["Entrance 1", "Car park"],
- description: `
+ area: [
+ [1294, 201],
+ [1364, 201],
+ [1364, 261],
+ [1294, 261],
+ ],
+ },
+ {
+ id: "e1",
+ label: "E1",
+ aliases: ["Entrance 1", "Car park"],
+ description: `
# Opening times
Friday: 9:30am - 11:00pm
@@ -205,18 +215,18 @@ const config: Config = {
Monday: 9:30am-11pm
`,
- area: [
- [1008, 690],
- [1170, 690],
- [1170, 808],
- [1008, 808],
- ],
- },
- {
- id: "e2",
- label: "E2",
- aliases: ["Entrance 2"],
- description: `
+ area: [
+ [1179, 742],
+ [1341, 742],
+ [1341, 860],
+ [1179, 860],
+ ],
+ },
+ {
+ id: "e2",
+ label: "E2",
+ aliases: ["Entrance 2"],
+ description: `
# Opening times
Friday: 9:30am - 11:00pm
@@ -227,29 +237,29 @@ const config: Config = {
Monday: 9:30am-11pm
`,
- area: [
- [470, 654],
- [632, 654],
- [632, 808],
- [470, 808],
- ],
- },
- {
- id: "e3",
- label: "E3",
- aliases: ["Entrance 3"],
- area: [
- [44, 706],
- [233, 655],
- [233, 808],
- [44, 808],
- ],
- },
- {
- id: "e4",
- label: "E4",
- aliases: ["Entrance 4", "Hotels"],
- description: `
+ area: [
+ [641, 706],
+ [803, 706],
+ [803, 860],
+ [641, 860],
+ ],
+ },
+ {
+ id: "e3",
+ label: "E3",
+ aliases: ["Entrance 3"],
+ area: [
+ [215, 758],
+ [404, 707],
+ [404, 860],
+ [215, 860],
+ ],
+ },
+ {
+ id: "e4",
+ label: "E4",
+ aliases: ["Entrance 4", "Hotels"],
+ description: `
# Opening times
Friday: 9:30am - 11:00pm
@@ -260,32 +270,39 @@ const config: Config = {
Monday: 9:30am-11pm
`,
- area: [
- [1008, 35],
- [1170, 35],
- [1170, 150],
- [1008, 150],
- ],
- },
- {
- id: "ironbridge",
- label: "Ironbridge",
- aliases: ["Programme"],
- description: `
+ area: [
+ [1179, 87],
+ [1341, 87],
+ [1341, 202],
+ [1179, 202],
+ ],
+ },
+ ],
+ },
+ {
+ id: "first",
+ label: "First floor",
+ src: "/first.png",
+ rooms: [
+ {
+ id: "ironbridge",
+ label: "Ironbridge",
+ aliases: ["Programme"],
+ description: `
[Programme schedule](https://guide.example.co.uk/ironbridge)
`,
- area: [
- [1321, 363],
- [1457, 363],
- [1457, 613],
- [1332, 647],
- [1321, 565],
- ],
- },
- {
- id: "dealers",
- label: "Dealers & Fan Tables",
- description: `
+ area: [
+ [86, 413],
+ [222, 413],
+ [222, 663],
+ [97, 697],
+ [86, 615],
+ ],
+ },
+ {
+ id: "dealers",
+ label: "Dealers & Fan Tables",
+ description: `
A selection of dealers and fan tables will be available for you to
browse. Please note that some dealers may only accept cash.
@@ -299,33 +316,33 @@ const config: Config = {
Monday: 10am-2pm
`,
- area: [
- [1465, 363],
- [1576, 363],
- [1576, 426],
- [1608, 426],
- [1608, 544],
- [1465, 581],
- ],
- },
- {
- id: "childcare",
- label: "Childcare",
- description: `
+ area: [
+ [230, 413],
+ [341, 413],
+ [341, 476],
+ [373, 476],
+ [373, 594],
+ [230, 631],
+ ],
+ },
+ {
+ id: "childcare",
+ label: "Childcare",
+ description: `
Childcare must have been pre-booked. If you have not pre-booked, you
will not be able to use this service.
`,
- area: [
- [1576, 363],
- [1576, 426],
- [1639, 426],
- [1639, 363],
- ],
- },
- {
- id: "art-show",
- label: "Art show",
- description: `
+ area: [
+ [341, 413],
+ [341, 476],
+ [404, 476],
+ [404, 413],
+ ],
+ },
+ {
+ id: "art-show",
+ label: "Art show",
+ description: `
The art show will be open from 10am-6pm each day. Please note that
some pieces may be for sale.
@@ -337,143 +354,151 @@ const config: Config = {
Sunday: 10am-5pm (collection of purchased art, 4pm-6:30pm)
`,
- area: [
- [1639, 363],
- [1752, 363],
- [1752, 505],
- [1608, 544],
- [1608, 426],
- [1639, 426],
- ],
- },
- {
- id: "toilets-first-floor",
- label: "Toilets (first floor)",
- aliases: ["Urinals", "Without urinals", "Accessible"],
- description: `
+ area: [
+ [404, 413],
+ [517, 413],
+ [517, 555],
+ [373, 594],
+ [373, 476],
+ [404, 476],
+ ],
+ },
+ {
+ id: "toilets-first-floor",
+ label: "Toilets (first floor)",
+ aliases: ["Urinals", "Without urinals", "Accessible"],
+ description: `
Gender neutral toilets with and without urinals.
`,
- area: [
- [1760, 434],
- [1867, 434],
- [1867, 504],
- [1760, 504],
- ],
- },
- {
- id: "green-room",
- label: "Green room",
- description: `
+ area: [
+ [525, 484],
+ [632, 484],
+ [632, 554],
+ [525, 554],
+ ],
+ },
+ {
+ id: "green-room",
+ label: "Green room",
+ description: `
Programme participants should arrive at the green room 15 minutes
before their item starts. Here you will meet with your fellow
panellists (both in-person and online) and discuss the item before it
starts. You will also be offered a complementary drink.
`,
- area: [
- [1584, 674],
- [1677, 674],
- [1677, 747],
- [1584, 747],
- ],
- },
- {
- id: "Wenlock",
- label: "Wenlock",
- aliases: ["Programme"],
- description: `
+ area: [
+ [349, 724],
+ [442, 724],
+ [442, 797],
+ [349, 797],
+ ],
+ },
+ {
+ id: "Wenlock",
+ label: "Wenlock",
+ aliases: ["Programme"],
+ description: `
[Programme schedule](https://guide.example.co.uk/wenlock)
`,
- area: [
- [1685, 674],
- [1776, 674],
- [1776, 747],
- [1685, 747],
- ],
- },
- {
- id: "stairs-first",
- label: "Stairs (first floor)",
- area: [
- [1754, 588],
- [1933, 588],
- [1933, 618],
- [1754, 618],
- ],
- },
- {
- id: "lift-small-first",
- label: "Lift (small, first floor)",
- description: `
+ area: [
+ [450, 724],
+ [541, 724],
+ [541, 797],
+ [450, 797],
+ ],
+ },
+ {
+ id: "stairs-first",
+ label: "Stairs (first floor)",
+ description: `
+ Connects to [ground floor](/map/ground/room/stairs-ground).
+ `,
+ area: [
+ [519, 638],
+ [698, 638],
+ [698, 668],
+ [519, 668],
+ ],
+ },
+ {
+ id: "lift-small-first",
+ label: "Lift (small, first floor)",
+ description: `
This lift is small and can only fit one wheelchair user at a time. If
you are able to, please take the stairs. If you need a larger lift,
please use the lift near the E4 entrance.
+
+ Connects to [ground floor](/map/ground/room/lift-small-ground).
`,
- area: [
- [1993, 602],
- [2024, 593],
- [2024, 623],
- [1993, 623],
- ],
- },
- {
- id: "coalport",
- label: "Coalport",
- aliases: ["Programme"],
- description: `
+ area: [
+ [758, 652],
+ [789, 643],
+ [789, 673],
+ [758, 673],
+ ],
+ },
+ {
+ id: "coalport",
+ label: "Coalport",
+ aliases: ["Programme"],
+ description: `
[Programme schedule](https://guide.example.co.uk/coalport)
`,
- area: [
- [1875, 434],
- [1977, 434],
- [1977, 504],
- [1875, 504],
- ],
- },
- {
- id: "gallary",
- label: "Gallary",
- aliases: ["Food & Drink", "Social space"],
- description: `
+ area: [
+ [640, 484],
+ [742, 484],
+ [742, 554],
+ [640, 554],
+ ],
+ },
+ {
+ id: "gallary",
+ label: "Gallary",
+ aliases: ["Food & Drink", "Social space"],
+ description: `
A place to sit and chat with friends, or to grab a bite to eat. There
will be a selection of food and drink available, including vegan and
gluten free options.
`,
- area: [
- [2029, 363],
- [2288, 363],
- [2288, 499],
- [2029, 499],
- ],
- },
- {
- id: "atcham",
- label: "Atcham",
- aliases: ["Programme"],
- description: `
+ area: [
+ [794, 413],
+ [1053, 413],
+ [1053, 549],
+ [794, 549],
+ ],
+ },
+ {
+ id: "atcham",
+ label: "Atcham",
+ aliases: ["Programme"],
+ description: `
[Programme schedule](https://guide.example.co.uk/atcham)
`,
- area: [
- [2296, 363],
- [2397, 363],
- [2397, 493],
- [2296, 493],
- ],
- },
- {
- id: "lift-large-first",
- label: "Lift (large, first floor)",
- description: `
+ area: [
+ [1061, 413],
+ [1162, 413],
+ [1162, 543],
+ [1061, 543],
+ ],
+ },
+ {
+ id: "lift-large-first",
+ label: "Lift (large, first floor)",
+ description: `
You will need to ask a member of staff to call the lift for you.
+
+ Connects to [ground floor](/map/ground/room/lift-large-ground).
`,
- area: [
- [2405, 320],
- [2468, 320],
- [2468, 369],
- [2405, 369],
- ],
- },
- ],
- },
+ area: [
+ [1170, 370],
+ [1233, 370],
+ [1233, 419],
+ [1170, 419],
+ ],
+ },
+ ],
+ },
+ ],
};
export default config;
diff --git a/app/config.types.ts b/app/config.types.ts
index 488697b..2a26dd1 100644
--- a/app/config.types.ts
+++ b/app/config.types.ts
@@ -1,4 +1,4 @@
-export interface Config {
+export type Config = {
/**
* Name of the event. Displayed in titles and about pages.
*/
@@ -66,23 +66,39 @@ export interface Config {
};
/**
- * The map to display.
+ * Maps to use for the event.
+ * If there is more than one map, a floor selector will be shown.
+ * The first map in the list is the default map.
*/
- map: {
- /**
- * Path of the map image.
- * Should be as high resolution as possible.
- */
- src: string;
- rooms: Room[];
- };
-}
+ maps: Map[];
+};
+
+export type Map = {
+ /**
+ * Unique identifier for the map. Appears in the URL.
+ */
+ id: string;
+
+ /**
+ * Label for the map. Appears in the floor selector if there are multiple maps.
+ */
+ label: string;
+
+ /**
+ * Path of the map image.
+ * Should be as high resolution as possible.
+ */
+ src: string;
-export interface Map {}
+ /**
+ * List of rooms on the map.
+ */
+ rooms: Room[];
+};
-export interface Room {
+export type Room = {
/**
- * Unique identififer for the room. Appears in the URL.
+ * Unique identifier for the room. Appears in the URL.
*/
id: string;
@@ -115,4 +131,4 @@ export interface Room {
* In the image co-oordinates; the origin is the top left corner of the image.
*/
area: [number, number][];
-}
+};
diff --git a/app/map/[map]/page.tsx b/app/map/[map]/page.tsx
new file mode 100644
index 0000000..7974c9d
--- /dev/null
+++ b/app/map/[map]/page.tsx
@@ -0,0 +1,17 @@
+import App from "../../App";
+import config from "../../config";
+
+export function generateStaticParams() {
+ return config.maps.map((map) => {
+ return { map: map.id };
+ });
+}
+
+export default async function Map({
+ params,
+}: {
+ params: Promise<{ map: string }>;
+}) {
+ const { map } = await params;
+ return ;
+}
diff --git a/app/map/[map]/room/[room]/page.tsx b/app/map/[map]/room/[room]/page.tsx
new file mode 100644
index 0000000..fbbdf63
--- /dev/null
+++ b/app/map/[map]/room/[room]/page.tsx
@@ -0,0 +1,20 @@
+import App from "../../../../App";
+import config from "../../../../config";
+
+export async function generateStaticParams() {
+ return config.maps.flatMap((map) =>
+ map.rooms.map((room) => ({
+ map: map.id,
+ room: room.id,
+ })),
+ );
+}
+
+export default async function Room({
+ params,
+}: {
+ params: Promise<{ map: string; room: string }>;
+}) {
+ const { map, room } = await params;
+ return ;
+}
diff --git a/app/room/[room]/page.tsx b/app/room/[room]/page.tsx
deleted file mode 100644
index a6c24e8..0000000
--- a/app/room/[room]/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import App from "../../App";
-import config from "../../config";
-
-export function generateStaticParams() {
- return config.map.rooms.map((room) => {
- return { room: room.id };
- });
-}
-
-export default async function Room({
- params,
-}: {
- params: Promise<{ room: string }>;
-}) {
- const { room } = await params;
- return ;
-}
diff --git a/public/first.png b/public/first.png
new file mode 100644
index 0000000000000000000000000000000000000000..bdcab3d656d87eede0fc05695717c259e4358e88
GIT binary patch
literal 74721
zcmeFZc{r7A7dE~PArzv_2}K!0NHTAdp(r79h%(QFZK%|isR)@e6eUy0%%(CV^E}%^
zGEbSuZ{2#{=Y8I9_#NN(=kGY)eH;yL_i)|UwbnY%b*^>oK#glxj*v5vqfn?LSC!E>
zP^d!~6pE;jj0BE+qP`FU{}5Zssmq~IxnUGLcMieN1kN|E$P@I{^ZCLLr_7XZsH0Gx
zTqu;^V-#u&4*5-@P_E}usA&@v>e4F|iryi