initial commit

This commit is contained in:
Lucas Pleß 2025-01-21 12:47:35 +01:00
commit 94883878f7
22 changed files with 3749 additions and 0 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
**/node_modules
**/dist

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vscode
__pycache__

18
Dockerfile Normal file
View file

@ -0,0 +1,18 @@
#build web app
FROM node:lts-alpine AS build-stage
WORKDIR /app
COPY /frontend ./
RUN npm install
RUN npm run build
#setup flask app
FROM python:3.10-alpine AS production-stage
WORKDIR /app
COPY ./backend /app/backend
#RUN mkdir /app/static
RUN pip install --no-cache-dir -r backend/requirements.txt
COPY --from=build-stage /app/dist /app/frontend/dist/
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--chdir", "backend", "app:app"]

13
backend/Pipfile Normal file
View file

@ -0,0 +1,13 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
flask = "*"
uwsgi = "*"
[requires]
python_version = "3.8"

107
backend/Pipfile.lock generated Normal file
View file

@ -0,0 +1,107 @@
{
"_meta": {
"hash": {
"sha256": "392f1c95a652ebb54d90d436d9b51a50d66a6ed0ce9dd16b444589df509a5f62"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"flask": {
"hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
],
"index": "pypi",
"version": "==1.1.2"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.2"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
},
"uwsgi": {
"hashes": [
"sha256:faa85e053c0b1be4d5585b0858d3a511d2cd10201802e8676060fd0a109e5869"
],
"index": "pypi",
"version": "==2.0.19.1"
},
"werkzeug": {
"hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.0.1"
}
},
"develop": {}
}

102
backend/app.py Normal file
View file

@ -0,0 +1,102 @@
import os
import logging
from flask import Flask, send_file, request, jsonify
from requests import HTTPError
from zammad_py import ZammadAPI
from zammad_py.api import Object as ZamObject
logger = logging.getLogger(__name__)
ZAMMAD_TOKEN = os.getenv("ZAMMAD_TOKEN")
ZAMMAD_URL = os.getenv("ZAMMAD_URL")
if not ZAMMAD_TOKEN or not ZAMMAD_URL:
logger.error('Missing conf env var(s)')
app = Flask(__name__,
static_folder=os.path.join("..","frontend","dist"))
zammad = ZammadAPI(url=ZAMMAD_URL, http_token=ZAMMAD_TOKEN)
@app.route("/")
def main():
index_path = os.path.join(app.static_folder, "index.html")
return send_file(index_path)
@app.route('/api/hello')
def start():
return "Hallo, Welt!"
@app.route('/api/buildings')
def api_buildings():
#return ["Theater Dortmund", "Schauspiel Dortmund", "Probebühnen Alte Straße", "Lager Alte Straße",
# "Probebühne KJT","Schreinerei","Schlosserei", "Lager Niedersachsenweg" ]
zob = ZamObject(zammad)
for z in zob.all():
if "gebaeude" == z['name']:
return z['data_option']['options']
return jsonify([])
@app.route("/api/groups")
def api_groups():
#return [{"text":'Hausmeister', "value":"5"}, {"text":"Haustechnik", "value":"4"}, {"text":"Leitung-HBT", "value":"2"}, {"text":"IT", "value":"4"}]
arr = []
for x in zammad.group.all():
if "Users" != x['name']:
arr.append(x['name'])
return jsonify(arr)
@app.route("/api/submit", methods=['POST'])
def api_submit():
data = request.json
print(data)
if data['firstname'] != "" and data['lastname'] != "" and data['email'] != "":
try:
zammad.user.create({"firstname":data['firstname'],"lastname":data['lastname'],"email":data['email'],"roles":["Customer"]})
except HTTPError as e:
print("User creation failed: {}".format(e))
if "Invalid email" in str(e):
return jsonify(error='Invalid Email'), 500
try:
zammad.on_behalf_of = data['email']
title = (data['description'][:25] + '..') if len(data['description']) > 25 else data['description']
zammad.ticket.create({"title":"Web-Form: {}".format(title),"customer":data['email'],
"group":data['group'],
"gebaeude":data['building'],
"raumnummer":data['room'],
"article": {
"subject": "",
"body": data['description'],
"type": "note",
"internal": "false"
}})
except HTTPError as e:
print("Ticket creation failed: {}".format(e))
return jsonify(error=str(e)), 500
return "ok"
# Everything not declared before (not a Flask route / API endpoint)...
@app.route("/<path:path>")
def route_frontend(path):
# ...could be a static file needed by the front end that
# doesn't use the `static` path (like in `<script src="bundle.js">`)
file_path = os.path.join(app.static_folder, path)
if os.path.isfile(file_path):
return send_file(file_path)
# ...or should be handled by the SPA's "router" in front end
else:
index_path = os.path.join(app.static_folder, "index.html")
return send_file(index_path)
if __name__ == "__main__":
# Only for debugging while developing
app.run(host="0.0.0.0", debug=True, port=8080)

4
backend/requirements.txt Normal file
View file

@ -0,0 +1,4 @@
Flask==2.2.2
Werkzeug==2.2.2
gunicorn==23.0.0
zammad_py==3.0.0

36
backend/uwsgi.ini Normal file
View file

@ -0,0 +1,36 @@
# [uwsgi]
# # the module itself, by referring to the wsgi.py file minus the extension, and the callable within the file, app:
# module = wsgi:app
# # Enable hot reload!
# py-autoreload = 1
# # Nginx to handle actual client connections, which will then pass requests to uWSGI.
# socket = :8080
# master = true
# processes = 4
# threads = 2
# # giving the Nginx group ownership of the uWSGI process later on,
# # so we need to make sure the group owner of the socket can read information from it and write to it.
# chmod-socket = 660
# # clean up the socket when the process stops by adding the vacuum option:
# vacuum = true
# die-on-term = true
[uwsgi]
# module = uwsgi:app
module = main
callable = app
# py-autoreload = 1
# master = true
# processes = 4
# threads = 2
# vacuum = true
# wsgi-file=/app/uwsgi.py

9
docker-compose.yml Normal file
View file

@ -0,0 +1,9 @@
services:
app:
build: .
container_name: local_single
restart: always
environment:
- APP_NAME=VueApp
ports:
- "8080:8080"

22
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
frontend/README.md Normal file
View file

@ -0,0 +1,24 @@
# hello
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
frontend/babel.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

16
frontend/index.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>Zammad Form</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

3016
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
frontend/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "hello",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"axios": "^1.7.9",
"bootstrap": "^5.3.3",
"core-js": "^3.6.5",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.5",
"vite-plugin-vue-devtools": "^7.6.8"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

16
frontend/src/App.vue Normal file
View file

@ -0,0 +1,16 @@
<script setup>
import Contact from './components/Contact.vue'
</script>
<template>
<header>
</header>
<main>
<Contact />
</main>
</template>

View file

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -0,0 +1,8 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}

View file

@ -0,0 +1,203 @@
<script setup>
import { ref, onMounted, reactive, computed } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required } from '@vuelidate/validators'
import axios from 'axios'
const formdata = reactive({
firstname: '', lastname: '', email: '', phone: '', description: '',
building: '', group: '', room: ''
})
const rules = computed(() => {
return {
email: { required },
firstname: { required },
lastname: { required },
description: { required },
building: { required },
group: { required },
room: { required }
}
})
const sending = ref(false);
const error = ref(false);
const success = ref(false);
const v$ = useVuelidate(rules, formdata);
const buildings = ref([]);
const groups = ref([]);
onMounted(() => {
axios.get('/api/buildings').then((response) => {
buildings.value = response.data;
});
axios.get('/api/groups').then((response) => {
groups.value = response.data;
})
})
async function submit() {
const result = await v$.value.$validate()
if (result) {
sending.value = true;
console.log(formdata);
axios.post('/api/submit', formdata, { headers: { 'Content-Type': 'application/json' } })
.then((response) => {
console.log(response);
success.value = true;
error.value = false;
})
.catch(err => {
console.error(err);
error.value = true;
success.value = false;
})
.finally(() => { sending.value = false });
}
}
</script>
<template>
<div class="container">
<h1>Ticket erstellen</h1>
<div v-if="success" class="alert alert-success" role="alert">
Das Formular wurde versendet. Es wurde ein neues Ticket angelegt.
</div>
<div v-if="error" class="alert alert-danger" role="alert">
Das Formular konnte nicht abgeschickt werden. Bitte wenden Sie sich per E-Mail an
<a href="mailto:it-theater@theaterdo.de">it-theater@theaterdo.de</a>.
</div>
<form v-if="!success" @submit.prevent="submit" class="needs-validation" novalidate>
<div class="row mb-3">
<div class="col">
<div data-mdb-input-init class="form-outline">
<label class="form-label" for="firstname">Vorname *</label>
<input type="text" id="firstname" class="form-control"
:class="{ 'is-invalid': v$.firstname.$error }" v-model="formdata.firstname" required />
<span class="invalid-feedback" v-if="v$.firstname.$error"> {{ v$.firstname.$errors[0].$message
}} </span>
</div>
</div>
<div class="col">
<div data-mdb-input-init class="form-outline">
<label class="form-label" for="lastname">Nachname *</label>
<input type="text" id="lastname" class="form-control"
:class="{ 'is-invalid': v$.lastname.$error }" v-model="formdata.lastname" required />
<span class="invalid-feedback" v-if="v$.lastname.$error"> {{ v$.lastname.$errors[0].$message }}
</span>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col">
<label for="emailaddress" class="form-label">Email Adresse *</label>
<input type="email" class="form-control" id="emailaddress" :class="{ 'is-invalid': v$.email.$error }"
v-model="formdata.email" required />
<span class="invalid-feedback" v-if="v$.email.$error"> {{ v$.email.$errors[0].$message }} </span>
</div>
<div class="col">
<label for="phone" class="form-label">Telefon:</label>
<input type="text" class="form-control" id="phone" v-model="formdata.phone">
</div>
</div>
<hr class="mb-4" />
<div class="row mb-3">
<div class="col">
<label for="building" class="form-label">Gebäude *</label>
<select id="building" class="form-select" :class="{ 'is-invalid': v$.building.$error }"
v-model="formdata.building" required>
<option v-for="option in buildings" :value="option.name">{{ option.name }}</option>
</select>
<span class="invalid-feedback" v-if="v$.building.$error"> {{ v$.building.$errors[0].$message }}
</span>
</div>
<div class="col">
<label for="room" class="form-label">Raum *</label>
<input type="text" class="form-control" id="room" :class="{ 'is-invalid': v$.room.$error }"
v-model="formdata.room" required />
<span class="invalid-feedback" v-if="v$.room.$error"> {{ v$.room.$errors[0].$message }} </span>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Was ist los? *</label>
<textarea class="form-control" id="description" rows="4" :class="{ 'is-invalid': v$.description.$error }"
v-model="formdata.description" required>
</textarea>
<span class="invalid-feedback" v-if="v$.description.$error"> {{ v$.description.$errors[0].$message }}
</span>
</div>
<div class="mb-3">
<label for="group" class="form-label">An wen soll das Ticket gehen? *</label>
<select id="group" class="form-select" v-model="formdata.group" :class="{ 'is-invalid': v$.group.$error }"
required>
<option v-for="option in groups" :value="option">{{ option }}</option>
</select>
<span class="invalid-feedback" v-if="v$.group.$error"> {{ v$.group.$errors[0].$message }} </span>
<a class="icon-link" href="#" data-bs-toggle="modal" data-bs-target="#zustaendigkeitenmodal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info" viewBox="0 0 16 16">
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
</svg>
Zuständigkeiten anzeigen
</a>
</div>
<hr/>
<button :disabled="sending" type="submit" class="btn btn-primary">Absenden</button>
</form>
<div class="modal fade" tabindex="-1" id="zustaendigkeitenmodal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Zuständigkeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h6>Haustechnik</h6>
<ul>
<li>Aufzugsstörungen, stecken bleiben, außer Betrieb..</li>
<li>Lüftung + Heizung...zu warm, zu kalt..</li>
<li>Stromausfälle...</li>
<li>Beleuchtung...Leuchtmittel defekt...Schalter funktioniert nicht...</li>
<li>Rauchmelderabschaltungen...mit Vorankündigung</li>
<li>kleinere Elektroinstallationen...</li>
<li>BGV A3 Geräteprüfung bei Neunanschaffung von z.b. Wasserkochern oder sonst...</li>
</ul>
<hr/>
<h6>Hausmeister</h6>
<ul>
<li>Abflussverstopfungen...</li>
<li>defekte Sanitäranlagen...Wasserhähne...Toiletten...Spülungen...</li>
<li>Außenbereiche...defekte Stellen...Stolperfallen...</li>
<li>Türen...klemmt...abgebrochene Schlüssel...Türklinken/Beschläge</li>
<li>Druckerpapierlieferungen...Umzugskartons...</li>
<li>Wasserschäden...</li>
<li>Kleinreparraturen...Fenstergriffe...Wandausbesserungen...</li>
<li>defekte Sitze in den Zuschauerräumen...</li>
<li>kleinere hausinterne Transporte...</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
</div>
</template>

9
frontend/src/main.js Normal file
View file

@ -0,0 +1,9 @@
import "bootstrap/dist/css/bootstrap.min.css"
import "bootstrap"
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

27
frontend/vite.config.js Normal file
View file

@ -0,0 +1,27 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8080/',
changeOrigin: true,
secure: false,
},
},
},
})