Version 1

This commit is contained in:
Julian Krauser 2024-08-15 11:42:08 +02:00
parent 7af8d74f29
commit ac29821ba4
34 changed files with 6312 additions and 1 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
.nuxt
.output
.git

15
.eslintrc.cjs Normal file
View 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
View file

@ -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
View file

@ -0,0 +1,5 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 120
}

15
Dockerfile Normal file
View 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;"]

View file

@ -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
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

40
package.json Normal file
View 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
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/handbook/ug-edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
public/handbook/ug-main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

124
src/App.vue Normal file
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View 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
View 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)),
},
},
});