diff --git a/tatort/.eslintignore b/tatort/.eslintignore
new file mode 100644
index 0000000..3897265
--- /dev/null
+++ b/tatort/.eslintignore
@@ -0,0 +1,13 @@
+.DS_Store
+node_modules
+/build
+/.svelte-kit
+/package
+.env
+.env.*
+!.env.example
+
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
diff --git a/tatort/.eslintrc.cjs b/tatort/.eslintrc.cjs
new file mode 100644
index 0000000..45450fd
--- /dev/null
+++ b/tatort/.eslintrc.cjs
@@ -0,0 +1,39 @@
+module.exports = {
+ root: true,
+ extends: ['eslint:recommended', 'prettier'],
+ plugins: ['svelte3'],
+ overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2022
+ },
+ env: {
+ browser: true,
+ es2017: true,
+ node: true
+ },
+ settings: {
+ 'svelte3/ignore-warnings': (warning) => warning.code.startsWith('a11y-')
+ },
+ rules: {
+ 'constructor-super': 'error',
+ 'linebreak-style': ['error', 'unix'],
+ quotes: ['error', 'single', { avoidEscape: true }],
+ semi: ['error', 'always'],
+ 'max-len': ['warn', { code: 132, ignoreComments: true }],
+ 'no-console': 'warn',
+ 'no-else-return': ['error', { allowElseIf: false }],
+ 'no-extra-boolean-cast': 'error',
+ 'no-extra-bind': 'error',
+ 'no-implicit-coercion': 'error',
+ 'no-multi-spaces': 'warn',
+ 'no-redeclare': 'error',
+ 'no-self-assign': 'error',
+ 'no-undef-init': 'error',
+ 'prefer-template': 'warn',
+ 'sort-imports': 'off',
+ 'jsx-a11y/no-autofocus': 'off',
+ 'jsx-a11y/click-events-have-key-events': 0,
+ 'a11y-click-events-have-key-events': 0
+ }
+};
diff --git a/tatort/.gitignore b/tatort/.gitignore
new file mode 100644
index 0000000..7809ee6
--- /dev/null
+++ b/tatort/.gitignore
@@ -0,0 +1,12 @@
+.DS_Store
+node_modules
+/build
+/.svelte-kit
+/package
+.env
+.env.*
+!.env.example
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+config.json
+package-lock.json
diff --git a/tatort/.npmrc b/tatort/.npmrc
new file mode 100644
index 0000000..0c05da4
--- /dev/null
+++ b/tatort/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+resolution-mode=highest
diff --git a/tatort/.prettierignore b/tatort/.prettierignore
new file mode 100644
index 0000000..3897265
--- /dev/null
+++ b/tatort/.prettierignore
@@ -0,0 +1,13 @@
+.DS_Store
+node_modules
+/build
+/.svelte-kit
+/package
+.env
+.env.*
+!.env.example
+
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
diff --git a/tatort/.prettierrc b/tatort/.prettierrc
new file mode 100644
index 0000000..95d17b4
--- /dev/null
+++ b/tatort/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "useTabs": true,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "printWidth": 100,
+ "plugins": ["prettier-plugin-svelte"],
+ "pluginSearchDirs": ["."],
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
+ "svelteSortOrder": "options-styles-scripts-markup"
+}
diff --git a/tatort/README.md b/tatort/README.md
new file mode 100644
index 0000000..5c91169
--- /dev/null
+++ b/tatort/README.md
@@ -0,0 +1,38 @@
+# create-svelte
+
+Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
+
+## Creating a project
+
+If you're seeing this, you've probably already done this step. Congrats!
+
+```bash
+# create a new project in the current directory
+npm create svelte@latest
+
+# create a new project in my-app
+npm create svelte@latest my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+
+```bash
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+To create a production version of your app:
+
+```bash
+npm run build
+```
+
+You can preview the production build with `npm run preview`.
+
+> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
diff --git a/tatort/config.json.sample b/tatort/config.json.sample
new file mode 100644
index 0000000..6acdf2c
--- /dev/null
+++ b/tatort/config.json.sample
@@ -0,0 +1,17 @@
+{
+ "minio": {
+ "endPoint": "s3.home.arpa",
+ "port": 9000,
+ "useSSL": true,
+ "accessKey": "tMhLrfog47lMm0HZ",
+ "secretKey": "v0e32UlzMsTKMpnizNDWAMbQ9Gk7ZTeV"
+ },
+ "jwt": {
+ "secret": "@S2!q@@wXz$dCQ8JoVsHLpzaJ6JCfB",
+ "expiresIn": 3600
+ },
+ "auth": {
+ "admin": { "password": "admin", "admin": true },
+ "teimar70": { "password": "test", "admin": false }
+ }
+}
diff --git a/tatort/daten/1.glb b/tatort/daten/1.glb
new file mode 100644
index 0000000..694cdba
Binary files /dev/null and b/tatort/daten/1.glb differ
diff --git a/tatort/daten/2.glb b/tatort/daten/2.glb
new file mode 100644
index 0000000..5f6099f
Binary files /dev/null and b/tatort/daten/2.glb differ
diff --git a/tatort/daten/3.glb b/tatort/daten/3.glb
new file mode 100644
index 0000000..27f19d8
Binary files /dev/null and b/tatort/daten/3.glb differ
diff --git a/tatort/daten/4.glb b/tatort/daten/4.glb
new file mode 100644
index 0000000..88ddabb
Binary files /dev/null and b/tatort/daten/4.glb differ
diff --git a/tatort/daten/5.glb b/tatort/daten/5.glb
new file mode 100644
index 0000000..5527dfe
Binary files /dev/null and b/tatort/daten/5.glb differ
diff --git a/tatort/jsconfig.json b/tatort/jsconfig.json
new file mode 100644
index 0000000..fe45e13
--- /dev/null
+++ b/tatort/jsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true
+ }
+ // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
+ //
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+ // from the referenced tsconfig.json - TypeScript does not merge them in
+}
diff --git a/tatort/package.json b/tatort/package.json
new file mode 100644
index 0000000..b21d7eb
--- /dev/null
+++ b/tatort/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "tatort",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
+ "test": "playwright test",
+ "test:unit": "vitest",
+ "lint": "prettier --plugin-search-dir . --check . && eslint .",
+ "format": "prettier --plugin-search-dir . --write ."
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.28.1",
+ "@sveltejs/adapter-auto": "^2.0.0",
+ "@sveltejs/kit": "^1.5.0",
+ "eslint": "^8.28.0",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-svelte3": "^4.0.0",
+ "prettier": "^2.8.0",
+ "prettier-plugin-svelte": "^2.8.1",
+ "svelte": "^3.54.0",
+ "svelte-check": "^3.0.1",
+ "typescript": "^5.0.0",
+ "vite": "^4.3.0",
+ "vitest": "^0.25.3"
+ },
+ "type": "module",
+ "dependencies": {
+ "@google/model-viewer": "^3.1.1",
+ "@sveltejs/adapter-node": "^1.2.4",
+ "@tailwindcss/forms": "^0.5.3",
+ "autoprefixer": "^10.4.14",
+ "jsonwebtoken": "^9.0.0",
+ "minio": "^7.1.1",
+ "postcss": "^8.4.24",
+ "svelte-cubed": "^0.2.1",
+ "tailwindcss": "^3.3.2",
+ "three": "^0.151.2"
+ }
+}
diff --git a/tatort/playwright.config.js b/tatort/playwright.config.js
new file mode 100644
index 0000000..a43c8c0
--- /dev/null
+++ b/tatort/playwright.config.js
@@ -0,0 +1,11 @@
+/** @type {import('@playwright/test').PlaywrightTestConfig} */
+const config = {
+ webServer: {
+ command: 'npm run build && npm run preview',
+ port: 4173
+ },
+ testDir: 'tests',
+ testMatch: /(.+\.)?(test|spec)\.[jt]s/
+};
+
+export default config;
diff --git a/tatort/postcss.config.cjs b/tatort/postcss.config.cjs
new file mode 100644
index 0000000..054c147
--- /dev/null
+++ b/tatort/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {}
+ }
+};
diff --git a/tatort/postcss.config.js b/tatort/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/tatort/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/tatort/src/app.css b/tatort/src/app.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/tatort/src/app.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/tatort/src/app.d.ts b/tatort/src/app.d.ts
new file mode 100644
index 0000000..5ab1fbb
--- /dev/null
+++ b/tatort/src/app.d.ts
@@ -0,0 +1,14 @@
+// See https://kit.svelte.dev/docs/types#app
+// for information about these interfaces
+declare global {
+ namespace App {
+ // interface Error {}
+ interface Locals {
+ user: any
+ }
+ // interface PageData {}
+ // interface Platform {}
+ }
+}
+
+export { };
diff --git a/tatort/src/app.html b/tatort/src/app.html
new file mode 100644
index 0000000..6302e29
--- /dev/null
+++ b/tatort/src/app.html
@@ -0,0 +1,14 @@
+
+
+
+ Tatort
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/tatort/src/hooks.server.js b/tatort/src/hooks.server.js
new file mode 100644
index 0000000..19b1bcc
--- /dev/null
+++ b/tatort/src/hooks.server.js
@@ -0,0 +1,16 @@
+import { decryptToken } from '$lib/auth';
+
+/** @type {import('@sveltejs/kit').Handle} */
+export async function handle({ event, resolve }) {
+ const jwt = event.cookies.get('session');
+ try {
+ if (jwt) {
+ event.locals.user = decryptToken(jwt);
+ return resolve(event);
+ }
+ } catch (err) {
+ await event.cookies.delete('session');
+ event.locals.user = null;
+ }
+ return resolve(event);
+}
diff --git a/tatort/src/index.test.js b/tatort/src/index.test.js
new file mode 100644
index 0000000..e07cbbd
--- /dev/null
+++ b/tatort/src/index.test.js
@@ -0,0 +1,7 @@
+import { describe, it, expect } from 'vitest';
+
+describe('sum test', () => {
+ it('adds 1 + 2 to equal 3', () => {
+ expect(1 + 2).toBe(3);
+ });
+});
diff --git a/tatort/src/lib/auth.js b/tatort/src/lib/auth.js
new file mode 100644
index 0000000..19d290a
--- /dev/null
+++ b/tatort/src/lib/auth.js
@@ -0,0 +1,29 @@
+import jwt from 'jsonwebtoken';
+
+import config from '$lib/config';
+
+const SECRET = config.jwt.secret;
+const EXPIRES_IN = config.jwt.expiresIn;
+
+const AUTH = config.auth;
+
+export function createToken(userData) {
+ return jwt.sign(userData, SECRET, { expiresIn: EXPIRES_IN });
+}
+
+export function decryptToken(token) {
+ return jwt.verify(token, SECRET);
+}
+
+export function authenticate(user, pass) {
+ let userData = null;
+
+ if (AUTH[user]) {
+ const { password, ...data } = AUTH[user];
+ if (password && password === pass) userData = data;
+ }
+
+ if (userData == null) return null;
+
+ return createToken({ id: user, ...userData });
+}
diff --git a/tatort/src/lib/components/ui/Alert.svelte b/tatort/src/lib/components/ui/Alert.svelte
new file mode 100644
index 0000000..d461847
--- /dev/null
+++ b/tatort/src/lib/components/ui/Alert.svelte
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
diff --git a/tatort/src/lib/components/ui/Button.svelte b/tatort/src/lib/components/ui/Button.svelte
new file mode 100644
index 0000000..dd05a7b
--- /dev/null
+++ b/tatort/src/lib/components/ui/Button.svelte
@@ -0,0 +1,202 @@
+
+
+
+
+{#if href}
+
+
+{:else}
+
+{/if}
diff --git a/tatort/src/lib/components/ui/DeleteIconButton.svelte b/tatort/src/lib/components/ui/DeleteIconButton.svelte
new file mode 100644
index 0000000..e2ef84c
--- /dev/null
+++ b/tatort/src/lib/components/ui/DeleteIconButton.svelte
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+ {#if active}
+
+
+
+ {/if}
+
diff --git a/tatort/src/lib/components/ui/Modal/Modal.svelte b/tatort/src/lib/components/ui/Modal/Modal.svelte
new file mode 100644
index 0000000..bdf0705
--- /dev/null
+++ b/tatort/src/lib/components/ui/Modal/Modal.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
+
diff --git a/tatort/src/lib/components/ui/Modal/ModalContent.svelte b/tatort/src/lib/components/ui/Modal/ModalContent.svelte
new file mode 100644
index 0000000..d791e1a
--- /dev/null
+++ b/tatort/src/lib/components/ui/Modal/ModalContent.svelte
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/tatort/src/lib/components/ui/Modal/ModalFooter.svelte b/tatort/src/lib/components/ui/Modal/ModalFooter.svelte
new file mode 100644
index 0000000..9a6eb5d
--- /dev/null
+++ b/tatort/src/lib/components/ui/Modal/ModalFooter.svelte
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tatort/src/lib/components/ui/Modal/ModalTitle.svelte b/tatort/src/lib/components/ui/Modal/ModalTitle.svelte
new file mode 100644
index 0000000..b39036b
--- /dev/null
+++ b/tatort/src/lib/components/ui/Modal/ModalTitle.svelte
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tatort/src/lib/components/ui/Notification.svelte b/tatort/src/lib/components/ui/Notification.svelte
new file mode 100644
index 0000000..247e902
--- /dev/null
+++ b/tatort/src/lib/components/ui/Notification.svelte
@@ -0,0 +1,87 @@
+
+
+
diff --git a/tatort/src/lib/components/ui/Panel.svelte b/tatort/src/lib/components/ui/Panel.svelte
new file mode 100644
index 0000000..ab1a807
--- /dev/null
+++ b/tatort/src/lib/components/ui/Panel.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/tatort/src/lib/components/ui/Select.svelte b/tatort/src/lib/components/ui/Select.svelte
new file mode 100644
index 0000000..d5752f3
--- /dev/null
+++ b/tatort/src/lib/components/ui/Select.svelte
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+ {#each options as option, index}
+ - {
+ selectOnChange(index);
+ }}
+ id="listbox-item-0"
+ role="option"
+ class="group relative cursor-default select-none py-2 pl-3 pr-9 text-gray-900 hover:bg-blue-500 hover:text-white"
+ >
+
+ {#if option.img}
+

+ {/if}
+
+
+ {option.title}
+
+
+ {#if selected === index}
+
+
+
+ {/if}
+
+ {/each}
+
+
+
+
diff --git a/tatort/src/lib/config.js b/tatort/src/lib/config.js
new file mode 100644
index 0000000..5c34af3
--- /dev/null
+++ b/tatort/src/lib/config.js
@@ -0,0 +1,3 @@
+import { readFileSync } from 'fs';
+
+export default JSON.parse(readFileSync('./config.json'));
diff --git a/tatort/src/lib/helper/caseNumberOccupied.js b/tatort/src/lib/helper/caseNumberOccupied.js
new file mode 100644
index 0000000..50062e8
--- /dev/null
+++ b/tatort/src/lib/helper/caseNumberOccupied.js
@@ -0,0 +1,22 @@
+import { client } from '$lib/minio';
+
+/**
+ * Check if caseNumber is used
+ * @param {string} caseNumber
+ * @returns {Promise {
+ let stream = client.listObjectsV2('tatort', prefix, false, '');
+ stream.on('data', () => {
+ stream.destroy();
+ resolve(true);
+ });
+ stream.on('end', () => {
+ resolve(false);
+ });
+ });
+
+ return promise;
+}
diff --git a/tatort/src/lib/helper/shortenFileSize.js b/tatort/src/lib/helper/shortenFileSize.js
new file mode 100644
index 0000000..1525dfb
--- /dev/null
+++ b/tatort/src/lib/helper/shortenFileSize.js
@@ -0,0 +1,22 @@
+const KILO = 1024;
+const MEGA = KILO * KILO;
+const GIGA = MEGA * KILO;
+
+/**
+ * Shortens the size in bytes
+ * @param {number} size
+ * @returns{string}
+ */
+export default function shortenFileSize(size) {
+ const giga = Math.floor(size / GIGA);
+ let remainder = size % GIGA;
+ const mega = Math.floor(remainder / MEGA);
+ remainder %= MEGA;
+ const kilo = Math.floor(remainder / KILO);
+ remainder %= KILO;
+
+ if (giga > 0) return `${giga} GB`;
+ if (mega > 0) return `${mega} MB`;
+ if (kilo > 0) return `${kilo} kB`;
+ return `${remainder} B`;
+}
diff --git a/tatort/src/lib/helper/timeElapsed.js b/tatort/src/lib/helper/timeElapsed.js
new file mode 100644
index 0000000..b2f8a73
--- /dev/null
+++ b/tatort/src/lib/helper/timeElapsed.js
@@ -0,0 +1,33 @@
+const MINUTE = 60;
+const HOUR = 60 * MINUTE;
+const DAY = 24 * HOUR;
+const YEAR = 365 * DAY;
+const MONTH = YEAR / 12;
+
+/**
+ * get readable string of time elapsed since date
+ * @param {Date} date
+ * @returns string
+ */
+export default function timeElapsed(date) {
+ const now = new Date();
+ const age = Math.floor((now.getTime() - date.getTime()) / 1000);
+
+ const years = Math.floor(age / YEAR);
+ let remainder = age % YEAR;
+ const months = Math.floor(remainder / MONTH);
+ remainder %= MONTH;
+ const days = Math.floor(remainder / DAY);
+ remainder %= DAY;
+ const hours = Math.floor(remainder / HOUR);
+ remainder %= HOUR;
+ const minutes = Math.floor(remainder / MINUTE);
+ const seconds = remainder % MINUTE;
+ if (years > 0) return years === 1 ? 'vor 1 Jahr' : `vor ${years} Jahren`;
+ if (months > 0) return months === 1 ? 'vor 1 Monat' : `vor ${months} Monaten`;
+ if (days > 0) return days === 1 ? 'vor 1 Tag' : `vor ${days} Tagen`;
+ if (hours > 0) return hours === 1 ? 'vor 1 Stunde' : `vor ${hours} Stunden`;
+ if (minutes > 0) return minutes === 1 ? 'vor 1 Minute' : `vor ${minutes} Minuten`;
+
+ return seconds === 1 ? 'vor 1 Sekunde' : `vor ${seconds} Sekunden`;
+}
diff --git a/tatort/src/lib/icons/Arrow-left.svelte b/tatort/src/lib/icons/Arrow-left.svelte
new file mode 100644
index 0000000..5b98b38
--- /dev/null
+++ b/tatort/src/lib/icons/Arrow-left.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Arrow-right.svelte b/tatort/src/lib/icons/Arrow-right.svelte
new file mode 100644
index 0000000..cb9c9e9
--- /dev/null
+++ b/tatort/src/lib/icons/Arrow-right.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Bell.svelte b/tatort/src/lib/icons/Bell.svelte
new file mode 100644
index 0000000..0ec709c
--- /dev/null
+++ b/tatort/src/lib/icons/Bell.svelte
@@ -0,0 +1,14 @@
+
diff --git a/tatort/src/lib/icons/Book.svelte b/tatort/src/lib/icons/Book.svelte
new file mode 100644
index 0000000..4cc27f3
--- /dev/null
+++ b/tatort/src/lib/icons/Book.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Check.svelte b/tatort/src/lib/icons/Check.svelte
new file mode 100644
index 0000000..1189789
--- /dev/null
+++ b/tatort/src/lib/icons/Check.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Chevron-left.svelte b/tatort/src/lib/icons/Chevron-left.svelte
new file mode 100644
index 0000000..2a34fa5
--- /dev/null
+++ b/tatort/src/lib/icons/Chevron-left.svelte
@@ -0,0 +1,8 @@
+
diff --git a/tatort/src/lib/icons/Chevron-right.svelte b/tatort/src/lib/icons/Chevron-right.svelte
new file mode 100644
index 0000000..8b7440d
--- /dev/null
+++ b/tatort/src/lib/icons/Chevron-right.svelte
@@ -0,0 +1,8 @@
+
diff --git a/tatort/src/lib/icons/Chevron.svelte b/tatort/src/lib/icons/Chevron.svelte
new file mode 100644
index 0000000..5440f50
--- /dev/null
+++ b/tatort/src/lib/icons/Chevron.svelte
@@ -0,0 +1,10 @@
+
diff --git a/tatort/src/lib/icons/Drag.svelte b/tatort/src/lib/icons/Drag.svelte
new file mode 100644
index 0000000..97def46
--- /dev/null
+++ b/tatort/src/lib/icons/Drag.svelte
@@ -0,0 +1,16 @@
+
diff --git a/tatort/src/lib/icons/Exclamation-circle.svelte b/tatort/src/lib/icons/Exclamation-circle.svelte
new file mode 100644
index 0000000..0b9c550
--- /dev/null
+++ b/tatort/src/lib/icons/Exclamation-circle.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Exclamation.svelte b/tatort/src/lib/icons/Exclamation.svelte
new file mode 100644
index 0000000..f35ed58
--- /dev/null
+++ b/tatort/src/lib/icons/Exclamation.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Info.svelte b/tatort/src/lib/icons/Info.svelte
new file mode 100644
index 0000000..560be4c
--- /dev/null
+++ b/tatort/src/lib/icons/Info.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Login.svelte b/tatort/src/lib/icons/Login.svelte
new file mode 100644
index 0000000..b164996
--- /dev/null
+++ b/tatort/src/lib/icons/Login.svelte
@@ -0,0 +1,14 @@
+
diff --git a/tatort/src/lib/icons/Play.svelte b/tatort/src/lib/icons/Play.svelte
new file mode 100644
index 0000000..1c78552
--- /dev/null
+++ b/tatort/src/lib/icons/Play.svelte
@@ -0,0 +1,18 @@
+
diff --git a/tatort/src/lib/icons/Plus.svelte b/tatort/src/lib/icons/Plus.svelte
new file mode 100644
index 0000000..50dfeb3
--- /dev/null
+++ b/tatort/src/lib/icons/Plus.svelte
@@ -0,0 +1,15 @@
+
diff --git a/tatort/src/lib/icons/Question-mark-circle.svelte b/tatort/src/lib/icons/Question-mark-circle.svelte
new file mode 100644
index 0000000..4a2645c
--- /dev/null
+++ b/tatort/src/lib/icons/Question-mark-circle.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Selector.svelte b/tatort/src/lib/icons/Selector.svelte
new file mode 100644
index 0000000..81fdec3
--- /dev/null
+++ b/tatort/src/lib/icons/Selector.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/icons/Trash.svelte b/tatort/src/lib/icons/Trash.svelte
new file mode 100644
index 0000000..2199194
--- /dev/null
+++ b/tatort/src/lib/icons/Trash.svelte
@@ -0,0 +1,14 @@
+
diff --git a/tatort/src/lib/icons/X.svelte b/tatort/src/lib/icons/X.svelte
new file mode 100644
index 0000000..56582da
--- /dev/null
+++ b/tatort/src/lib/icons/X.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tatort/src/lib/minio.js b/tatort/src/lib/minio.js
new file mode 100644
index 0000000..3f1b6b7
--- /dev/null
+++ b/tatort/src/lib/minio.js
@@ -0,0 +1,6 @@
+import Minio from 'minio';
+import config from '$lib/config';
+
+process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
+
+export const client = new Minio.Client(config.minio);
diff --git a/tatort/src/routes/(angemeldet)/+layout.server.js b/tatort/src/routes/(angemeldet)/+layout.server.js
new file mode 100644
index 0000000..c54cfba
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/+layout.server.js
@@ -0,0 +1,9 @@
+import { redirect } from '@sveltejs/kit';
+
+/** @type {import('./$types').PageServerLoad} */
+export function load(event) {
+ if (!event.locals.user && event.url.pathname !== '/anmeldung') throw redirect(303, '/anmeldung');
+ return {
+ user: event.locals.user
+ };
+}
diff --git a/tatort/src/routes/(angemeldet)/+layout.svelte b/tatort/src/routes/(angemeldet)/+layout.svelte
new file mode 100644
index 0000000..bbc47d3
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/+layout.svelte
@@ -0,0 +1,96 @@
+
+
+
diff --git a/tatort/src/routes/(angemeldet)/+page.svelte b/tatort/src/routes/(angemeldet)/+page.svelte
new file mode 100644
index 0000000..dc92470
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/+page.svelte
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+ {#if data.user.admin}
+
+
+
+ Liste
+
+
+
+ Verschaffe Dir einen Überblick über alle gespeicherten Tatorte.
+
+
+ {/if}
+ {#if data.user.admin}
+
+
+
+ Neueer Vorgang
+
+
+
Stelle einen weiteren Tatort für die Anwendung bereit.
+
+ {/if}
+ {#if data.user.admin}
+
+
+
+ Hinzufügen
+
+
+
Fügen Sie einem Tatort Bilder hinzu.
+
+ {/if}
+
+
+
+ Ansicht
+
+
+
Schau Dir einen Tatort in der 3D Ansicht an.
+
+
+
diff --git a/tatort/src/routes/(angemeldet)/list/+page.svelte b/tatort/src/routes/(angemeldet)/list/+page.svelte
new file mode 100644
index 0000000..fff40ab
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/list/+page.svelte
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
Liste der Vorgänge
+
+
+
+ {#each list as item}
+ -
+
+
+
+
+
+ {/each}
+
+
+
diff --git a/tatort/src/routes/(angemeldet)/list/[vorgang]/+page.svelte b/tatort/src/routes/(angemeldet)/list/[vorgang]/+page.svelte
new file mode 100644
index 0000000..9b4c240
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/list/[vorgang]/+page.svelte
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
Vorgang {$page.params.vorgang}
+
+
+
diff --git a/tatort/src/routes/(angemeldet)/tatorte/+page.server.js b/tatort/src/routes/(angemeldet)/tatorte/+page.server.js
new file mode 100644
index 0000000..48f197d
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/tatorte/+page.server.js
@@ -0,0 +1,37 @@
+import { client } from '$lib/minio';
+import { fail } from '@sveltejs/kit';
+
+import caseNumberOccupied from '$lib/helper/caseNumberOccupied';
+
+/** @type {import('./$types').Actions} */
+export const actions = {
+ default: async ({ request }) => {
+ const data = await request.formData();
+ const caseNumber = data.get('caseNumber');
+ const description = data.get('description');
+
+ if (!caseNumber) {
+ return fail(400, {
+ caseNumber,
+ description,
+ error: { caseNumber: 'Es muss eine Vorgangsnummer vorhanden sein.' }
+ });
+ }
+
+ if (await caseNumberOccupied(`${caseNumber}`)) {
+ return fail(400, {
+ caseNumber,
+ description,
+ error: { caseNumber: 'Die Vorgangsnummer wurde im System bereits angelegt.' }
+ });
+ }
+
+ const config = `${JSON.stringify({ caseNumber, description, version: 1 })}\n`;
+
+ await client.putObject('tatort', `${caseNumber}/config.json`, config, {
+ 'Content-Type': 'application/json'
+ });
+
+ return { success: true };
+ }
+};
diff --git a/tatort/src/routes/(angemeldet)/tatorte/+page.svelte b/tatort/src/routes/(angemeldet)/tatorte/+page.svelte
new file mode 100644
index 0000000..8b657e5
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/tatorte/+page.svelte
@@ -0,0 +1,99 @@
+
+
+
+
+
Neuer Vorgang
+
+
+
+
+
vorgang anlegen
+ {#if form?.success}
+ Vorgang erfolgreich angelegt
+ {:else}
+ Fehler beim Upload
+ {/if}
+
+
+
+
diff --git a/tatort/src/routes/(angemeldet)/upload/+page.server.js b/tatort/src/routes/(angemeldet)/upload/+page.server.js
new file mode 100644
index 0000000..4d08dcc
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/upload/+page.server.js
@@ -0,0 +1,103 @@
+import path from 'path';
+import { writeFile } from 'fs/promises';
+import { createReadStream } from 'fs';
+import Minio from 'minio';
+import { Readable } from 'stream';
+const MINIO_ACCESS_KEY = 'tMhLrfog47lMm0HZ';
+import { client } from '$lib/minio';
+import { fail } from '@sveltejs/kit';
+
+function isRequiredFieldValid(value) {
+ if (value == null) return false;
+
+ if (typeof value === 'string' || value instanceof String) return value.trim() !== '';
+
+ return true;
+}
+
+/** @type {import('./$types').Actions} */
+export const actions = {
+ url: async ({ request }) => {
+ const data = await request.formData();
+ const vorgang = data.get('vorgang');
+ const name = data.get('name');
+ const type = data.get('type');
+ const fileName = data.get('fileName');
+
+ let objectName = `${vorgang}/${name}`;
+ switch (type) {
+ case 'image/png':
+ if (!objectName.endsWith('.png')) objectName += '.png';
+ break;
+ case '':
+ if (fileName.endsWith('.glb') && !objectName.endsWith('.glb')) objectName += '.glb';
+ }
+
+ const url = await client.presignedPutObject('tatort', objectName);
+
+ return { url };
+ },
+ validate: async ({ request }) => {
+ const requestData = await request.formData();
+ const data = Object.fromEntries(requestData);
+ const vorgang = data.vorgang;
+ const name = data.name;
+ let success = true;
+ let err = {};
+
+ if (isRequiredFieldValid(vorgang)) err.vorgang = null;
+ else {
+ err.vorgang = 'Das Feld Vorgang darf nicht leer bleiben.';
+ success = false;
+ }
+
+ if (isRequiredFieldValid(name)) err.name = null;
+ else {
+ err.name = 'Das Feld Name darf nicht leer bleiben.';
+ success = false;
+ }
+
+ if (success) return { success };
+
+ return fail(400, err);
+ },
+
+ upload: async ({ request }) => {
+ const requestData = await request.formData();
+ const data = Object.fromEntries(requestData);
+ const vorgang = data.vorgang;
+ const name = data.name;
+ console.log('I:', vorgang, name);
+
+ const url = await client.presignedPutObject('tatort', `${vorgang}/${name}`, 60);
+
+ console.log('O:', url);
+ return { url };
+ },
+ upload3: async ({ request }) => {
+ const requestData = await request.formData();
+ const data = Object.fromEntries(requestData);
+ const name = data.name;
+ const stream = data.file.stream();
+ console.log('Data:', stream);
+ const metaData = { 'Content-Type': 'model-gtlf-binary', 'X-VorgangsNr': '4711' };
+ const result = new Promise((resolve, reject) => {
+ client.putObject('tatort', name, Readable.from(stream), metaData, function (err, etag) {
+ if (err) return reject(err);
+ resolve(etag);
+ });
+ });
+ let etag = null;
+ let error = null;
+ try {
+ etag = await result;
+ console.log(etag);
+ } catch (err) {
+ error = err;
+ console.log('Error:', err);
+ }
+
+ return { etag, error };
+ //await writeFile(filePath, Buffer.from(await data.file.arrayBuffer()));
+ }
+};
diff --git a/tatort/src/routes/(angemeldet)/upload/+page.svelte b/tatort/src/routes/(angemeldet)/upload/+page.svelte
new file mode 100644
index 0000000..5937f13
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/upload/+page.svelte
@@ -0,0 +1,244 @@
+
+
+
+
+
Datei zu Vorgang hinzufügen
+
+
+
+
+
+
+ This information will be displayed publicly so be careful what you share.
+
+
+
+
+
+
+ {#if formErrors?.vorgang}
+
{formErrors.vorgang}
+ {/if}
+
+
+
+
+
+ {#if formErrors?.name}
+
{formErrors.name}
+ {/if}
+
+
+
+
+
+
+
+
+
+
oder ziehe sie ins Feld
+
+
GLB Dateien bis zu 1GB
+ {#if files?.length}
+
+
Datei: {files[0].name}
+
+ Größe: {shortenFileSize(files[0].size)}
+
+
+ {/if}
+
+
+ {#if formErrors?.file}
+
{formErrors.file}
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
Upload
+ {#if inProgress}
+ Upload läuft...
+ {:else if etag}
+ Upload erfolgreich
+ {:else}
+ Fehler beim Upload
+ {/if}
+
+
+
+
diff --git a/tatort/src/routes/(angemeldet)/view/+page.server.js b/tatort/src/routes/(angemeldet)/view/+page.server.js
new file mode 100644
index 0000000..337f6e4
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/view/+page.server.js
@@ -0,0 +1,27 @@
+import caseNumberOccupied from '$lib/helper/caseNumberOccupied';
+import { fail, redirect } from '@sveltejs/kit';
+
+/** @type {import('./$types').Actions} */
+export const actions = {
+ default: async ({ request }) => {
+ const data = await request.formData();
+ const caseNumber = data.get('caseNumber');
+
+ if (!caseNumber) {
+ return fail(400, {
+ success: false,
+ caseNumber,
+ error: { caseNumber: 'Die Vorgangsnummer darf nicht leer sein.' }
+ });
+ }
+
+ if (!(await caseNumberOccupied(caseNumber))) {
+ return fail(400, {
+ success: false,
+ caseNumber,
+ error: { caseNumber: 'Die Vorgangsnummer existiert in dieser Anwendung nicht.' }
+ });
+ }
+ throw redirect(303, `/list/${caseNumber}`);
+ }
+};
diff --git a/tatort/src/routes/(angemeldet)/view/+page.svelte b/tatort/src/routes/(angemeldet)/view/+page.svelte
new file mode 100644
index 0000000..997ba49
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/view/+page.svelte
@@ -0,0 +1,64 @@
+
+
+
+
+
Vorgang ansehen
+
+
+
+
diff --git a/tatort/src/routes/(angemeldet)/view/[vorgang]/[tatort]/+page.server.js b/tatort/src/routes/(angemeldet)/view/[vorgang]/[tatort]/+page.server.js
new file mode 100644
index 0000000..131154f
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/view/[vorgang]/[tatort]/+page.server.js
@@ -0,0 +1,8 @@
+import { client } from '$lib/minio';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ const { vorgang, tatort } = params;
+ const url = await client.presignedUrl('GET', 'tatort', `${vorgang}/${tatort}`);
+ return { url };
+}
diff --git a/tatort/src/routes/(angemeldet)/view/[vorgang]/[tatort]/+page.svelte b/tatort/src/routes/(angemeldet)/view/[vorgang]/[tatort]/+page.svelte
new file mode 100644
index 0000000..5de3a8f
--- /dev/null
+++ b/tatort/src/routes/(angemeldet)/view/[vorgang]/[tatort]/+page.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
Loading {progress}%
+
+
+
+
diff --git a/tatort/src/routes/+layout.svelte b/tatort/src/routes/+layout.svelte
new file mode 100644
index 0000000..2e511e0
--- /dev/null
+++ b/tatort/src/routes/+layout.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tatort/src/routes/anmeldung/+layout.svelte b/tatort/src/routes/anmeldung/+layout.svelte
new file mode 100644
index 0000000..d3622c6
--- /dev/null
+++ b/tatort/src/routes/anmeldung/+layout.svelte
@@ -0,0 +1 @@
+
diff --git a/tatort/src/routes/anmeldung/+page.server.js b/tatort/src/routes/anmeldung/+page.server.js
new file mode 100644
index 0000000..6daba80
--- /dev/null
+++ b/tatort/src/routes/anmeldung/+page.server.js
@@ -0,0 +1,31 @@
+import { dev } from '$app/environment';
+import { fail, redirect } from '@sveltejs/kit';
+import { authenticate } from '$lib/auth';
+
+const COOKIE_NAME = 'session';
+
+/** @type {import('./$types').Actions} */
+export const actions = {
+ login: async ({ request, cookies }) => {
+ const data = await request.formData();
+ const user = data.get('user');
+ const password = data.get('password');
+
+ const token = authenticate(user, password);
+
+ if (!token) return fail(400, { user, incorrect: true });
+
+ cookies.set(COOKIE_NAME, token, {
+ path: '/',
+ httpOnly: true,
+ sameSite: 'strict',
+ secure: !dev
+ });
+ throw redirect(303, '/');
+ },
+ logout: async (event) => {
+ event.cookies.delete(COOKIE_NAME);
+ event.locals.user = null;
+ return { success: true };
+ }
+};
diff --git a/tatort/src/routes/anmeldung/+page.svelte b/tatort/src/routes/anmeldung/+page.svelte
new file mode 100644
index 0000000..d953f3b
--- /dev/null
+++ b/tatort/src/routes/anmeldung/+page.svelte
@@ -0,0 +1,75 @@
+
+
+
+
+
+

+
+
+ Anmeldung zum 3D Tatort
+
+
+
+
+
+
+
diff --git a/tatort/src/routes/api/list/[[vorgang]]/+server.js b/tatort/src/routes/api/list/[[vorgang]]/+server.js
new file mode 100644
index 0000000..9115150
--- /dev/null
+++ b/tatort/src/routes/api/list/[[vorgang]]/+server.js
@@ -0,0 +1,35 @@
+import { client } from '$lib/minio';
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET({ params }) {
+ const prefix = params.vorgang ? `${params.vorgang}/` : '';
+ let stream = client.listObjectsV2('tatort', prefix, false, '');
+ const result = new ReadableStream({
+ start(controller) {
+ stream.on('data', (data) => {
+ if (prefix === '') {
+ if (data.prefix)
+ controller.enqueue(`${JSON.stringify({ ...data, name: data.prefix.slice(0, -1) })}\n`);
+ return;
+ }
+
+ const name = data.name.slice(prefix.length);
+ if (name === 'config.json') return;
+
+ controller.enqueue(`${JSON.stringify({ ...data, name, prefix })}\n`);
+ });
+ stream.on('end', () => {
+ controller.close();
+ });
+ },
+ cancel() {
+ stream.destroy();
+ }
+ });
+
+ return new Response(result, {
+ headers: {
+ 'content-type': 'text/event-stream'
+ }
+ });
+}
diff --git a/tatort/src/routes/api/tatort/+server.js b/tatort/src/routes/api/tatort/+server.js
new file mode 100644
index 0000000..dbc89c7
--- /dev/null
+++ b/tatort/src/routes/api/tatort/+server.js
@@ -0,0 +1,26 @@
+import { client } from '$lib/minio';
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET() {
+ var stream = client.listObjectsV2('tatort', '', true);
+ const result = new ReadableStream({
+ start(controller) {
+ stream.on('data', (data) => {
+ //console.log(data);
+ controller.enqueue(`${JSON.stringify(data)}\n`);
+ });
+ stream.on('end', () => {
+ controller.close();
+ });
+ },
+ cancel() {
+ stream.destroy();
+ }
+ });
+
+ return new Response(result, {
+ headers: {
+ 'content-type': 'text/event-stream'
+ }
+ });
+}
diff --git a/tatort/src/routes/api/upload/+server.js b/tatort/src/routes/api/upload/+server.js
new file mode 100644
index 0000000..e870252
--- /dev/null
+++ b/tatort/src/routes/api/upload/+server.js
@@ -0,0 +1,6 @@
+import { client } from '$lib/minio';
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET(params) {
+ console.log('GET', params);
+}
diff --git a/tatort/static/Landeswappen_NI.svg b/tatort/static/Landeswappen_NI.svg
new file mode 100644
index 0000000..587fcc8
--- /dev/null
+++ b/tatort/static/Landeswappen_NI.svg
@@ -0,0 +1,1300 @@
+
+
diff --git a/tatort/static/favicon.png b/tatort/static/favicon.png
new file mode 100644
index 0000000..825b9e6
Binary files /dev/null and b/tatort/static/favicon.png differ
diff --git a/tatort/svelte.config.js b/tatort/svelte.config.js
new file mode 100644
index 0000000..da2ab37
--- /dev/null
+++ b/tatort/svelte.config.js
@@ -0,0 +1,22 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/kit/vite';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ kit: {
+ // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
+ // If your environment is not supported or you settled on a specific environment, switch out the adapter.
+ // See https://kit.svelte.dev/docs/adapters for more information about adapters.
+ adapter: adapter()
+ },
+ preprocess: vitePreprocess(),
+ onwarn: (warning, defaultHandler) => {
+ // don't warn on