Version 1
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
.nuxt
|
||||
.output
|
||||
.git
|
15
.eslintrc.cjs
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
30
.gitignore
vendored
|
@ -9,3 +9,33 @@ docs/_book
|
|||
# TODO: where does this rule come from?
|
||||
test/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
5
.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120
|
||||
}
|
15
Dockerfile
Normal file
|
@ -0,0 +1,15 @@
|
|||
# build stage
|
||||
FROM node:18-alpine as build-stage
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# production stage
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
|
@ -1,3 +1,5 @@
|
|||
# measuring-stations
|
||||
|
||||
Auto-Reload images and edit url params
|
||||
Auto-Reload images and edit url params
|
||||
|
||||
Test it under [ug.jk-effects.cloud](ug.jk-effects.cloud)
|
1
env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
13
index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>UG-ÖEL Pegel&Messstelle</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
16
nginx.conf
Normal file
|
@ -0,0 +1,16 @@
|
|||
worker_processes 4;
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
|
||||
server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
5537
package-lock.json
generated
Normal file
40
package.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "measuring-stations",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.1.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.12.5",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"npm-run-all2": "^6.1.2",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "~5.4.0",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-vue-devtools": "^7.0.25",
|
||||
"vue-tsc": "^2.0.11"
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/handbook/edge-add-new.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
public/handbook/edge-add.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
public/handbook/edge-secureContent.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
public/handbook/hnd-bayern-link.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
public/handbook/hnd-bayern.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
public/handbook/ug-create.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
public/handbook/ug-created.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
public/handbook/ug-edit.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
public/handbook/ug-edited.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
public/handbook/ug-main.png
Normal file
After Width: | Height: | Size: 21 KiB |
124
src/App.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<h1 class="text-center text-2xl py-2 font-bold">Ständeansicht</h1>
|
||||
<div class="absolute top-2 right-2 flex flex-row gap-2">
|
||||
<span title="Anleitung">
|
||||
<BookOpenIcon class="h-8 w-8 text-black cursor-pointer" @click="handbook = !handbook" />
|
||||
</span>
|
||||
<span title="Ansicht exportieren">
|
||||
<DocumentArrowDownIcon class="h-8 w-8 text-black cursor-pointer" @click="exportData" />
|
||||
</span>
|
||||
<span title="Ansicht importieren">
|
||||
<DocumentArrowUpIcon
|
||||
class="h-8 w-8 text-black cursor-pointer"
|
||||
@click="($refs.fileImport as HTMLInputElement).click()"
|
||||
/><input class="!hidden" type="file" ref="fileImport" @change="importData" />
|
||||
</span>
|
||||
<span title="Einstellungen">
|
||||
<Cog6ToothIcon class="h-8 w-8 text-black cursor-pointer" @click="settings = true" />
|
||||
</span>
|
||||
<span title="neu erstellen">
|
||||
<PlusIcon class="h-8 w-8 text-black cursor-pointer" @click="popup = true" />
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="w-full h-full overflow-y-scroll">
|
||||
<handbookPage v-if="handbook" @close="handbook = false" />
|
||||
<div
|
||||
v-else
|
||||
class="w-full grid auto-rows-auto gap-4 p-4"
|
||||
:class="colcount == 0 ? 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4' : ''"
|
||||
:style="colcount == 0 ? '' : `grid-template-columns: repeat(${colcount}, minmax(0, 1fr));`"
|
||||
>
|
||||
<statistic v-for="item in items" :key="item.id" :id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="popup"
|
||||
@click="popup = false"
|
||||
class="absolute h-full w-full bg-black/40 z-20 flex justify-center items-center"
|
||||
>
|
||||
<form
|
||||
ref="form"
|
||||
@submit.prevent="submitForm"
|
||||
@click.stop
|
||||
class="relative w-96 h-auto bg-white flex flex-col gap-2 rounded-md p-4"
|
||||
>
|
||||
<XMarkIcon class="h-5 w-5 text-black cursor-pointer absolute top-2 right-2" @click="popup = false" />
|
||||
<h1 class="text-center text-lg">neuen Eintrag</h1>
|
||||
<div class="flex flex-col">
|
||||
<label for="title">Titel</label>
|
||||
<input id="title" type="text" class="w-full" required />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="img">Bildlink</label>
|
||||
<input id="img" type="text" class="w-full" required />
|
||||
</div>
|
||||
<button primary>erstellen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="settings"
|
||||
@click="settings = false"
|
||||
class="absolute h-full w-full bg-black/40 z-20 flex justify-center items-center"
|
||||
>
|
||||
<form
|
||||
ref="formsettings"
|
||||
@submit.prevent="submitFormSettings"
|
||||
@click.stop
|
||||
class="relative w-96 h-auto bg-white flex flex-col gap-2 rounded-md p-4"
|
||||
>
|
||||
<XMarkIcon class="h-5 w-5 text-black cursor-pointer absolute top-2 right-2" @click="settings = false" />
|
||||
<h1 class="text-center text-lg">Einstellungen</h1>
|
||||
<div class="flex flex-col">
|
||||
<label for="colcount">Spaltenzahl (0 = automatisch)</label>
|
||||
<input id="colcount" type="number" class="w-full" min="0" required :value="colcount" />
|
||||
</div>
|
||||
<button primary>speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import statistic from "./components/statistic.vue";
|
||||
import handbookPage from "./components/handbook.vue";
|
||||
import { useDataStorage } from "./stores/dataStorage";
|
||||
import { PlusIcon, Cog6ToothIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
import { DocumentArrowDownIcon, DocumentArrowUpIcon, BookOpenIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
popup: false,
|
||||
settings: false,
|
||||
handbook: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useDataStorage, ["items", "colcount"]),
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useDataStorage, ["create", "load", "saveSettings", "exportData", "importData"]),
|
||||
submitForm(form: any) {
|
||||
let data = form.target.elements;
|
||||
this.create(data.title.value, data.img.value);
|
||||
(this.$refs.form as HTMLFormElement).reset();
|
||||
this.popup = false;
|
||||
},
|
||||
submitFormSettings(form: any) {
|
||||
let data = form.target.elements;
|
||||
this.saveSettings({ colcount: parseInt(data.colcount.value) });
|
||||
(this.$refs.formsettings as HTMLFormElement).reset();
|
||||
this.settings = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
121
src/components/handbook.vue
Normal file
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="relative flex flex-col justify-center w-full min-h-full py-5 px-7">
|
||||
<XMarkIcon class="absolute top-2 right-2 h-6 w-6 text-black cursor-pointer" @click="$emit('close')" />
|
||||
<h1 class="text-center text-xl">Anleitung</h1>
|
||||
<br />
|
||||
<p class="text-center">
|
||||
Diese Seite ist gedacht zur gruppierten/gesammelten Darstellung von Bildern.
|
||||
<br />Sich mit der Zeit verändernde Bilder werden alle 5min aktualisiert.
|
||||
</p>
|
||||
<br />
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 justify-items-end auto-cols-max">
|
||||
<img src="/handbook/hnd-bayern.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">HND-Bayern</h1>
|
||||
<p>Station des HND auswählen</p>
|
||||
<a href="https://www.hnd.bayern.de/pegel/meldestufen/donau_bis_kelheim">
|
||||
https://www.hnd.bayern.de/pegel/meldestufen/donau_bis_kelheim
|
||||
</a>
|
||||
</div>
|
||||
<img src="/handbook/hnd-bayern-link.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">HND-Bayern Bildlink</h1>
|
||||
<p>
|
||||
Wähle per rechtsklick auf das Bild den Meüpunk <b>Bildlink kopieren</b> aus, damit dieser in die Ansicht
|
||||
eingefügt werden kann.
|
||||
</p>
|
||||
</div>
|
||||
<img src="/handbook/ug-main.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">Ständeansicht</h1>
|
||||
<p>gehe zurück auf die Startseite der Ständeansicht</p>
|
||||
<p>Nutze das plus oben rechts, um eine neues Feld hinzuzufügen.</p>
|
||||
</div>
|
||||
<img src="/handbook/ug-create.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">neue Station erstellen</h1>
|
||||
<p>Füge den Link unter Bildlink ein und füge eine Bezeichnung für die Ansicht hinzu.</p>
|
||||
</div>
|
||||
<img src="/handbook/ug-created.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">Übersicht</h1>
|
||||
<p>Die neu erstellte Station erscheint am Ende</p>
|
||||
</div>
|
||||
<img src="/handbook/ug-edit.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">Funktionen pro Station</h1>
|
||||
<p>
|
||||
Die stationen können einzeln bearbeitet werden. Hier können einzelne Link-Werte separat bearbeitet werden.
|
||||
</p>
|
||||
<p class="flex flex-row gap-2">
|
||||
<XMarkIcon class="h-8 w-8 text-black" /> Eintrag löschen (bestätigung durch popup)
|
||||
</p>
|
||||
<p class="flex flex-row gap-2"><PencilIcon class="h-8 w-8 text-black" /> Eintrag bearbeiten</p>
|
||||
<p class="flex flex-row gap-2">
|
||||
<ArrowsPointingOutIcon class="h-8 w-8 text-black" />
|
||||
<ArrowsPointingInIcon class="h-8 w-8 text-black" />
|
||||
Eintrag auf Vollbild stellen bzw zurück
|
||||
</p>
|
||||
<p class="flex flex-row gap-2">
|
||||
<DocumentArrowUpIcon class="h-8 w-8 text-black" />
|
||||
Ansicht von Datei importieren
|
||||
</p>
|
||||
<p class="flex flex-row gap-2">
|
||||
<DocumentArrowDownIcon class="h-8 w-8 text-black" />
|
||||
Ansicht als Datei exportieren
|
||||
</p>
|
||||
</div>
|
||||
<img src="/handbook/ug-edited.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">Änderung und hinzufügen von Daten</h1>
|
||||
<p>Parameter können einzeln in diesem PopUp geändert werden.</p>
|
||||
<p>
|
||||
Sollten einzelne weitere Werte benötigt werden, können diese in dem Bildlink feld per
|
||||
<b>&bezeichner=wert</b> oder <b>?bezeichner=wert</b> hinzugefügt werden
|
||||
</p>
|
||||
<p>vhs: vorhersage</p>
|
||||
<p>days: Zeitraum</p>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<details>
|
||||
<summary class="cursor-pointer">Wenn Bilder nicht angezeigt werden können</summary>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 px-7 gap-4 justify-items-end">
|
||||
<img src="/handbook/edge-secureContent.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">Insecure Werte zulassen</h1>
|
||||
<p>
|
||||
Bilder können möglicherweise nicht angezeigt werden, da Inhalte per http auf einer https seite nicht geladen
|
||||
werden können. <br />
|
||||
<b>Den Link nicht auf https ändern</b> <br />
|
||||
Damit die Bilder dennoch geladen werden können, muss dies im browser eingestellt werden.
|
||||
</p>
|
||||
<a href="edge://settings/content/insecureContent">edge://settings/content/insecureContent</a>
|
||||
<a href="chrome://settings/content/insecureContent">chrome://settings/content/insecureContent</a>
|
||||
</div>
|
||||
<img src="/handbook/edge-add.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">Webseite zulassen</h1>
|
||||
<p>öffne das popup, um eine webseite hinzuzufügen</p>
|
||||
</div>
|
||||
<img src="/handbook/edge-add-new.png" class="max-h-80 w-2/3" />
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-start">
|
||||
<h1 class="text-lg">Ansicht freigeben</h1>
|
||||
<p>Füge als url <b>ug.jk-effects.cloud</b> hinzu</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { XMarkIcon, PencilIcon, ArrowsPointingOutIcon, ArrowsPointingInIcon } from "@heroicons/vue/24/solid";
|
||||
import { DocumentArrowDownIcon, DocumentArrowUpIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
emits: ["close"],
|
||||
});
|
||||
</script>
|
157
src/components/statistic.vue
Normal file
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<div
|
||||
class="border-2 border-gray-300 bg-white rounded-md overflow-hidden"
|
||||
:class="zoom ? 'fixed inset-0 w-full h-full z-10' : 'relative'"
|
||||
>
|
||||
<div class="absolute top-1 right-1 flex flex-row gap-2">
|
||||
<span title="löschen">
|
||||
<XMarkIcon class="h-6 w-6 text-black cursor-pointer" @click="remove = true" />
|
||||
</span>
|
||||
<span title="bearbeiten"><PencilIcon class="h-6 w-6 text-black cursor-pointer" @click="popup = true" /></span>
|
||||
<span v-if="zoom" title="kleiner">
|
||||
<ArrowsPointingInIcon class="h-6 w-6 text-black cursor-pointer" @click="zoom = false" />
|
||||
</span>
|
||||
<span v-else title="größer">
|
||||
<ArrowsPointingOutIcon class="h-6 w-6 text-black cursor-pointer" @click="zoom = true" />
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="pl-1 text-xl" :class="zoom ? 'text-center' : ''">{{ local.title }}</h1>
|
||||
<div class="flex flex-col justify-around items-center w-full" :class="zoom ? 'h-full' : ' min-h-fit'">
|
||||
<img ref="img" :src="local.img" class="w-full h-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="popup"
|
||||
@click="popup = false"
|
||||
class="absolute inset-0 h-full w-full bg-black/40 z-20 flex justify-center items-center"
|
||||
>
|
||||
<form
|
||||
@submit.prevent="submitForm"
|
||||
@click.stop
|
||||
class="relative w-96 h-auto bg-white flex flex-col gap-2 rounded-md p-4"
|
||||
>
|
||||
<XMarkIcon class="h-5 w-5 text-black cursor-pointer absolute top-2 right-2" @click="popup = false" />
|
||||
<h1 class="text-center text-lg">neuen Eintrag</h1>
|
||||
<div class="flex flex-col">
|
||||
<label for="title">Titel</label>
|
||||
<input id="title" type="text" class="w-full" required :value="local.title" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="img">Bildlink</label>
|
||||
<input
|
||||
id="img"
|
||||
type="text"
|
||||
class="w-full"
|
||||
required
|
||||
:value="local.img"
|
||||
@change="(e) => (imgurl = (e.target as HTMLInputElement)?.value ?? '')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col border-l-2 pl-2 h-56 overflow-y-auto">
|
||||
<p>URL (überschreibt obige url)</p>
|
||||
<div class="flex flex-col">
|
||||
<label for="origin">origin</label>
|
||||
<input id="origin" type="text" class="w-full" required :value="url.origin" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="path">path</label>
|
||||
<input id="path" type="text" class="w-full" required :value="url.path" />
|
||||
</div>
|
||||
<div v-for="se in url.query.entries()" :key="se[0]" class="flex flex-col">
|
||||
<label :for="se[0]">{{ se[0] }}</label>
|
||||
<input :id="se[0]" type="text" class="w-full" required :value="se[1]" />
|
||||
</div>
|
||||
</div>
|
||||
<button primary>aktualisieren</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="remove"
|
||||
@click="remove = false"
|
||||
class="absolute inset-0 h-full w-full bg-black/40 z-20 flex justify-center items-center"
|
||||
>
|
||||
<form
|
||||
@submit.prevent="deleteForm"
|
||||
@click.stop
|
||||
class="relative w-96 h-auto bg-white flex flex-col gap-2 rounded-md p-4"
|
||||
>
|
||||
<XMarkIcon class="h-5 w-5 text-black cursor-pointer absolute top-2 right-2" @click="remove = false" />
|
||||
<button secondary @click="remove = false" type="button">abbrechen</button>
|
||||
<button primary type="submit">löschen?</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useDataStorage } from "@/stores/dataStorage";
|
||||
import { XMarkIcon, PencilIcon, ArrowsPointingOutIcon, ArrowsPointingInIcon } from "@heroicons/vue/24/solid";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
id: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
interval: null as any,
|
||||
popup: false,
|
||||
imgurl: "",
|
||||
zoom: false,
|
||||
remove: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.imgurl = this.local.img;
|
||||
this.interval = setInterval(
|
||||
() => {
|
||||
let newUrl = "";
|
||||
if (this.local.img.includes("?")) newUrl = this.local.img + "&time=";
|
||||
else newUrl = this.local.img + "?time=";
|
||||
(this.$refs.img as HTMLImageElement).src = newUrl + Date.now();
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
computed: {
|
||||
...mapState(useDataStorage, ["item"]),
|
||||
local() {
|
||||
return this.item(this.id ?? "");
|
||||
},
|
||||
url() {
|
||||
let url = new URL(this.imgurl);
|
||||
return {
|
||||
query: url.searchParams,
|
||||
origin: url.origin,
|
||||
path: url.pathname,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useDataStorage, ["delete", "update"]),
|
||||
submitForm(form: any) {
|
||||
let data = form.target.elements;
|
||||
let url = data.origin.value + (data.path.value.startsWith("/") ? data.path.value : "/" + data.path.value);
|
||||
let index = 0;
|
||||
for (let key of new URL(this.imgurl).searchParams.keys()) {
|
||||
if (index == 0) url += "?";
|
||||
else url += "&";
|
||||
url += key + "=" + data[key].value;
|
||||
index++;
|
||||
}
|
||||
this.update(this.id ?? "", data.title.value, url);
|
||||
this.popup = false;
|
||||
this.imgurl = url;
|
||||
},
|
||||
deleteForm() {
|
||||
this.delete(this.id ?? "");
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
61
src/main.css
Normal file
|
@ -0,0 +1,61 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #c9c9c9 transparent;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent; /*f1f1f1;*/
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #c9c9c9;
|
||||
border-radius: 12px;
|
||||
border: 0px solid #ffffff;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply h-full w-screen overflow-hidden bg-white;
|
||||
height: 100svh;
|
||||
}
|
||||
|
||||
#app {
|
||||
@apply w-full h-full overflow-hidden flex flex-col;
|
||||
}
|
||||
|
||||
button:not([headlessui]),
|
||||
a[button]:not([headlessui]) {
|
||||
@apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0;
|
||||
}
|
||||
|
||||
button[primary]:not([primary="false"]),
|
||||
a[button][primary]:not([primary="false"]) {
|
||||
@apply border border-transparent text-white bg-gray-500 hover:bg-gray-400;
|
||||
}
|
||||
|
||||
button[primary-outline]:not([primary-outline="false"]),
|
||||
a[button][primary-outline]:not([primary-outline="false"]) {
|
||||
@apply border-2 border-red-400 text-black hover:bg-indigo-100;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@apply opacity-75 pointer-events-none;
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]),
|
||||
textarea {
|
||||
@apply rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none;
|
||||
/** focus:ring-indigo-500 focus:border-indigo-500 */
|
||||
}
|
11
src/main.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import "./main.css";
|
||||
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
|
||||
app.mount("#app");
|
86
src/stores/dataStorage.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { ref, computed } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export interface item {
|
||||
id: string;
|
||||
img: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface settings {
|
||||
colcount: number;
|
||||
}
|
||||
|
||||
export const useDataStorage = defineStore("data", {
|
||||
state: () => {
|
||||
return {
|
||||
items: [] as Array<item>,
|
||||
colcount: 0 as number,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
item: (state) => (item: string) => state.items.find((elem) => elem.id == item) ?? ({} as item),
|
||||
},
|
||||
actions: {
|
||||
load() {
|
||||
let store = localStorage.getItem("staende");
|
||||
let setting = localStorage.getItem("settings");
|
||||
if (store) this.items = JSON.parse(store);
|
||||
if (setting) this.colcount = JSON.parse(setting).colcount ?? 0;
|
||||
},
|
||||
create(title: string, img: string) {
|
||||
this.items.push({
|
||||
id: Date.now().toString(16),
|
||||
title: title,
|
||||
img: img,
|
||||
});
|
||||
let string = JSON.stringify(this.items);
|
||||
localStorage.setItem("staende", string);
|
||||
},
|
||||
update(id: string, title: string, img: string) {
|
||||
let item = this.items.find((elem) => elem.id == id);
|
||||
if (item) {
|
||||
item.img = img;
|
||||
item.title = title;
|
||||
let string = JSON.stringify(this.items);
|
||||
localStorage.setItem("staende", string);
|
||||
}
|
||||
},
|
||||
delete(id: string) {
|
||||
let index = this.items.findIndex((elem) => elem.id == id);
|
||||
if (index != -1) this.items.splice(index, 1);
|
||||
let string = JSON.stringify(this.items);
|
||||
localStorage.setItem("staende", string);
|
||||
},
|
||||
saveSettings(data: settings) {
|
||||
this.colcount = data.colcount;
|
||||
localStorage.setItem("settings", JSON.stringify(data));
|
||||
},
|
||||
importData(event: Event) {
|
||||
var file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result == "string" && reader.result.startsWith("FORMAT-UG-ÖEL:")) {
|
||||
var result = reader.result.replace("FORMAT-UG-ÖEL:", "");
|
||||
this.items = JSON.parse(result);
|
||||
localStorage.setItem("staende", result);
|
||||
} else {
|
||||
alert("Datei entspricht nicht dem Import Format");
|
||||
}
|
||||
};
|
||||
|
||||
if (file) {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
},
|
||||
exportData() {
|
||||
var data = "FORMAT-UG-ÖEL:" + localStorage.getItem("staende");
|
||||
var downloadableLink = document.createElement("a");
|
||||
downloadableLink.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(data ?? ""));
|
||||
downloadableLink.download = "UG_OeEL_Pegel_Messstelle.txt";
|
||||
downloadableLink.click();
|
||||
},
|
||||
},
|
||||
});
|
8
tailwind.config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
14
tsconfig.app.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
tsconfig.node.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
15
vite.config.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import VueDevTools from "vite-plugin-vue-devtools";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), VueDevTools()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|