diff --git a/assets/scripts/KaplayMap/events.js b/assets/scripts/KaplayMap/events.js index 0ae7bcc..4736330 100644 --- a/assets/scripts/KaplayMap/events.js +++ b/assets/scripts/KaplayMap/events.js @@ -5,6 +5,19 @@ export class EventManager { constructor() {} + async getAllEvents() { + // DO NOT use this function for general use - may overload the server(s). get ONCE and then cache! + const currentLocalization = this.mainmanager.getCurrentLangCode(); + + let allEventsReqSend = fetch(`/data/${currentLocalization}/events/`); + + let allEventsReq = await allEventsReqSend; + let allEvents; + if (allEventsReq.ok) allEvents = await allEventsReq.json(); + + return allEvents; + } + async getEvents() { const currentLocalization = this.mainmanager.getCurrentLangCode(); const currentFloor = this.mainmanager.getCurrentFocus("floor"); diff --git a/assets/scripts/KaplayMap/gameobj.js b/assets/scripts/KaplayMap/gameobj.js index b175562..e6038f7 100644 --- a/assets/scripts/KaplayMap/gameobj.js +++ b/assets/scripts/KaplayMap/gameobj.js @@ -12,6 +12,8 @@ export class GameObjManager { font: "monospace", fontSize: 24, maxFontWidth: 16, + floorOpacity: 0.75, + roomOpacity: 0.75, }; mainmanager; @@ -38,8 +40,8 @@ export class GameObjManager { this.floorObject = this.map.kp.make([ this.map.kp.polygon(polygon.pts), - this.map.kp.outline(1, this.map.kp.BLACK), this.map.kp.color(this.map.kp.Color.fromHex("303030")), + this.map.kp.opacity(this.opts.floorOpacity), this.map.kp.pos(), this.map.kp.z(5), ]); @@ -48,13 +50,19 @@ export class GameObjManager { this.map.camBounds = bounds; - this.floorObject.onUpdate(() => { - const camScale = 1 / this.map.kp.camScale().y; - this.floorObject.outline.width = 8 * camScale; - }); - this.floorObject.onDraw(() => { const camScale = 1 / this.map.kp.camScale().y; + + this.map.kp.drawPolygon({ + pts: polygon.pts, + pos: this.map.kp.vec2(0), + fill: false, + outline: { + color: this.map.kp.Color.fromHex(this.opts.borderColor), + width: 8 * camScale, + }, + }); + this.map.kp.drawText({ text: currentFloor.name, size: 24 * camScale, @@ -120,7 +128,7 @@ export class GameObjManager { const obj = this.map.kp.make([ this.map.kp.polygon(polygon.pts), this.map.kp.color(this.map.kp.Color.fromHex(this.opts.bgColor)), - this.map.kp.opacity(0.5), + this.map.kp.opacity(this.opts.roomOpacity), this.map.kp.area(), this.map.kp.pos(), this.map.kp.z(6), @@ -133,8 +141,6 @@ export class GameObjManager { this.roomObjects.set(room.id, obj); obj.onUpdate(() => { - const camScale = 1 / this.map.kp.camScale().y; - const roomFocused = this.mainmanager.getCurrentFocus("room") == room.id; if (roomFocused) { diff --git a/assets/scripts/KaplayMap/localization.js b/assets/scripts/KaplayMap/localization.js index d951ef5..dc079e9 100644 --- a/assets/scripts/KaplayMap/localization.js +++ b/assets/scripts/KaplayMap/localization.js @@ -6,7 +6,10 @@ export default new Map([ date_starting: "Starting", date_started: "Started", date_summary: (prefix, time) => `${prefix} ${time}`, + list_index: (index) => `, ${index}`, parenthesis: (content) => `(${content})`, + openParenthesis: "(", + closeParenthesis: ")", separator: (p1, p2) => `${p1} - ${p2}`, hours_long: (hours) => hours == 1 ? `${hours} hour long` : `${hours} hours long`, @@ -19,6 +22,22 @@ export default new Map([ event_inspector_header: (name) => `Event ${name}`, event_inspector_back: "Return to events panel", event_inspector_minimize: "Toggle event inspector docking", + zoom_in_button: "Zoom in", + zoom_out_button: "Zoom out", + menu_search_button: "Search events", + menu_language_button: "Change language", + menu_about_button: "About EventMapper", + search_dialog_close_button: "Close search", + search_dialog_search_button: "Run search", + search_dialog_header_title: "Search Events", + search_filters: { + name: ({ src }) => `Found by title`, + description: ({ src }) => `Found by description`, + url: ({ src }) => `Found by URL ${src}`, + floor: ({ src }) => `Found by floor ${src}`, + room: ({ src }) => `Found by room ${src}`, + }, + regexAnyWordCharacter: /[^\w]/g, }, ], ]); diff --git a/assets/scripts/KaplayMap/mapper.js b/assets/scripts/KaplayMap/mapper.js index 9b85ccc..d578bfd 100644 --- a/assets/scripts/KaplayMap/mapper.js +++ b/assets/scripts/KaplayMap/mapper.js @@ -2,6 +2,7 @@ import { EventManager } from "./events.js"; import { FloorManager } from "./floors.js"; import { GameObjManager } from "./gameobj.js"; import { LangManager } from "./lang.js"; +import { TagManager } from "./tags.js"; import { UIManager } from "./ui.js"; export class EventMapperManager { @@ -32,6 +33,9 @@ export class EventMapperManager { this.floormanager = new FloorManager(); this.floormanager.mainmanager = this; + this.tagmanager = new TagManager(); + this.tagmanager.mainmanager = this; + this.uimanager = new UIManager(mapUi); this.uimanager.mainmanager = this; @@ -54,6 +58,14 @@ export class EventMapperManager { this.uimanager.setLoading(true); + await this.tagmanager.getTags(); + + this.uimanager.__updateSearchUI(); + + this.uimanager.setLoading(false); + + this.uimanager.setLoading(true); + window.addEventListener("hashchange", () => this.hashchange()); await this.hashchange(); @@ -113,6 +125,8 @@ export class EventMapperManager { if (!this.uimanager.getEventsEmpty()) this.uimanager.setEventsMinimized(false); + // this.uimanager.setSearchDialogOpen(false); + await this.uimanager.__updateSearch(); this.uimanager.setLoading(false); } @@ -127,6 +141,9 @@ export class EventMapperManager { if (!this.uimanager.getEventsEmpty()) this.uimanager.setEventsMinimized(false); + if (id != null) { + this.uimanager.setSearchDialogOpen(false); + } this.uimanager.setEventsInspector(false); } @@ -137,6 +154,7 @@ export class EventMapperManager { this.gameobjmanager.zoomToRoom(this.convertIdFocus(id, "room")); this.uimanager.updateInspector(); + this.uimanager.setSearchDialogOpen(false); } this.uimanager.setEventsInspector(id != null); @@ -294,6 +312,8 @@ export class EventMapperManager { return this.eventmanager[func](...args); case "floor": return this.floormanager[func](...args); + case "tag": + return this.tagmanager[func](...args); case "ui": return this.uimanager[func](...args); case "gameobj": @@ -311,6 +331,8 @@ export class EventMapperManager { return this.eventmanager[variable]; case "floor": return this.floormanager[variable]; + case "tag": + return this.tagmanager[variable]; case "ui": return this.uimanager[variable]; case "gameobj": diff --git a/assets/scripts/KaplayMap/tags.js b/assets/scripts/KaplayMap/tags.js new file mode 100644 index 0000000..2b0088e --- /dev/null +++ b/assets/scripts/KaplayMap/tags.js @@ -0,0 +1,25 @@ +export class TagManager { + mainmanager; + + #tags = new Map([]); + + constructor() {} + + async getTags() { + const currentLocalization = this.mainmanager.getCurrentLangCode(); + + let allTagsReqSend = fetch(`/data/${currentLocalization}/tags`); + + let allTagsReq = await allTagsReqSend; + let allTags; + if (allTagsReq.ok) allTags = await allTagsReq.json(); + + this.#tags.clear(); + + allTags.forEach((tag) => this.#tags.set(tag.id, tag)); + } + + get allTags() { + return this.#tags; + } +} diff --git a/assets/scripts/KaplayMap/ui.js b/assets/scripts/KaplayMap/ui.js index 26151d4..b0b3c2c 100644 --- a/assets/scripts/KaplayMap/ui.js +++ b/assets/scripts/KaplayMap/ui.js @@ -20,6 +20,10 @@ export class UIManager { this.__initZoom(); + this.__initMenu(); + + this.__initSearchUI(); + this.__initEvents(); this.__initInspector(); @@ -61,8 +65,10 @@ export class UIManager { setEventsInspector(state) { if ( state != this.uiElements.eventsContainer.classList.contains("inspector") - ) + ) { this.setEventsMinimized(false); + this.setSearchDialogOpen(false); + } if (state) { this.uiElements.eventsContainer.classList.add("inspector"); } else { @@ -70,6 +76,15 @@ export class UIManager { } } + setSearchDialogOpen(state) { + if (state) { + this.uiElements.searchDialogScrim.classList.add("open"); + this.__updateSearch(); + } else { + this.uiElements.searchDialogScrim.classList.remove("open"); + } + } + getEventsEmpty() { return this.uiElements.events.classList.contains("empty"); } @@ -77,6 +92,8 @@ export class UIManager { // Init __initUIElements() { + this.uiElements.controls = this.ui.querySelector("#controls"); + // Zoom this.uiElements.zoomButtons = this.ui.querySelector("#zoom"); this.uiElements.zoomIn = @@ -89,6 +106,12 @@ export class UIManager { this.uiElements.floorButtons = this.uiElements.floors.querySelector("#floor-buttons"); + // Menu Bar + this.uiElements.menuBar = this.ui.querySelector("#menu-bar"); + this.uiElements.menuBarSearch = this.ui.querySelector("#menu-search"); + this.uiElements.menuBarLang = this.ui.querySelector("#menu-lang"); + this.uiElements.menuBarAbout = this.ui.querySelector("#menu-about"); + // Events this.uiElements.eventsContainer = this.ui.querySelector("#events-container"); @@ -126,11 +149,34 @@ export class UIManager { this.uiElements.eventsInspectorMinimizeButton = this.uiElements.eventsInspector.querySelector("#minimize"); + // Search Dialog + this.uiElements.searchDialogScrim = this.ui.querySelector( + "#search-dialog-scrim" + ); + this.uiElements.searchDialog = + this.uiElements.searchDialogScrim.querySelector("#search-dialog"); + this.uiElements.searchDialogHeader = + this.uiElements.searchDialog.querySelector("#search-dialog-header"); + this.uiElements.searchDialogHeaderClose = + this.uiElements.searchDialogHeader.querySelector("#close"); + this.uiElements.searchDialogHeaderTitle = + this.uiElements.searchDialogHeader.querySelector("#search-dialog-title"); + this.uiElements.searchDialogBar = + this.uiElements.searchDialog.querySelector("#search-dialog-bar"); + this.uiElements.searchDialogBarInput = + this.uiElements.searchDialogBar.querySelector("#search-dialog-input"); + this.uiElements.searchDialogTags = + this.uiElements.searchDialog.querySelector("#search-dialog-tags"); + this.uiElements.searchDialogList = + this.uiElements.searchDialog.querySelector("#search-list"); + // Loading this.uiElements.loading = this.ui.querySelector("#loading"); } __initZoom() { + const currentLocalization = this.mainmanager.getCurrentLocalization(); + this.uiElements.zoomIn.addEventListener("click", () => this.mainmanager.callManagerFunction("map", "zoomIn") ); @@ -146,6 +192,320 @@ export class UIManager { this.mainmanager.addManagerEventListener("map", "zoom", () => { this.setZoomDisabled(0); }); + + this.uiElements.zoomIn.title = currentLocalization.zoom_in_button; + this.uiElements.zoomOut.title = currentLocalization.zoom_out_button; + } + + __initMenu() { + this.uiElements.menuBarSearch.addEventListener("click", () => { + this.setSearchDialogOpen(true); + }); + + this.__updateMenu(); + } + + __updateMenu() { + const currentLocalization = this.mainmanager.getCurrentLocalization(); + + this.uiElements.menuBarSearch.title = + currentLocalization.menu_search_button; + this.uiElements.menuBarLang.title = + currentLocalization.menu_language_button; + this.uiElements.menuBarAbout.title = currentLocalization.menu_about_button; + } + + __initSearchUI() { + this.uiElements.searchDialogHeaderClose.addEventListener("click", () => { + this.setSearchDialogOpen(false); + }); + this.uiElements.searchDialogScrim.addEventListener("click", () => { + this.setSearchDialogOpen(false); + }); + this.uiElements.searchDialog.addEventListener("click", (event) => { + event.stopPropagation(); + }); + this.uiElements.searchDialogBarInput.addEventListener("input", () => { + this.__updateSearch(); + }); + } + + #Search__selectedTags = []; + #Search__allEvents = null; + + __updateSearchUI() { + const currentLocalization = this.mainmanager.getCurrentLocalization(); + + this.uiElements.searchDialogHeaderClose.title = + currentLocalization.search_dialog_close_button; + this.uiElements.searchDialogHeaderTitle.textContent = + currentLocalization.search_dialog_header_title; + + const tags = this.mainmanager.getManagerVariable("tag", "allTags"); + + this.uiElements.searchDialogTags.replaceChildren(); + + tags.forEach((value, id) => { + const tagButton = document.createElement("button"); + tagButton.classList.add("search-tag"); + tagButton.textContent = value.name; + tagButton.title = value.description; + tagButton.id = "tag-" + id; + tagButton.addEventListener("click", () => this.__updateTagState(id)); + + this.uiElements.searchDialogTags.appendChild(tagButton); + }); + } + + __updateTagState(tag) { + const tags = this.mainmanager.getManagerVariable("tag", "allTags"); + + const tagPos = this.#Search__selectedTags.indexOf(tag); + if (tagPos >= 0) this.#Search__selectedTags.splice(tagPos, 1); + else this.#Search__selectedTags.push(tag); + + this.#Search__selectedTags = this.#Search__selectedTags.filter( + (val) => + val == tag || tags.get(val).radiogroup != tags.get(tag).radiogroup + ); + + this.__updateSearchUISoft(); + this.__updateSearch(); + } + + __updateSearchUISoft() { + const tags = this.mainmanager.getManagerVariable("tag", "allTags"); + + tags.forEach((_, id) => { + const child = this.uiElements.searchDialogTags.querySelector( + "#tag-" + id + ); + if (this.#Search__selectedTags.includes(id)) + child.classList.add("selected"); + else child.classList.remove("selected"); + }); + } + + async __initSearch() { + // Runs on first search, as to not make the client wait too long when starting EventMapper (loading the same thing twice) + this.setLoading(true); + this.#Search__allEvents = await this.mainmanager.callManagerFunction( + "event", + "getAllEvents" + ); + this.setLoading(false); + } + + async __updateSearch() { + const currentLocalization = this.mainmanager.getCurrentLocalization(); + if (this.#Search__allEvents == null) await this.__initSearch(); + + const searchTransform = (x) => + x.toLowerCase().replace(currentLocalization.regexAnyWordCharacter, ""); + + const searchValue = searchTransform( + this.uiElements.searchDialogBarInput.value + ); + + let whatFilter = []; + let filteredEvents = this.#Search__allEvents.filter((event) => { + if (searchValue.length < 1) { + whatFilter.push(null); + return true; + } + // First, filter by text + if (searchTransform(event.name).includes(searchValue)) { + whatFilter.push({ + type: "name", + src: event.name, + }); + return true; + } else if (searchTransform(event.description).includes(searchValue)) { + whatFilter.push({ + type: "description", + src: event.description, + }); + return true; + } else if (searchTransform(event.url).includes(searchValue)) { + whatFilter.push({ + type: "url", + src: event.url, + }); + return true; + } else { + // Filter by room/floor + const eventFloorId = this.mainmanager.convertIdFocus(event.id, "floor"); + const eventFloor = this.mainmanager + .getAllFocusObject("floor") + .get(eventFloorId); + + if (eventFloor != null) { + if (searchTransform(eventFloor.name).includes(searchValue)) { + whatFilter.push({ + type: "floor", + src: eventFloor.name, + }); + return true; + } + } else { + if (searchTransform(eventFloorId).includes(searchValue)) { + whatFilter.push({ + type: "floor", + src: eventFloorId, + }); + return true; + } + } + + const eventRoomId = this.mainmanager.convertIdFocus(event.id, "room"); + const eventRoom = this.mainmanager + .getAllFocusObject("room") + .get(eventRoomId); + + if (eventRoom != null) { + if (searchTransform(eventRoom.name).includes(searchValue)) { + whatFilter.push({ + type: "room", + src: eventRoom.name, + }); + return true; + } + } else { + if (searchTransform(eventRoomId).includes(searchValue)) { + whatFilter.push({ + type: "room", + src: eventRoomId, + }); + return true; + } + } + } + }); + + if (this.#Search__selectedTags.length > 0) + filteredEvents = filteredEvents.filter((event, idx) => { + return this.#Search__selectedTags.every((tag) => + event.tags.includes(tag) + ); + }); + + this.uiElements.searchDialogList.replaceChildren(); + const currentDate = new Date(); + filteredEvents.forEach((event, idx) => { + const eventFloorId = this.mainmanager.convertIdFocus(event.id, "floor"); + const eventDateStart = new Date(event.when.start); + const eventDateEnd = new Date(event.when.end); + + const filterReason = whatFilter[idx]; + + const eventStarted = currentDate > eventDateStart; + const eventEnded = currentDate > eventDateEnd; + + let datePrefix; + + if (eventEnded) { + return; // Don't display this event + } else if (eventStarted) { + datePrefix = currentLocalization.date_started; + } else { + datePrefix = currentLocalization.date_starting; + } + + const eventListContainer = document.createElement("div"); + eventListContainer.classList.add("event-list-container"); + eventListContainer.id = "event-" + event.id; + + const eventListContainerSummary = document.createElement("p"); + + if (filterReason != null) { + const eventListContainerSummaryReason = document.createElement("small"); + eventListContainerSummaryReason.textContent = + currentLocalization.search_filters[filterReason.type](filterReason); + eventListContainerSummaryReason.classList.add("reason"); + + eventListContainerSummary.appendChild(eventListContainerSummaryReason); + + eventListContainerSummary.appendChild(document.createElement("br")); + } + + const eventListContainerSummaryName = document.createElement("a"); + eventListContainerSummaryName.href = "#" + event.id; + eventListContainerSummaryName.textContent = event.name; + + eventListContainerSummary.appendChild(eventListContainerSummaryName); + + eventListContainerSummary.appendChild(document.createTextNode(" ")); + + if (this.mainmanager.getCurrentFocus("floor") != eventFloorId) { + const eventFloor = this.mainmanager + .getAllFocusObject("floor") + .get(eventFloorId); + + const eventListContainerSummaryFloor = document.createElement("a"); + eventListContainerSummaryFloor.href = "#" + eventFloorId; + eventListContainerSummaryFloor.textContent = + currentLocalization.parenthesis(eventFloor?.name ?? eventFloorId); + + eventListContainerSummary.appendChild(eventListContainerSummaryFloor); + } else { + const eventRoomId = this.mainmanager.convertIdFocus(event.id, "room"); + const eventRoom = this.mainmanager + .getAllFocusObject("room") + .get(eventRoomId); + + const eventListContainerSummaryRoom = document.createElement("a"); + eventListContainerSummaryRoom.href = "#" + eventRoomId; + eventListContainerSummaryRoom.textContent = + currentLocalization.parenthesis(eventRoom?.name ?? eventRoomId); + + eventListContainerSummary.appendChild(eventListContainerSummaryRoom); + } + + eventListContainerSummary.appendChild(document.createElement("br")); + + const eventListContainerSummarySmall = document.createElement("small"); + + const eventListContainerSummaryTime = document.createElement("span"); + eventListContainerSummaryTime.classList.add("clarification"); + eventListContainerSummaryTime.title = eventDateStart.toLocaleString( + this.mainmanager.getCurrentLangCode(), + { + dateStyle: "short", + timeStyle: "short", + } + ); + eventListContainerSummaryTime.textContent = + currentLocalization.date_summary( + datePrefix, + getRelativeTime( + eventDateStart, + currentDate, + this.mainmanager.getCurrentLangCode() + ) + ); + + eventListContainerSummarySmall.appendChild(eventListContainerSummaryTime); + + eventListContainerSummarySmall.appendChild(document.createTextNode(" ")); + + const eventListContainerSummaryLength = document.createElement("span"); + eventListContainerSummaryLength.textContent = + currentLocalization.parenthesis( + currentLocalization.minutes_long( + getMinutesDifference(eventDateStart, eventDateEnd) + ) + ); + + eventListContainerSummarySmall.appendChild( + eventListContainerSummaryLength + ); + + eventListContainerSummary.appendChild(eventListContainerSummarySmall); + + eventListContainer.appendChild(eventListContainerSummary); + + this.uiElements.searchDialogList.appendChild(eventListContainer); + }); } __initInspector() { @@ -213,8 +573,7 @@ export class UIManager { "" ); - const eventWhere = document.createElement("a"); - eventWhere.href = "#" + eventRoom.id; + const eventWhere = document.createElement("span"); eventWhere.textContent = eventRoom.name; this.uiElements.eventsInspectorEventLength.appendChild(eventWhere); @@ -416,9 +775,10 @@ export class UIManager { this.uiElements.floorButtons.replaceChildren(); // Put them back - currentFloors.forEach(({ name }, id) => { + currentFloors.forEach(({ name, shortName }, id) => { const floorButton = document.createElement("button"); - floorButton.textContent = name; + floorButton.textContent = shortName; + floorButton.title = name; floorButton.classList.add("floor-button"); floorButton.id = "floor-" + id; if (id === mainFocus) floorButton.classList.add("selected"); @@ -430,16 +790,15 @@ export class UIManager { }); if (currentFloors.size < 1) { - this.uiElements.floors.classList.add("empty"); + this.uiElements.controls.classList.add("empty"); this.floorsEmpty = true; } else { - this.uiElements.floors.classList.remove("empty"); + this.uiElements.controls.classList.remove("empty"); this.floorsEmpty = false; } } __updateFloorsSoft() { - const currentLocalization = this.mainmanager.getCurrentLocalization(); const currentFloors = this.mainmanager.getAllFocusObject("floor"); const mainFocus = this.mainmanager.getCurrentFocus("floor"); diff --git a/assets/styles/dialog.css b/assets/styles/dialog.css new file mode 100644 index 0000000..9030de0 --- /dev/null +++ b/assets/styles/dialog.css @@ -0,0 +1,90 @@ +/* I fell into a burning ring of fire / Went down down down / And the flames went higher */ +#search-dialog-scrim { + position: absolute; + left: 0; + top: 0; + width: 100vw; + height: 100%; + background-color: #0008; + opacity: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.125s; +} + +#search-dialog-scrim.open { + opacity: 1; + pointer-events: all; + user-select: text; +} + +#search-dialog { + box-sizing: border-box; + display: inline-flex; + gap: 8px; + padding: 8px; + border-radius: 4px; + background-color: #202020; + color: #fff; + box-shadow: 0 0 8px 0 #0008; + flex-direction: column; + width: calc(100% - 16px); + max-width: 624px; +} + +#search-dialog #search-dialog-header { + display: grid; + align-items: center; + grid-template-columns: auto 36px; + gap: 8px; +} + +#search-dialog #search-dialog-bar { + display: grid; + align-items: center; + grid-template-columns: auto; + gap: 8px; +} + +#search-dialog #search-dialog-tags { + display: flex; + align-items: center; + overflow-x: auto; + overflow-y: hidden; + gap: 8px; + width: 100%; + padding: 6px 0; +} + +#search-dialog #search-dialog-tags .search-tag { + white-space: nowrap; + height: 24px; + padding: 0 8px; +} + +#search-dialog #search-dialog-tags .search-tag.selected { + border: 1px solid #fff; +} + +#search-dialog #search-list { + width: 100%; + overflow-y: auto; + overflow-x: hidden; + box-sizing: border-box; + padding: 8px; + background-color: #0002; + display: flex; + flex-direction: column; + gap: 4px; + display: inline-block; +} + +#search-dialog #search-list .event-list-container p { + margin: 0; +} + +#search-dialog #search-list .event-list-container p .reason { + opacity: 0.5; +} \ No newline at end of file diff --git a/assets/styles/style.css b/assets/styles/style.css index e7a56ad..55447cd 100644 --- a/assets/styles/style.css +++ b/assets/styles/style.css @@ -4,7 +4,7 @@ body, html { margin: 0; - height: 100vh; + height: 100%; width: 100vw; box-sizing: border-box; overflow: hidden; @@ -42,6 +42,7 @@ button:not(.link) { border-radius: 4px; height: 36px; border: 1px solid #0008; + user-select: none; } button:not(.link):hover { @@ -68,6 +69,23 @@ button.link { cursor: pointer; } +button:not(.link):focus, +input:focus { + outline: 1px solid #808080; +} + +input { + background-color: #0004; + border: none; + color: inherit; + font: inherit; + box-sizing: border-box; + padding: 6px; + border-radius: 4px; + height: 36px; + border: 1px solid #0008; +} + a { color: currentColor; } @@ -85,25 +103,39 @@ a { left: 0; top: 0; width: 100vw; - height: 100vh; + height: 100%; pointer-events: none; user-select: none; } +#map-ui #controls { + display: flex; + flex-direction: column; + gap: 8px; + position: absolute; + top: 0; + right: 0; + padding: 8px; + background-color: #fff4; + border-radius: 0 0 0 4px; +} + +#map-ui #controls.empty { + right: -100%; +} + #map-ui #zoom { pointer-events: all; box-sizing: border-box; display: flex; flex-direction: column; gap: 8px; - position: absolute; padding: 8px; - top: 16px; - left: 16px; border-radius: 4px; background-color: #202020; color: #fff; box-shadow: 0 0 8px 0 #0008; + transition: left 0.5s; } #map-ui #zoom .zoom-button { @@ -114,19 +146,12 @@ a { pointer-events: all; display: inline-block; box-sizing: border-box; - position: absolute; padding: 8px; - top: 16px; - right: 16px; border-radius: 4px; background-color: #202020; color: #fff; box-shadow: 0 0 8px 0 #0008; - transition: right 0.5s; -} - -#map-ui #floors.empty { - right: -100%; + transition: left 0.5s; } #map-ui #floors #floor-buttons { @@ -145,6 +170,27 @@ a { border: 1px solid #fff; } +#map-ui #menu-bar { + position: absolute; + left: 8px; + top: 8px; + pointer-events: all; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; + border-radius: 4px; + background-color: #202020; + color: #fff; + box-shadow: 0 0 8px 0 #0008; + transition: left 0.5s; +} + +#map-ui #menu-bar .menu-button { + width: 36px; +} + #map-ui #events-container { position: absolute; left: 0; @@ -156,13 +202,14 @@ a { #map-ui #events-container #events, #map-ui #events-container #events-inspector { pointer-events: all; + /* user-select: text; */ box-sizing: border-box; display: inline-flex; position: absolute; gap: 8px; padding: 8px; - left: 16px; - bottom: 16px; + left: 8px; + bottom: 8px; border-radius: 4px; translate: 0 calc(100% + 32px); background-color: #202020; @@ -172,7 +219,7 @@ a { max-height: calc(50vh - 16px); flex-direction: column; width: 400px; - max-width: calc(100% - 32px); + max-width: calc(100% - 16px); z-index: 9; } @@ -269,6 +316,7 @@ a { background-color: #2228; border-radius: 8px; opacity: 0; + z-index: 1000; transition: opacity 0.25s; } diff --git a/pages/index.html b/pages/index.html index 1767a32..c9da32e 100644 --- a/pages/index.html +++ b/pages/index.html @@ -10,18 +10,29 @@ rel="stylesheet" href="/assets/styles/style.css" /> +
-
- - -
-
-
- +
+
+ +
+
+
+ +
+
+
+
@@ -51,6 +62,24 @@
+
+
+
+
+

+
+ +
+
+ +
+
+
+
+ +
+
+
diff --git a/scripts/server.js b/scripts/server.js index a824756..534f709 100644 --- a/scripts/server.js +++ b/scripts/server.js @@ -22,9 +22,48 @@ app.get('/data/:lang/events/:floor', async (req, res) => { // Merge events and localization const lang = req.params.lang; const merged = events.map(event => { - event.name = event.lang[lang]?.name ?? ""; - event.description = event.lang[lang]?.description ?? ""; - event.url = event.lang[lang]?.url ?? ""; + const curLang = event.lang[lang] ?? event.lang[config.default_language]; + + event.name = curLang.name ?? ""; + event.description = curLang.description ?? ""; + event.url = curLang.url ?? ""; + delete event.lang; + return event; + }); + + return res.send(merged); +}) + +app.get('/data/:lang/events/', async (req, res) => { + // Get floors + let floorsReq = await fetch(new URL("floors.json", config.data_url)); + let floors; + if (floorsReq.ok) + floors = await floorsReq.json(); + else + return res.status(400).send("Bad Request"); + + let allEvents = []; + + await Promise.allSettled(floors.map(async (curFloor) => { + let eventsReq = await fetch(new URL(`events/${curFloor.id}.json`, config.data_url)); + let events; + if (eventsReq.ok) + events = await eventsReq.json(); + else + return null; + + allEvents = allEvents.concat(events); + })); + + // Merge events and localization + const lang = req.params.lang; + const merged = allEvents.map(event => { + const curLang = event.lang[lang] ?? event.lang[config.default_language]; + + event.name = curLang.name ?? ""; + event.description = curLang.description ?? ""; + event.url = curLang.url ?? ""; delete event.lang; return event; }); @@ -33,21 +72,48 @@ app.get('/data/:lang/events/:floor', async (req, res) => { }) app.get('/data/:lang/floors', async (req, res) => { - // Get layers - let layersReq = await fetch(new URL("floors.json", config.data_url)); - let layers; - if (layersReq.ok) - layers = await layersReq.json(); + // Get floors + let floorsReq = await fetch(new URL("floors.json", config.data_url)); + let floors; + if (floorsReq.ok) + floors = await floorsReq.json(); else return res.status(400).send("Bad Request"); - // Merge layers and localization + // Merge floors and localization const lang = req.params.lang; - const merged = layers.map(layer => { - layer.name = layer.lang[lang]?.name ?? ""; - layer.description = layer.lang[lang]?.description ?? ""; - delete layer.lang; - return layer; + const merged = floors.map(floor => { + const curLang = floor.lang[lang] ?? floor.lang[config.default_language]; + + floor.name = curLang.name ?? ""; + floor.shortName = curLang.shortName ?? ""; + floor.description = curLang.description ?? ""; + delete floor.lang; + return floor; + }); + + return res.send(merged); +}) + +app.get('/data/:lang/tags', async (req, res) => { + // Get tags + let tagsReq = await fetch(new URL("tags.json", config.data_url)); + let tags; + if (tagsReq.ok) + tags = await tagsReq.json(); + else + return res.status(400).send("Bad Request"); + + // Merge floors and localization + const lang = req.params.lang; + const merged = tags.map(tag => { + const curLang = tag.lang[lang] ?? tag.lang[config.default_language]; + + tag.name = curLang.name ?? ""; + tag.shortName = curLang.shortName ?? ""; + tag.description = curLang.description ?? ""; + delete tag.lang; + return tag; }); return res.send(merged); @@ -65,9 +131,11 @@ app.get('/data/:lang/rooms/:floor', async (req, res) => { // Merge rooms and localization const lang = req.params.lang; const merged = rooms.map(room => { - room.name = room.lang[lang]?.name ?? ""; - room.shortName = room.lang[lang]?.shortName ?? ""; - room.description = room.lang[lang]?.description ?? ""; + const curLang = room.lang[lang] ?? room.lang[config.default_language]; + + room.name = curLang.name ?? ""; + room.shortName = curLang.shortName ?? ""; + room.description = curLang.description ?? ""; delete room.lang; return room; });