first attempt at making a UI (clickdummy)
This commit is contained in:
parent
66c22a915f
commit
b80e3fe4f0
13 changed files with 4775 additions and 0 deletions
24
app/.gitignore
vendored
Normal file
24
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
17
app/astro.config.mjs
Normal file
17
app/astro.config.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// @ts-check
|
||||||
|
import {defineConfig} from 'astro/config';
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
import vue from "@astrojs/vue";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
locales: ["en", "de"],
|
||||||
|
defaultLocale: "en",
|
||||||
|
},
|
||||||
|
integrations: [vue()],
|
||||||
|
});
|
||||||
27
app/package.json
Normal file
27
app/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "dooris-app",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/vue": "^6.0.1",
|
||||||
|
"@lucide/astro": "^1.14.0",
|
||||||
|
"@lucide/vue": "^1.14.0",
|
||||||
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
|
"astro": "^6.2.1",
|
||||||
|
"lucide-solid": "^1.14.0",
|
||||||
|
"tailwindcss": "^4.2.4",
|
||||||
|
"vue": "^3.5.33"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"daisyui": "^5.5.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
4298
app/pnpm-lock.yaml
generated
Normal file
4298
app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
45
app/src/components/Alert.astro
Normal file
45
app/src/components/Alert.astro
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
import {Check, CircleAlert, Info, TriangleAlert} from "@lucide/astro"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
classList: string;
|
||||||
|
color: "info" | "success" | "warning" | "error";
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {id, classList = "", color = "info", title, description} = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
classList,
|
||||||
|
"alert alert-soft w-full",
|
||||||
|
color == "info" && "alert-info",
|
||||||
|
color == "success" && "alert-success",
|
||||||
|
color == "warning" && "alert-warning",
|
||||||
|
color == "error" && "alert-error",
|
||||||
|
]}
|
||||||
|
id={id}
|
||||||
|
role="alert">
|
||||||
|
<slot name="icon">
|
||||||
|
{color === "info" && (
|
||||||
|
<Info class="size-7"/>
|
||||||
|
)}
|
||||||
|
{color === "success" && (
|
||||||
|
<Check class="size-7"/>
|
||||||
|
)}
|
||||||
|
{color === "warning" && (
|
||||||
|
<CircleAlert class="size-7"/>
|
||||||
|
)}
|
||||||
|
{color === "error" && (
|
||||||
|
<TriangleAlert class="size-7"/>
|
||||||
|
)}
|
||||||
|
</slot>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span>{title}</span>
|
||||||
|
<span class="text-sm opacity-90 mt-1" set:html={description}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
61
app/src/components/DoorTemplate.astro
Normal file
61
app/src/components/DoorTemplate.astro
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
import {CircleQuestionMark, Lock, LockOpen, TriangleAlert} from "@lucide/astro"
|
||||||
|
import {getLangFromUrl, useTranslations} from "../i18n/utils"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const t = useTranslations(lang)
|
||||||
|
---
|
||||||
|
|
||||||
|
<template id="template-door">
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
"card flex flex-col w-full group ring",
|
||||||
|
"data-[state=open]:ring-success data-[state=open]:bg-success/5",
|
||||||
|
"data-[state=closed]:ring-error data-[state=closed]:bg-error/5",
|
||||||
|
"data-[state=unknown]:ring-warning data-[state=unknown]:bg-warning/5",
|
||||||
|
"data-[state=moving]:ring-info data-[state=moving]:bg-info/5",
|
||||||
|
]}
|
||||||
|
data-state
|
||||||
|
data-battery
|
||||||
|
data-active
|
||||||
|
data-id
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex max-md:flex-wrap items-center gap-2">
|
||||||
|
<div class="w-full flex justify-between">
|
||||||
|
<h2 class="text-xl font-bold" data-label></h2>
|
||||||
|
<span class="badge badge-error hidden ms-auto group-data-battery:flex whitespace-nowrap">
|
||||||
|
<TriangleAlert class="size-4"/>
|
||||||
|
{t("batteryLow")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden group-data-active:grid grid-cols-2 w-full items-center gap-2 mt-4">
|
||||||
|
<button class="btn btn-outline btn-error">
|
||||||
|
{t("button.close")}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline btn-success">
|
||||||
|
{t("button.open")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="w-full rounded-t-none badge badge-lg badge-success hidden group-data-[state=open]:flex">
|
||||||
|
<LockOpen class="size-4"/>
|
||||||
|
{t("state.open")}
|
||||||
|
</span>
|
||||||
|
<span class="w-full rounded-t-none badge badge-lg badge-error hidden group-data-[state=closed]:flex">
|
||||||
|
<Lock class="size-4"/>
|
||||||
|
{t("state.closed")}
|
||||||
|
</span>
|
||||||
|
<span class="w-full rounded-t-none badge badge-lg badge-warning hidden group-data-[state=unknown]:flex">
|
||||||
|
<CircleQuestionMark class="size-4"/>
|
||||||
|
{t("state.unknown")}
|
||||||
|
</span>
|
||||||
|
<span class="w-full rounded-t-none badge badge-lg badge-info hidden group-data-[state=moving]:flex">
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{t("state.moving")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
41
app/src/i18n/ui.ts
Normal file
41
app/src/i18n/ui.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
export const languages = {
|
||||||
|
en: "English",
|
||||||
|
de: "Deutsch",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultLang = "en"
|
||||||
|
|
||||||
|
export const ui = {
|
||||||
|
en: {
|
||||||
|
"unauthorized.title": "Unauthorized",
|
||||||
|
"unauthorized.description": `To use the locks you have to have the "intern@" status.<br>
|
||||||
|
More infos: <a class='underline' href='https://wiki.hamburg.ccc.de/club:prozesse:aufnahmeprozesse-fuer-berechtigungsgruppen'>CCCHH Wiki</a>`,
|
||||||
|
"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.open": "Open",
|
||||||
|
"state.closed": "Closed",
|
||||||
|
"state.unknown": "Unknown",
|
||||||
|
"state.moving": "Moving",
|
||||||
|
"batteryLow": "Battery low",
|
||||||
|
"button.open": "Open",
|
||||||
|
"button.close": "Close",
|
||||||
|
"login": "Login",
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
"unauthorized.title": "Nicht berechtigt",
|
||||||
|
"unauthorized.description": `Um die Schlösser bedienen zu können, musst du den "intern@" Status haben.<br>
|
||||||
|
Weitere Infos: <a class='underline' href='https://wiki.hamburg.ccc.de/club:prozesse:aufnahmeprozesse-fuer-berechtigungsgruppen'>CCCHH Wiki</a>`,
|
||||||
|
"unauthenticated.title": "Nicht angemeldet",
|
||||||
|
"unauthenticated.description": `Um die Schlösser bedienen zu können, musst du dich mit deiner CCCHH ID anmelden und den "intern@" Status haben.<br>
|
||||||
|
Weitere Infos: <a class='underline' href='https://wiki.hamburg.ccc.de/club:prozesse:aufnahmeprozesse-fuer-berechtigungsgruppen'>CCCHH Wiki</a>`,
|
||||||
|
"state.open": "Offen",
|
||||||
|
"state.closed": "Geschlossen",
|
||||||
|
"state.unknown": "Unbekannt",
|
||||||
|
"state.moving": "In Bewegung",
|
||||||
|
"batteryLow": "Batterie fast leer",
|
||||||
|
"button.open": "Öffnen",
|
||||||
|
"button.close": "Schließen",
|
||||||
|
"login": "Anmelden",
|
||||||
|
},
|
||||||
|
} as const
|
||||||
13
app/src/i18n/utils.ts
Normal file
13
app/src/i18n/utils.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ui, defaultLang } from './ui';
|
||||||
|
|
||||||
|
export function getLangFromUrl(url: URL) {
|
||||||
|
const [, lang] = url.pathname.split('/');
|
||||||
|
if (lang in ui) return lang as keyof typeof ui;
|
||||||
|
return defaultLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslations(lang: keyof typeof ui) {
|
||||||
|
return function t(key: keyof typeof ui[typeof defaultLang]) {
|
||||||
|
return ui[lang][key] || ui[defaultLang][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/src/layouts/Layout.astro
Normal file
42
app/src/layouts/Layout.astro
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
import {UserRound, LogIn, DoorOpen} from "@lucide/astro"
|
||||||
|
import {getLangFromUrl, useTranslations} from "../i18n/utils"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const t = useTranslations(lang)
|
||||||
|
---
|
||||||
|
|
||||||
|
<!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>dooris</title>
|
||||||
|
</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">
|
||||||
|
|
||||||
|
<span class="text-xl font-semibold flex gap-1 items-center">
|
||||||
|
<DoorOpen/>
|
||||||
|
DOORIS
|
||||||
|
</span>
|
||||||
|
<div class="badge badge-soft badge-xl hidden" id="badge-username">
|
||||||
|
<UserRound class="size-5"/>
|
||||||
|
<span id="username"></span>
|
||||||
|
</div>
|
||||||
|
<a href={`/auth/login?next=/${lang}`} class="btn btn-soft btn-info" id="button-login">
|
||||||
|
<LogIn/>
|
||||||
|
{t("login")}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<main class="p-2">
|
||||||
|
<slot/>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
163
app/src/pages/[lang]/index.astro
Normal file
163
app/src/pages/[lang]/index.astro
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
import "../../styles/global.css"
|
||||||
|
import Alert from "../../components/Alert.astro"
|
||||||
|
import DoorTemplate from "../../components/DoorTemplate.astro"
|
||||||
|
import {LogIn, Lock} from "@lucide/astro"
|
||||||
|
import {getLangFromUrl, useTranslations} from "../../i18n/utils"
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
return [
|
||||||
|
{params: {lang: "en"}},
|
||||||
|
{params: {lang: "de"}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const t = useTranslations(lang)
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout username="">
|
||||||
|
<div class="grid place-items-center grid-cols-1 gap-4 max-w-xl mx-auto" id="list">
|
||||||
|
<Alert
|
||||||
|
classList="hidden"
|
||||||
|
id="alert-unauthenticated"
|
||||||
|
color="info"
|
||||||
|
title={t("unauthenticated.title")}
|
||||||
|
description={t("unauthenticated.description")}
|
||||||
|
>
|
||||||
|
<Fragment slot="icon"> <!-- pass table header -->
|
||||||
|
<LogIn class="size-7"/>
|
||||||
|
</Fragment>
|
||||||
|
</Alert>
|
||||||
|
<Alert
|
||||||
|
classList="hidden"
|
||||||
|
id="alert-unauthorized"
|
||||||
|
color="info"
|
||||||
|
title={t("unauthorized.title")}
|
||||||
|
description={t("unauthorized.description")}
|
||||||
|
>
|
||||||
|
<Fragment slot="icon"> <!-- pass table header -->
|
||||||
|
<Lock class="size-7"/>
|
||||||
|
</Fragment>
|
||||||
|
</Alert>
|
||||||
|
<div class="divider w-full my-0" id="alert-divider"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DoorTemplate state="unknown"/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import user from "@lucide/astro/icons/user"
|
||||||
|
|
||||||
|
const list: HTMLDivElement = document.querySelector("#list")!
|
||||||
|
const template: HTMLTemplateElement = document.querySelector("#template-door")!
|
||||||
|
|
||||||
|
type DoorType = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
state: "open" | "closed" | "unknown" | "moving";
|
||||||
|
batteryLow: boolean;
|
||||||
|
}
|
||||||
|
type AuthType = {
|
||||||
|
username: string;
|
||||||
|
authorized: boolean;
|
||||||
|
authenticated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth: AuthType = {
|
||||||
|
username: "user",
|
||||||
|
authorized: true,
|
||||||
|
authenticated: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const doors: Array<DoorType> = [
|
||||||
|
{
|
||||||
|
id: "abcdef",
|
||||||
|
label: "Hauptraum",
|
||||||
|
state: "open",
|
||||||
|
batteryLow: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "12345",
|
||||||
|
label: "Werkstatt",
|
||||||
|
state: "closed",
|
||||||
|
batteryLow: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function setDoorInfo(doorElement: HTMLDivElement, door: DoorType) {
|
||||||
|
const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")!
|
||||||
|
const buttonElements: Array<HTMLButtonElement> = Array.from(doorElement.querySelectorAll("button"))
|
||||||
|
|
||||||
|
doorElement.dataset.id = door.id
|
||||||
|
doorElement.dataset.state = door.state
|
||||||
|
|
||||||
|
if (auth.authenticated && auth.authorized) {
|
||||||
|
doorElement.dataset.active = ""
|
||||||
|
} else {
|
||||||
|
delete doorElement.dataset.active
|
||||||
|
}
|
||||||
|
|
||||||
|
if (door.batteryLow) {
|
||||||
|
doorElement.dataset.battery = ""
|
||||||
|
} else {
|
||||||
|
delete doorElement.dataset.battery
|
||||||
|
}
|
||||||
|
|
||||||
|
labelElement.innerHTML = door.label
|
||||||
|
|
||||||
|
buttonElements.forEach(button => {
|
||||||
|
console.log(button)
|
||||||
|
button.disabled = door.state === "moving"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
for (const door of doors) {
|
||||||
|
let doorElement: HTMLDivElement | null = list.querySelector(`[data-id='${door.id}']`)
|
||||||
|
if (doorElement) {
|
||||||
|
setDoorInfo(doorElement, door)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetElement = document.importNode(template.content, true)
|
||||||
|
doorElement = targetElement.querySelector("[data-state]")!
|
||||||
|
setDoorInfo(doorElement, door)
|
||||||
|
list.appendChild(targetElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertUnauthenticated: HTMLDivElement = document.querySelector("#alert-unauthenticated")!
|
||||||
|
const alertUnauthorized: HTMLDivElement = document.querySelector("#alert-unauthorized")!
|
||||||
|
const alertDivider: HTMLDivElement = document.querySelector("#alert-divider")!
|
||||||
|
const badgeUsername: HTMLDivElement = document.querySelector("#badge-username")!
|
||||||
|
const buttonLogin: HTMLDivElement = document.querySelector("#button-login")!
|
||||||
|
const usernameElement: HTMLSpanElement = badgeUsername.querySelector('#username')!
|
||||||
|
|
||||||
|
alertUnauthenticated.classList.toggle("hidden", auth.authenticated)
|
||||||
|
alertUnauthorized.classList.toggle("hidden", !auth.authenticated || auth.authorized)
|
||||||
|
alertDivider.classList.toggle("hidden", auth.authenticated && auth.authorized)
|
||||||
|
badgeUsername.classList.toggle("hidden", !auth.authenticated)
|
||||||
|
usernameElement.innerHTML = auth.username
|
||||||
|
buttonLogin.classList.toggle("hidden", auth.authenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
doors[0] = {
|
||||||
|
id: "abcdef",
|
||||||
|
label: "Hauptraum",
|
||||||
|
state: "unknown",
|
||||||
|
batteryLow: false,
|
||||||
|
}
|
||||||
|
doors[1] = {
|
||||||
|
id: "12345",
|
||||||
|
label: "Werkstatt",
|
||||||
|
state: "moving",
|
||||||
|
batteryLow: true,
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}, 2000)
|
||||||
|
</script>
|
||||||
|
</Layout>
|
||||||
26
app/src/pages/index.astro
Normal file
26
app/src/pages/index.astro
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
<!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>dooris</title>
|
||||||
|
<script>
|
||||||
|
const userLang = new Intl.Locale(navigator.language).language;
|
||||||
|
const urls: Record<string, string> = {
|
||||||
|
'en': './en',
|
||||||
|
'de': './de',
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = urls[userLang] ?? urls.en;
|
||||||
|
document.location.href = url;
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
5
app/src/styles/global.css
Normal file
5
app/src/styles/global.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes: light --default, dark --prefersdark;
|
||||||
|
}
|
||||||
|
|
||||||
13
app/tsconfig.json
Normal file
13
app/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [
|
||||||
|
".astro/types.d.ts",
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue