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 0000000..bdcab3d Binary files /dev/null and b/public/first.png differ diff --git a/public/ground.png b/public/ground.png new file mode 100644 index 0000000..8694f54 Binary files /dev/null and b/public/ground.png differ diff --git a/public/map.png b/public/map.png deleted file mode 100644 index 95ff0d2..0000000 Binary files a/public/map.png and /dev/null differ