Add Chat feature #1
13 changed files with 988 additions and 12 deletions
|
|
@ -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"
|
||||
|
|
|
|||
42
app/pnpm-lock.yaml
generated
42
app/pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
444
app/src/assets/chat.ts
Normal file
444
app/src/assets/chat.ts
Normal file
|
|
@ -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<string>;
|
||||
startsWith?: Array<string>;
|
||||
endsWith?: Array<string>;
|
||||
response: string;
|
||||
errors?: Record<string, string>
|
||||
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 = `
|
||||
<div class="chat-image avatar">
|
||||
<div class="w-10 rounded-full ${lang === 'uwu' ? 'bg-transparent' : 'bg-primary'} flex items-center justify-center">
|
||||
${lang === 'uwu' ? '<span class="text-4xl">🥺</span>' : '<i data-lucide="bot"></i>'}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const chatHeader = `
|
||||
<div class="chat-header">
|
||||
${t("chat.dooris")}
|
||||
</div>
|
||||
`
|
||||
|
||||
const chatAvatarUser = `
|
||||
<div class="chat-image avatar">
|
||||
<div class="w-10 rounded-full ${lang === 'uwu' ? 'bg-transparent' : 'bg-secondary'} flex items-center justify-center">
|
||||
${lang === 'uwu' ? '<span class="text-4xl">🥺</span>' : '<i data-lucide="user-round"></i>'}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const chatHeaderUser = `
|
||||
<div class="chat-header">
|
||||
${t("chat.user")}
|
||||
</div>
|
||||
`
|
||||
|
||||
const chatWait = `
|
||||
<div class="chat chat-start w-full prose" data-waiting>
|
||||
${chatAvatar}
|
||||
${chatHeader}
|
||||
<div class="chat-bubble chat-bubble-primary whitespace-normal min-h-10">
|
||||
<span class="loading loading-dots loading-md"></span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const templateInjectChat = `
|
||||
<details class="dropdown dropdown-top dropdown-end absolute right-2 bottom-2" id="chat-root">
|
||||
<summary class="btn btn-lg btn-circle btn-primary me-2 mb-2">
|
||||
<i data-lucide="stars"></i>
|
||||
</summary>
|
||||
<div class="dropdown-content card h-140 w-90 bg-base-300 p-4 mb-4" style="max-height: min(calc(100vh - 10rem), 50rem); max-width: calc(100vw - 1rem)">
|
||||
<div class="w-full overflow-y-auto h-full prose leading-5 grow pb-4" id="chat"></div>
|
||||
<div class="join mt-auto w-full">
|
||||
<div class="w-full">
|
||||
<label class="input join-item">
|
||||
<input type="text" id="chat-text" />
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-secondary join-item" id="send-chat">
|
||||
<i data-lucide="send"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
`
|
||||
|
||||
type ChatMessage = {
|
||||
type: "bot" | "user",
|
||||
message: string,
|
||||
special?: boolean;
|
||||
id?: string;
|
||||
fresh?: boolean;
|
||||
}
|
||||
|
||||
const chat: Array<ChatMessage> = []
|
||||
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) => `
|
||||
<div class="chat chat-end w-full ps-10">
|
||||
${chatAvatarUser}
|
||||
${chatHeaderUser}
|
||||
<div class="chat-bubble chat-bubble-secondary whitespace-normal" style="word-break: break-word;">${message}</div>
|
||||
</div>`
|
||||
|
||||
const chatBubbleIds: Array<string> = []
|
||||
|
||||
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<string>) {
|
||||
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<string> = []
|
||||
|
||||
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 = `<ul>`
|
||||
for (let door of doors) {
|
||||
if (msg.includes(door.label.toLowerCase())) {
|
||||
matchedDoors.push(door)
|
||||
}
|
||||
doorListElement += `<li>${door.label}</li>`
|
||||
}
|
||||
doorListElement += `</ul>`
|
||||
|
||||
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 => `<span class="badge">${door.label}</span>`)
|
||||
.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)
|
||||
}
|
||||
})
|
||||
86
app/src/assets/chatDict_de.ts
Normal file
86
app/src/assets/chatDict_de.ts
Normal file
|
|
@ -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 😕<br> 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 😕<br> 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!<br>Wie kann ich dir behilflich sein? 🤓",
|
||||
priority: -1,
|
||||
},
|
||||
{
|
||||
response: "Das habe ich leider nicht verstanden 😕",
|
||||
priority: -2,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
export default responses
|
||||
86
app/src/assets/chatDict_en.ts
Normal file
86
app/src/assets/chatDict_en.ts
Normal file
|
|
@ -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 😕<br> 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 😕<br> 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!<br>How can I help you? 🤓",
|
||||
priority: -1,
|
||||
},
|
||||
{
|
||||
response: "Sorry, I didn't understand that 😕",
|
||||
priority: -2,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
export default responses
|
||||
78
app/src/assets/chatDict_uwu.ts
Normal file
78
app/src/assets/chatDict_uwu.ts
Normal file
|
|
@ -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 🤨<br> 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 🤨<br> 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
|
||||
|
|
@ -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<paths>()
|
||||
|
||||
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<DoorType>;
|
||||
auth: AuthType;
|
||||
doorAction: (action: 'unlock' | 'lock', doorId: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
const auth: AuthType = {
|
||||
username: "user",
|
||||
authorized: true,
|
||||
|
|
@ -51,6 +61,9 @@ const timeouts: Record<string, number | null> = {
|
|||
|
||||
const doors: Array<DoorType> = []
|
||||
|
||||
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']")!
|
||||
|
|
|
|||
117
app/src/assets/removeDiacritics.ts
Normal file
117
app/src/assets/removeDiacritics.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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.<br>
|
||||
More infos: <a class='underline' href='https://wiki.hamburg.ccc.de/club:prozesse:aufnahmeprozesse-fuer-berechtigungsgruppen'>CCCHH Wiki</a>`,
|
||||
"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.<br>
|
||||
|
|
@ -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 <span class='text-neutral-content/50 text-xs'>(DOORIS)</span>",
|
||||
|
|
@ -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 <span class='opacity-50 text-xs'>(DOORIS)</span>",
|
||||
"unauthorized.title": "no 🙅",
|
||||
"unauthorized.description": `sowwy, u can not use the portals without the "intern@" status.<br>
|
||||
moar infos: <a class='underline' href='https://wiki.hamburg.ccc.de/club:prozesse:aufnahmeprozesse-fuer-berechtigungsgruppen'>CCCHH Wiki</a>`,
|
||||
"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<br>
|
||||
moar infos: <a class='underline' href='https://wiki.hamburg.ccc.de/club:prozesse:aufnahmeprozesse-fuer-berechtigungsgruppen'>CCCHH Wiki</a>`,
|
||||
"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<br>
|
||||
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
|
||||
|
|
@ -20,7 +20,7 @@ const t = useTranslations(lang)
|
|||
</head>
|
||||
<body class="min-h-screen">
|
||||
<header class="p-2">
|
||||
<div class="navbar bg-base-300 text-neutral-content flex w-full justify-between items-center rounded-xl px-4">
|
||||
<div class="navbar bg-base-300 flex w-full justify-between items-center rounded-xl px-4">
|
||||
|
||||
<h1 class="text-xl font-semibold flex gap-1 items-center">
|
||||
<DoorOpen/>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export async function getStaticPaths() {
|
|||
return [
|
||||
{params: {lang: "en"}},
|
||||
{params: {lang: "de"}},
|
||||
{params: {lang: "uwu"}},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +21,11 @@ const t = useTranslations(lang)
|
|||
|
||||
<Layout>
|
||||
<Fragment slot="head">
|
||||
<script is:inline define:vars={{ lang }}>
|
||||
window.lang = lang;
|
||||
</script>
|
||||
<script src="../../assets/main.ts"/>
|
||||
<script src="../../assets/chat.ts"></script>
|
||||
</Fragment>
|
||||
<div class="grid place-items-center grid-cols-1 gap-4 max-w-xl mx-auto" id="list">
|
||||
<Alert
|
||||
|
|
@ -31,7 +36,7 @@ const t = useTranslations(lang)
|
|||
description={t("loggedOut.description")}
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<a href="/auth/login" class="btn btn-sm btn-warning">
|
||||
<a href={`/auth/login?next=/${lang}`} class="btn btn-sm btn-warning">
|
||||
<LogIn/>
|
||||
{t("login")}
|
||||
</a>
|
||||
|
|
@ -81,5 +86,6 @@ const t = useTranslations(lang)
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DoorTemplate state="unknown"/>
|
||||
</Layout>
|
||||
|
|
|
|||
19
app/src/pages/unlock.astro
Normal file
19
app/src/pages/unlock.astro
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
<meta name="generator" content={Astro.generator}/>
|
||||
<title>unlock dooris</title>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,50 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "daisyui" {
|
||||
themes: light --default, dark --prefersdark;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
background-image: linear-gradient(60deg, transparent 30%, rgba(100%, 100%, 100%, 0.2) 50%, transparent 70%);
|
||||
background-size: 300%;
|
||||
background-position-x: 100%;
|
||||
animation: shimmer 2s infinite linear;
|
||||
box-shadow: var(--color-primary) 0 0 1em 0.2em;
|
||||
border-end-start-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
.shimmer::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
to {
|
||||
background-position-x: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chat-start:has( + .chat-start:not(.hidden)) {
|
||||
padding-bottom: 0.1rem;
|
||||
|
||||
> .chat-image {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
> .chat-bubble {
|
||||
border-end-start-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
> .chat-bubble::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& + .chat-start {
|
||||
padding-top: 0.1rem;
|
||||
|
||||
> .chat-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue