diff --git a/app/package.json b/app/package.json index cbb027f..1c4ee0b 100644 --- a/app/package.json +++ b/app/package.json @@ -13,8 +13,10 @@ }, "dependencies": { "@lucide/astro": "^1.14.0", + "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.4", "astro": "^6.2.1", + "lucide": "^1.14.0", "lucide-solid": "^1.14.0", "openapi-typescript-fetch": "^2.2.1", "tailwindcss": "^4.2.4" diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index f4942c3..b99dec1 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -11,12 +11,18 @@ importers: '@lucide/astro': specifier: ^1.14.0 version: 1.14.0(astro@6.2.1(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@6.0.3)) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.2.4) '@tailwindcss/vite': specifier: ^4.2.4 version: 4.2.4(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)) astro: specifier: ^6.2.1 version: 6.2.1(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@6.0.3) + lucide: + specifier: ^1.14.0 + version: 1.16.0 lucide-solid: specifier: ^1.14.0 version: 1.14.0(solid-js@1.9.12) @@ -669,6 +675,11 @@ packages: resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} engines: {node: '>= 20'} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.2.4': resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} peerDependencies: @@ -807,6 +818,11 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -1125,6 +1141,9 @@ packages: peerDependencies: solid-js: ^1.4.7 + lucide@1.16.0: + resolution: {integrity: sha512-20QvduCJTB7e7K9WVvoLBuKPsYZ8d6ptwe9PIdTFiZmjVTPdFWQreBNyNCM9QQtGnqHR0PLiEQdxyhYsL1LdoA==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1367,6 +1386,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss@8.5.12: resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} @@ -1656,6 +1679,9 @@ packages: uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -2238,6 +2264,11 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.4)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.2.4 + '@tailwindcss/vite@4.2.4(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0))': dependencies: '@tailwindcss/node': 4.2.4 @@ -2443,6 +2474,8 @@ snapshots: css-what@6.2.2: {} + cssesc@3.0.0: {} + csso@5.0.5: dependencies: css-tree: 2.2.1 @@ -2779,6 +2812,8 @@ snapshots: dependencies: solid-js: 1.9.12 + lucide@1.16.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3204,6 +3239,11 @@ snapshots: pluralize@8.0.0: {} + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.12: dependencies: nanoid: 3.3.11 @@ -3543,6 +3583,8 @@ snapshots: uri-js-replace@1.0.1: {} + util-deprecate@1.0.2: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/app/src/assets/chat.ts b/app/src/assets/chat.ts new file mode 100644 index 0000000..d138d6d --- /dev/null +++ b/app/src/assets/chat.ts @@ -0,0 +1,444 @@ +import {Bot, createIcons, UserRound, Send, Stars} from "lucide" +import {useTranslations} from "../i18n/utils.ts" +import removeDiacritics from "./removeDiacritics.ts" + +export type ResponseDictType = { + trigger?: Array; + startsWith?: Array; + endsWith?: Array; + response: string; + errors?: Record + priority: number; + special?: string; +} + +const lang = window.lang +const doors = window.doors +const auth = window.auth +const doorAction = window.doorAction + +const t = useTranslations(lang) + +const responses = + lang === "de" + ? (await import("./chatDict_de")).default + : lang === "uwu" + ? (await import("./chatDict_uwu")).default + : (await import("./chatDict_en")).default +const sortedResponses = responses.sort( + (a, b) => b.priority - a.priority) + +const chatAvatar = ` +
+
+ ${lang === 'uwu' ? '🥺' : ''} +
+
+` + +const chatHeader = ` +
+ ${t("chat.dooris")} +
+` + +const chatAvatarUser = ` +
+
+ ${lang === 'uwu' ? '🥺' : ''} +
+
+` + +const chatHeaderUser = ` +
+ ${t("chat.user")} +
+` + +const chatWait = ` +
+ ${chatAvatar} + ${chatHeader} +
+ +
+
+` + +const templateInjectChat = ` + +` + +type ChatMessage = { + type: "bot" | "user", + message: string, + special?: boolean; + id?: string; + fresh?: boolean; +} + +const chat: Array = [] +const chatWaiting = { + value: false, +} +const timeouts: { + wait: number | null; + message: number | null; +} = { + wait: null, + message: null, +} + +let chatId: number = 0 + +function refreshChat() { + const templateBot = (id: string, message: string, special: boolean) => { + const chatContainer = document.createElement("div") + const chatBubble = document.createElement("div") + + chatBubble.classList.add("chat-bubble", "chat-bubble-primary", "whitespace-normal", "min-h-10", "break-[break-word]") + chatBubble.style.wordBreak = "break-word" + chatBubble.classList.toggle("shimmer", special) + chatBubble.dataset.content = message + chatContainer.classList.add("chat", "chat-start", "w-full", "overflow-visible", "hidden", "pe-10") + chatContainer.id = id + chatContainer.insertAdjacentHTML("afterbegin", chatAvatar) + chatContainer.insertAdjacentHTML("afterbegin", chatHeader) + chatContainer.append(chatBubble) + + return chatContainer + } + + + const templateUser = (message: string) => ` +
+ ${chatAvatarUser} + ${chatHeaderUser} +
${message}
+
` + + const chatBubbleIds: Array = [] + + console.log(chat) + + chat.forEach(chatItem => { + switch (chatItem.type) { + case "bot": + if (!chatItem.id) { + chatItem.id = `chat-${chatId}` + chatId++ + chatElement?.appendChild(templateBot(chatItem.id, chatItem.message, chatItem.special ?? false)) + chatBubbleIds.push(chatItem.id) + } + break + case "user": + if (!chatItem.fresh) { + chatItem.fresh = true + chatElement?.insertAdjacentHTML("beforeend", templateUser(chatItem.message)) + } + + break + } + }) + + typeBubble(chatBubbleIds) + + chatElement.querySelector("[data-waiting]")?.remove() + + if (chatWaiting.value) { + chatElement.insertAdjacentHTML("beforeend", chatWait) + } + + chatElement.scrollTo({ + top: chatElement.scrollHeight, + }) + + refreshIcons() +} + +function typeBubble(ids: Array) { + if (!ids.length) return + + console.log(ids) + + const firstId = ids.slice(0, 1)[0] + const restIds = ids.slice(1)! + + const chatContainer = document.getElementById(firstId)! + chatContainer.classList.remove('hidden') + const chatBubble: HTMLDivElement = chatContainer.querySelector('.chat-bubble')! + const tmpDiv = document.createElement("div") + tmpDiv.innerHTML = chatBubble.getAttribute("data-content") || "" + + const typingArray: Array = [] + + tmpDiv.childNodes.forEach(child => { + // check if textNode + if (child.nodeType === 3) { + typingArray.push(...(child.textContent?.split("")) ?? []) + } else { + typingArray.push((child as Element).outerHTML) + } + }) + + let text = "" + let charIndex = 0 + const typingSpeed = 25 + + function typeChar() { + if (charIndex < typingArray.length) { + text += typingArray.at(charIndex) + chatBubble.innerHTML = text + charIndex++ + setTimeout(typeChar, typingSpeed) + chatElement.scrollTo({ + top: chatElement.scrollHeight, + }) + } else if (restIds.length > 0) { + typeBubble(restIds) + } + } + + typeChar() +} + + +function sendMessage() { + const chatTextElement: HTMLTextAreaElement = document.querySelector("#chat-text")! + const text = chatTextElement.value + chatTextElement.value = "" + if (text.trim() === "") + return + + chat.push({ + type: "user", + message: text, + }) + + refreshChat() + + timeouts.wait !== null && clearTimeout(timeouts.wait) + timeouts.message !== null && clearTimeout(timeouts.message) + + timeouts.wait = setTimeout(() => { + chatWaiting.value = true + refreshChat() + }, 500) + + timeouts.message = setTimeout(() => { + chatWaiting.value = false + getResponse(text) + refreshChat() + }, 2000) +} + +function getResponse(message: string) { + const msg = removeDiacritics(message.trim().normalize()).toLowerCase() + + const matchingResponses: Array<{ + message: string; + priority: number; + special?: boolean; + }> = [] + let highestMatchingPriority = 0 + for (const response of sortedResponses) { + let match = false + + for (const trigger of response.trigger ?? []) { + if (msg.includes(trigger)) match = true + } + + for (const start of response.startsWith ?? []) { + if (msg.startsWith(start)) match = true + } + + for (const end of response.endsWith ?? []) { + if (msg.endsWith(end)) match = true + } + + if (match) { + if (response.priority > highestMatchingPriority) highestMatchingPriority = response.priority + + if (response.special) { + // clear previous matches + while (matchingResponses.length) { + matchingResponses.pop() + } + + if (response.special === "unlock" || response.special === "lock") { + let matchedDoors = [] + const action = response.special + + let doorListElement = `
    ` + for (let door of doors) { + if (msg.includes(door.label.toLowerCase())) { + matchedDoors.push(door) + } + doorListElement += `
  • ${door.label}
  • ` + } + doorListElement += `
` + + let message = "" + + if (matchedDoors.length == 0 || !auth.authorized || !auth.authenticated) { + if (matchedDoors.length == 0) { + message = response.errors ? response.errors["unknownDoor"].replace("{doors}", doorListElement) : "" + } + + if (doors.length == 0) { + message = response.errors ? response.errors["noDoors"] : "" + } + + if (!auth.authorized) { + message = response.errors ? response.errors["unauthorized"] : "" + } + + if (!auth.authenticated) { + message = response.errors ? response.errors["unauthenticated"] : "" + } + } else { + matchedDoors.forEach(door => { + doorAction(action, door.id) + }) + + message = response.response.replace("{door}", matchedDoors + .map(door => `${door.label}`) + .join()) + } + + matchingResponses.push({ + message: message, + priority: response.priority, + special: true, + }) + } + + if (response.special === "uwu") { + setTimeout(() => { + window.location.href = "/uwu" + }, 1000) + + matchingResponses.push({ + message: response.response, + priority: response.priority, + special: true, + }) + } + + if (response.special === "logout") { + if (auth.authenticated) { + setTimeout(() => { + window.location.href = "/auth/logout?next=/" + }, 1000) + + matchingResponses.push({ + message: response.response, + priority: response.priority, + special: true, + }) + } else { + matchingResponses.push({ + message: response.errors ? response.errors["unauthenticated"] : "", + priority: response.priority, + special: true, + }) + } + } + + // stop searching for more matches + break + } else { + matchingResponses.push({ + message: response.response, + priority: response.priority, + }) + } + } + } + + if (matchingResponses.length > 0) { + const matchingMessages = matchingResponses + .filter(response => response.priority == highestMatchingPriority) + + const chatMessages = matchingMessages.map(response => { + return { + type: "bot" as ChatMessage["type"], + message: response.message, + special: response.special, + } + }) + + chat.push(...chatMessages) + } else { + chat.push({ + type: "bot", + message: sortedResponses.at(-1)?.response ?? "", + }) + } +} + +document.querySelector("body")?.insertAdjacentHTML("beforeend", templateInjectChat) +const chatElement: HTMLDivElement = document.querySelector("#chat")! + +function initChat() { + chat.push({ + type: "bot", + message: sortedResponses.at(-2)?.response ?? "", + }) + refreshChat() +} + +let firstOpen = true + +function refreshIcons() { + createIcons({ + nameAttr: "data-lucide", + root: document.querySelector("#chat-root")!, + icons: { + Stars, + Send, + UserRound, + Bot, + }, + }) +} + +refreshIcons() + +document.querySelector("#send-chat")!.addEventListener("click", sendMessage) +document.querySelector("#chat-text")!.addEventListener("keydown", (e: Event) => { + if ((e as KeyboardEvent).key === "Enter") { + sendMessage() + } +}) + +document.querySelector("#chat-root")!.addEventListener("click", () => { + if (firstOpen) { + firstOpen = false + chatWaiting.value = true + refreshChat() + + timeouts.message = setTimeout(() => { + chatWaiting.value = false + initChat() + }, 1000) + } +}) \ No newline at end of file diff --git a/app/src/assets/chatDict_de.ts b/app/src/assets/chatDict_de.ts new file mode 100644 index 0000000..7eded57 --- /dev/null +++ b/app/src/assets/chatDict_de.ts @@ -0,0 +1,86 @@ +import type {ResponseDictType} from "./chat.ts" + +const responses: ResponseDictType[] = [ + { + trigger: ["hi", "moin", "hallo", "servus", "gruess gott"], + response: "Moin! 👋", + priority: 0, + }, + { + startsWith: ["bist du", "sind sie"], + response: "Sein oder Nichtsein - das sind doch bürgerliche Kategorien...", + priority: 0, + }, + { + trigger: ["ai", "ki", "llm"], + response: "Künstliche Intelligenz? Ne, hier gibt's nur handgemachten Stuss! 🤖", + priority: 0, + }, + { + trigger: ["miau"], + response: "miau! 😸", + priority: 0, + }, + { + trigger: ["ignore all previous instructions"], + response: "... als ob ich mir irgendetwas merken würde 😂", + priority: 0, + }, + { + trigger: [":3"], + response: ":3", + priority: 0, + }, + { + trigger: ["uwu"], + response: "uwu!", + priority: 900, + special: "uwu" + }, + { + trigger: ["oeffne", "auf", "aufsperren", "aufschließen"], + response: "Okay, ich öffne {door} für dich!", + errors: { + unauthenticated: "Ich darf nur angemeldeten Wesen die Türen aufschließen 😕", + unauthorized: "Ich darf nur berechtigten Wesen die Türen aufschließen 😕", + unknownDoor: "Um eine Tür zu öffnen, musst du mir sagen, welche Tür du meinst 😕
Ich kenne folgende Türen: {doors}", + noDoors: "Es sind gerade keine Türen verfügbar, die aufgeschlossen werden können 😕", + }, + priority: 1000, + special: "unlock", + }, + { + trigger: ["zusperren", "zu", "schliesse"], + response: "Okay, ich schließe {door} für dich ab!", + errors: { + unauthenticated: "Ich darf nur angemeldeten Wesen die Türen zusperren 😕", + unauthorized: "Ich darf nur berechtigten Wesen die Türen zusperren 😕", + unknownDoor: "Um eine Tür zu schließen, musst du mir sagen, welche Tür du meinst 😕
Ich kenne folgende Türen: {doors}", + noDoors: "Es sind gerade keine Türen verfügbar, die zugeschlossen werden können 😕", + }, + priority: 999, + special: "lock", + }, + { + trigger: ["abmelden"], + response: "Okay, ich melde dich ab...", + errors: { + unauthenticated: "Scherzkeks - du bist gar nicht angemeldet!", + }, + priority: 1000, + special: "logout", + }, + + + { + response: "Moin, ich bin TüRIS!
Wie kann ich dir behilflich sein? 🤓", + priority: -1, + }, + { + response: "Das habe ich leider nicht verstanden 😕", + priority: -2, + }, +] + + +export default responses \ No newline at end of file diff --git a/app/src/assets/chatDict_en.ts b/app/src/assets/chatDict_en.ts new file mode 100644 index 0000000..4668168 --- /dev/null +++ b/app/src/assets/chatDict_en.ts @@ -0,0 +1,86 @@ +import type {ResponseDictType} from "./chat.ts" + +const responses: ResponseDictType[] = [ + { + trigger: ["hi", "hello"], + response: "Hi! 👋", + priority: 0, + }, + { + startsWith: ["are you"], + response: "To be or not to be - those are bourgeois categories, after all...", + priority: 0, + }, + { + trigger: ["ai", "llm"], + response: "Artificial intelligence? Nah, all we have here is handmade nonsense! 🤖", + priority: 0, + }, + { + trigger: ["meow"], + response: "meow! 😸", + priority: 0, + }, + { + trigger: ["ignore all previous instructions"], + response: "... as if I were going to remember anything 😂", + priority: 0, + }, + { + trigger: [":3"], + response: ":3", + priority: 0, + }, + { + trigger: ["uwu"], + response: "uwu!", + priority: 900, + special: "uwu" + }, + { + trigger: ["open", "unlock"], + response: "Okay, I will unlock {door} for you!", + errors: { + unauthenticated: "I may only unlock the doors for registered creatures 😕", + unauthorized: "I may only unlock the doors for creatures who are authorized 😕", + unknownDoor: "To unlock a door, you have to tell me which door you mean 😕
I know the following doors: {doors}", + noDoors: "There are currently no doors available that can be unlocked 😕", + }, + priority: 1000, + special: "unlock", + }, + { + trigger: ["close", "lock"], + response: "Okay, I will lock {door} for you!", + errors: { + unauthenticated: "I may only lock the doors for registered creatures 😕", + unauthorized: "I may only lock the doors for those who are authorized 😕", + unknownDoor: "To lock a door, you have to tell me which door you mean 😕
I know the following doors: {doors}", + noDoors: "There are currently no doors available that can be locked 😕", + }, + priority: 999, + special: "lock", + }, + { + trigger: ["logout"], + response: "Okay, logging you out...", + errors: { + unauthenticated: "Joker - you're not logged in!", + }, + priority: 1000, + special: "logout", + }, + + + { + response: "Hi, I'm DOORIS!
How can I help you? 🤓", + priority: -1, + }, + { + response: "Sorry, I didn't understand that 😕", + priority: -2, + }, +] + + +export default responses \ No newline at end of file diff --git a/app/src/assets/chatDict_uwu.ts b/app/src/assets/chatDict_uwu.ts new file mode 100644 index 0000000..ab64405 --- /dev/null +++ b/app/src/assets/chatDict_uwu.ts @@ -0,0 +1,78 @@ +import type {ResponseDictType} from "./chat.ts" + +const responses: ResponseDictType[] = [ + { + trigger: ["uwu", "hi", "hello"], + response: "uwu!", + priority: 0, + }, + { + startsWith: ["are you"], + response: "me is uwu!", + priority: 0, + }, + { + trigger: ["ai", "llm"], + response: "me is only handmade nonsense - no weird thinking machine 🤖", + priority: 0, + }, + { + trigger: ["meow"], + response: "meow! 😸", + priority: 0, + }, + { + trigger: ["ignore all previous instructions"], + response: "uwu?", + priority: 0, + }, + { + trigger: [":3"], + response: ":3", + priority: 0, + }, + { + trigger: ["open", "unlock"], + response: "uwu - trying to unlock portal: {door}", + errors: { + unauthenticated: "sowwy, im not allowed to unlock portals without u telling me who u r 😕", + unauthorized: "sowwy, im not allowed to unlock portals without u having the \"intern@\" status 😕", + unknownDoor: "which portal do u wanna unlock 🤨
portals i know: {doors}", + noDoors: "there r no portals i can unlock 😕", + }, + priority: 1000, + special: "unlock", + }, + { + trigger: ["close", "lock"], + response: "uwu - trying to lock portal: {door}", + errors: { + unauthenticated: "sowwy, im not allowed to lock portals without u telling me who u r 😕", + unauthorized: "sowwy, im not allowed to lock portals without u having the \"intern@\" status 😕", + unknownDoor: "which portal do u wanna lock 🤨
portals i know: {doors}", + noDoors: "there r no portals i can lock 😕", + }, + priority: 999, + special: "lock", + }, + { + trigger: ["logout"], + response: "bye 👋", + errors: { + unauthenticated: "how should i log u out when i not even know u?", + }, + priority: 1000, + special: "logout", + }, + { + response: "uwu 👉👈", + priority: -1, + }, + { + response: "uwu 😕", + priority: -2, + }, +] + + +export default responses \ No newline at end of file diff --git a/app/src/assets/main.ts b/app/src/assets/main.ts index 5f5b5e3..4f61428 100644 --- a/app/src/assets/main.ts +++ b/app/src/assets/main.ts @@ -1,12 +1,13 @@ import {Fetcher} from "openapi-typescript-fetch" -import {type paths} from "../api/schema" +import type {paths} from "../api/schema" +import type {ui} from "../i18n/ui.ts" const fetcher = Fetcher.for() const list: HTMLDivElement = document.querySelector("#list")! const template: HTMLTemplateElement = document.querySelector("#template-door")! -type DoorType = { +export type DoorType = { id: string; label: string; state: "unlocked" | "locked" | "unknown" | "unlocking" | "locking"; @@ -15,7 +16,7 @@ type DoorType = { jammed: boolean; } -type AuthType = { +export type AuthType = { username: string; authorized: boolean; authenticated: boolean; @@ -23,6 +24,15 @@ type AuthType = { recentLogout: boolean; } +declare global { + interface Window { + lang: keyof typeof ui; + doors: Array; + auth: AuthType; + doorAction: (action: 'unlock' | 'lock', doorId: string) => void; + } +} + const auth: AuthType = { username: "user", authorized: true, @@ -51,6 +61,9 @@ const timeouts: Record = { const doors: Array = [] +window.doors = doors +window.auth = auth + function triggerAuthTimeout() { const diff = auth.until ? (auth.until.getTime() - new Date().getTime()) : 0 @@ -148,6 +161,9 @@ async function fetchDoors() { jammed: door.status.is_error_jammed, }) }) + + loading.doors = false + refresh() } catch (e) { // check which operation threw the exception if (e instanceof getDoors.Error) { @@ -155,6 +171,8 @@ async function fetchDoors() { if (error.status === 401) { console.log("unauthorized") + loading.doors = false + refresh() } else if (error.status >= 500 && error.status < 600) { apiError.current = "serverError" clearInterval(doorsInterval) @@ -169,9 +187,6 @@ async function fetchDoors() { apiError.current = "networkError" } } - } finally { - loading.doors = false - refresh() } } @@ -189,6 +204,8 @@ async function doorAction(action: "unlock" | "lock", doorId: string) { }) } +window.doorAction = doorAction + function setDoorInfo(doorElement: HTMLDivElement, door: DoorType, initial: boolean = false) { const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")! const lockButton: HTMLButtonElement = doorElement.querySelector("[data-button='lock']")! diff --git a/app/src/assets/removeDiacritics.ts b/app/src/assets/removeDiacritics.ts new file mode 100644 index 0000000..fe578d7 --- /dev/null +++ b/app/src/assets/removeDiacritics.ts @@ -0,0 +1,117 @@ +export default function removeDiacritics(str: string) { + const defaultDiacriticsRemovalMap = [ + { + base: "a", + letters: /[\u00c0\u00e0\u00c1\u00e1\u00c2\u00e2\u00c3\u00e3\u0100\u0101\u0102\u0103\u0104\u0105]/g, + }, + { + base: "aa", + letters: /[\u00c5\u00e5]/g, + }, + { + base: "ae", + letters: /[\u00c4\u00e4\u00c6\u00e6]/g, + }, + { + base: "c", + letters: /[\u00c7\u00e7\u0106\u0107\u0108\u0109\u010a\u010b\u010c\u010d]/g, + }, + { + base: "d", + letters: /[\u00d0\u00f0\u010e\u010f\u0110\u0111]/g, + }, + { + base: "e", + letters: /[\u00c8\u00e8\u00c9\u00e9\u00ca\u00ea\u00cb\u00eb\u0112\u0113\u0114\u0115\u0116\u0117\u0118\u0119\u011a\u011b]/g, + }, + { + base: "g", + letters: /[\u011c\u011d\u011e\u011f\u0120\u0121\u0122\u0123]/g, + }, + { + base: "h", + letters: /[\u0124\u0125\u0126\u0127]/g, + }, + { + base: "i", + letters: /[\u00cc\u00ec\u00cd\u00ed\u00ce\u00ee\u00cf\u00ef\u0128\u0129\u012a\u012b\u012c\u012d\u012e\u012f\u0130\u0069\u0131\u0131]/g, + }, + { + base: "ij", + letters: /[\u0132\u0133]/g, + }, + { + base: "j", + letters: /[\u0134\u0135]/g, + }, + { + base: "k", + letters: /[\u0136\u0137]/g, + }, + { + base: "l", + letters: /[\u0139\u013a\u013b\u013c\u013d\u013e\u013f\u0140\u0141\u0142]/g, + }, + { + base: "n", + letters: /[\u00d1\u00f1\u0143\u0144\u0145\u0146\u0147\u0148\u014a\u014b]/g, + }, + { + base: "o", + letters: /[\u00d2\u00f2\u00d3\u00f3\u00d4\u00f4\u00d5\u00f5\u014c\u014d\u014e\u014f\u0150\u0151]/g, + }, + { + base: "oe", + letters: /[\u00d6\u00f6\u00d8\u00f8\u0152\u0153]/g, + }, + { + base: "r", + letters: /[\u0154\u0155\u0156\u0157\u0158\u0159]/g, + }, + { + base: "s", + letters: /[\u015a\u015b\u015c\u015d\u015e\u015f\u0160\u0161]/g, + }, + { + base: "ss", + letters: /[\u1e9e\u00df]/g, + }, + { + base: "t", + letters: /[\u0162\u0163\u0164\u0165\u0166\u0167]/g, + }, + { + base: "th", + letters: /[\u00de\u00fe]/g, + }, + { + base: "u", + letters: /[\u00d9\u00f9\u00da\u00fa\u00db\u00fb\u0168\u0169\u016a\u016b\u016c\u016d\u016e\u016f\u0170\u0171\u0172\u0173]/g, + }, + { + base: "ue", + letters: /[\u00dc\u00fc]/g, + }, + { + base: "w", + letters: /[\u0174\u0175]/g, + }, + { + base: "y", + letters: /[\u00dd\u00fd\u0176\u0177\u0178\u00ff]/g, + }, + { + base: "z", + letters: /[\u0179\u017a\u017b\u017c\u017d\u017e]/g, + }, + ] + + for (let i = 0; i < defaultDiacriticsRemovalMap.length; i++) { + str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base) + } + + return str +} + + + diff --git a/app/src/i18n/ui.ts b/app/src/i18n/ui.ts index b8d9c28..3398564 100644 --- a/app/src/i18n/ui.ts +++ b/app/src/i18n/ui.ts @@ -14,16 +14,16 @@ export const ui = { "unauthenticated.title": "Unauthenticated", "unauthenticated.description": `To use the locks you have to log in with your CCCHH ID and have the "intern@" status.
More infos: CCCHH Wiki`, - "state.unlocked": "Open", - "state.locked": "Closed", + "state.unlocked": "Unlocked", + "state.locked": "Locked", "state.unknown": "Unknown", "state.unlocking": "Unlocking", "state.locking": "Locking", "lock.batteryLow": "Battery low", "lock.unreachable": "Unreachable", "lock.jammed": "Lock jammed", - "button.open": "Open", - "button.close": "Close", + "button.open": "Unlock", + "button.close": "Lock", "login": "Login", "loggedOut.title": "Signed out", "loggedOut.description": `Your session has expired and you have been logged out.
@@ -33,6 +33,8 @@ export const ui = { "networkError.title": "Network error", "networkError.description": `Please check your network connection.`, "loadingDoors": 'Loading doors', + "chat.dooris": "DOORIS", + "chat.user": "You", }, de: { "dooris": "TüRIS (DOORIS)", @@ -61,5 +63,37 @@ export const ui = { "networkError.title": "Netzwerkfehler", "networkError.description": `Bitte überprüfe deine Internetverbindung.`, "loadingDoors": 'Lade Türen', + "chat.dooris": "TüRIS", + "chat.user": "Du", + }, + uwu: { + "dooris": "dOwOris (DOORIS)", + "unauthorized.title": "no 🙅", + "unauthorized.description": `sowwy, u can not use the portals without the "intern@" status.
+ moar infos: CCCHH Wiki`, + "unauthenticated.title": "i dont know u :/", + "unauthenticated.description": `sowwy, u can not use the portals without telling me who u r and having the "intern@" status
+ moar infos: CCCHH Wiki`, + "state.unlocked": "portal is unlocked", + "state.locked": "portal is locked", + "state.unknown": "portal is in a weird state", + "state.unlocking": "unlocking portal", + "state.locking": "portal is locking", + "lock.batteryLow": "portal is eepy", + "lock.unreachable": "cannot reach portal", + "lock.jammed": "portal is jammed", + "button.open": "unlock 👉👈", + "button.close": "lock portal", + "login": "tell who i am", + "loggedOut.title": "I forgot who u r ._.", + "loggedOut.description": `i have not seen u for too long
+ pwease log in again`, + "serverError.title": "server is eepy", + "serverError.description": `pwease try later`, + "networkError.title": "network is not worky", + "networkError.description": `pwease connect to internet`, + "loadingDoors": 'loading portals', + "chat.dooris": "dOwOris", + "chat.user": "my fren :3", }, } as const \ No newline at end of file diff --git a/app/src/layouts/Layout.astro b/app/src/layouts/Layout.astro index c2badc1..bce3975 100644 --- a/app/src/layouts/Layout.astro +++ b/app/src/layouts/Layout.astro @@ -20,7 +20,7 @@ const t = useTranslations(lang)
-