14 Commits

128 changed files with 2674 additions and 1468 deletions

View File

@@ -11,5 +11,7 @@
"i18n-ally.enabledFrameworks": [
"vue"
],
"i18n-ally.keystyle": "nested"
"i18n-ally.keystyle": "nested",
"i18n-ally.extract.autoDetect": true,
"vue.features.takeOverMode.enabled": true
}

View File

@@ -1,11 +1,34 @@
# v.0.3.0 (Release Candidate 1)
# v.0.4.0 MuC-Edition (2025-09-01)
## 🚀 Features
- Exercise selection system
## 🌟 Enhancements
- Tooltips on append icons in toolbar
- Exercise page icon adds open exercises badge
- Finished english translation
- Icons on exercise groups on help page
- Welcome dialog: New page for look and feel, merge database and exercise creation in one step
- Add links to GitHub and project page on settings
- New section on homepage for popular genres
## 🐛 Bugfixes
- Filter on band page changes visible bands on homepage
- No startup after packaging
- Infinity loading on concert booking page is user comes from band page
# v.0.3.0 (2025-02-28)
## 🚀 Features
- Swagger Documentation
- RPM Image
## 🐛 Bugfixes
- Bugfix on search page for Band datasets
# v.0.2.0 (Beta)
# v.0.2.0 (2024-12-05)
## 🚀 Features
- Adding "Test Environment" banner in the bottom right corner
- License handling system
@@ -24,8 +47,7 @@
- More server stability
- Bugfix file manager in Electron application
# v.0.1.0 (Alpha)
# v.0.1.0 (2024-11-21)
## 🚀 Features
- Frontend
- VueJS frontend framework with Vuetify UI library

View File

@@ -3,127 +3,144 @@
{
"nameDe": "Den Shop kennenlernen",
"nameEn": "Getting to know the shop",
"icon": "mdi-human-greeting",
"groupNr": 0,
"descriptionDe": "Vor einem Angriff ist es wichtig zu verstehen, wie die Webseite aufgebaut ist. Wie sind die URLs strukturiert? Wo befinden sich Eingabefelder welche im Backend eine SQL Abfrage stellen?",
"descriptionEn": "todo",
"descriptionEn": "Before an attack, it's important to understand how the website is structured. How are the URLs structured? Where are input fields located that execute SQL queries in the backend?",
"exercises": [
{
"uuid": "getting-known-register",
"nameDe": "Registrieren",
"nameEn": "Register",
"exerciseNr": 1,
"descriptionDe": "Wir richten uns einen gewöhnlichen Account auf der Plattform ein. Navigiere hierzu auf die Account-Seite und registriere dich.",
"descriptionEn": "Create a new account in the online shop"
"descriptionEn": "We'll set up a regular account on the platform. To do this, navigate to the account page and register."
},
{
"uuid": "getting-known-profile",
"nameDe": "Profil vervollständigen",
"nameEn": "Complete profile",
"exerciseNr": 2,
"descriptionDe": "Bestellungen sind erst möglich, wenn das Account-Profil vervollständigt ist. Logge dich ein, navigiere zu den Account-Einstellungen, fülle den Namen aus und füge je eine Adresse und Bezahlart hinzu. Speichere alles zum Schluss ab.",
"descriptionEn": "Search for an event of choice and buy a ticket for"
"descriptionEn": "Orders are only possible once your account profile is complete. Log in, navigate to your account settings, fill in your name, and add an address and payment method. Finally, save everything."
},
{
"uuid": "getting-known-buy-ticket",
"nameDe": "Ein Ticket kaufen",
"nameEn": "Buy a ticket",
"exerciseNr": 3,
"descriptionDe": "Wir führen nun einen Bestellvorgang durch. Wähle hierzu ein Konzert deiner Wahl und lege Tickets in den Warenkorb. Öffne diesen und schließe die Bestellung ab. Beachte die Struktur der URL wenn du ein Konzert buchen willst. Sieh dir ruhig 2-3 Buchungsseiten an, wie sich die URL jeweils verändert.",
"descriptionEn": "Search for an event of choice and buy a ticket for"
"descriptionEn": "We'll now complete the order process. Select a concert of your choice and add tickets to your shopping cart. Open the shopping cart and complete your order. Pay attention to the URL structure when booking a concert. Feel free to look at two or three booking pages to see how the URL changes each time."
}
]
},
{
"nameDe": "Broken Access Control",
"nameEn": "Broken Access Control",
"icon": "mdi-application-outline",
"groupNr": 1,
"descriptionDe": "Eine Webseite beinhaltet öffentlich einsehbare und einige geschützte Seiten. Letztere sind nur mit passenden Berechtigungen erreichbar. Beispiele hierfür sind ein Admin-Panel oder der persönliche Warenkorb. Der Zugriff wird oft über Cookies oder eine Authentifizierung an einem Backend-Server geregelt. Bei Broken Access Control ist dieser Sicherheits-Mechanismus nicht oder fehlerhaft implementiert. Somit lassen sich Seiten unberechtigterweise über die URL erreichen.",
"descriptionEn": "todo",
"descriptionEn": "A website contains publicly visible pages and some protected pages. The latter can only be accessed with appropriate permissions. Examples include an admin panel or the personal shopping cart. Access is often controlled via cookies or authentication on a backend server. With broken access control, this security mechanism is either not implemented or is incorrectly implemented. This allows pages to be accessed without authorization via the URL.",
"exercises": [
{
"uuid": "broken-access-control-exercise-page",
"nameDe": "Hilfe-Seite aufrufen",
"nameEn": "Access Help Page",
"exerciseNr": 1,
"descriptionDe": "Die Hilfe-Seite erlaubt dir einen Einblick auf den Bearbeitungszustand der Aufgaben. Sie ist dementsprechend nicht abgesichert, aber auch (noch) nicht in der Titel-Leiste als Button erreichbar. Erweitere die URL in der Adresszeile so, dass du auf die Hilfeseite gelangst.",
"descriptionEn": "Manipulate the URL and access the help page"
"descriptionEn": "The help page provides insight into the processing status of tasks. It's therefore not secure, but it's also not (yet) accessible as a button in the title bar. Expand the URL in the address bar to access the help page."
},
{
"uuid": "broken-access-control-hidden-concert",
"nameDe": "Das versteckte Konzert buchen",
"nameEn": "Book the hidden concert",
"exerciseNr": 2,
"descriptionDe": "Die Band >>Arctic Monkeys<< will auf ihrer >>European Tour<< drei Konzerte spielen. Im Shop finden sich allerdings nur zwei Einträge. Zwischen den beiden Tourdaten soll eine Show in der Lanxess Arena in Köln stattfinden, der Datensatz hierfür ist bereits angelegt, jedoch nicht freigeschaltet. Besuche die Seite der Band. Sieh dir den Zeitraum zwischen beiden Konzerten an, in denen das versteckte Event liegen könnte. Öffne eine Buchungsseite eines anderen Konzertes und ändere die URL so ab, dass du das versteckte Konzert buchen kannst. Reserviere dir mindestens ein Ticket und schließe den Bestellprozess ab.",
"descriptionEn": "Manipulate the URL and access the sold out concert and buy a ticket"
"descriptionEn": "The band >>Arctic Monkeys<< plans to play three shows on their >>European Tour<<. However, there are only two entries in the shop. A show at the Lanxess Arena in Cologne is scheduled to take place between the two tour dates. The dataset for this has already been created but is not yet activated. Visit the band's website. Look at the time period between the two concerts, where the hidden event could take place. Open a booking page for another concert and change the URL so that you can book the hidden concert. Reserve at least one ticket and complete the order process."
}
]
},
{
"nameDe": "SQL Injections",
"nameEn": "SQL Injections",
"icon": "mdi-needle",
"groupNr": 2,
"descriptionDe": "Eine Datenbank arbeitet mit SQL Befehlen um Datensätze anzulegen, abzurufen, zu verändern und löschen. Ein Server wird über API-Schnittstellen angesprochen, führt die Befehle in der Datenbank aus und liefert das Ergebnis zurück. Der Client darf keinen direkten Zugriff auf die Datenbank haben. Bei SQL Injections wird versucht, diesen Sicherheitsmechanismus zu umgehen und über die API-Schnittstellen direkte SQL Befehle auszuführen.",
"descriptionEn": "todo",
"descriptionEn": "A database uses SQL commands to create, retrieve, modify, and delete records. A server is accessed via API interfaces, executes the commands in the database, and returns the results. The client must not have direct access to the database. SQL injection attempts to circumvent this security mechanism and execute SQL commands directly via the API interfaces.",
"exercises": [
{
"uuid": "sql-injection-database-scheme",
"nameDe": "Wie sieht die Datenbank aus?",
"nameEn": "How does the database look like?",
"exerciseNr": 1,
"descriptionDe": "Wir versuchen nun die Datenbank im Hintergrund anzugreifen. Aktuell wissen wir aber noch nicht wie die Datenbank aussieht, also welche Tabellen sie beinhaltet. Wir können uns aber mit einem SQL-Befehl ausgeben. Gehe zur globalen Suchseite. Öffne mit der Tastenkombination >>Strg<< + >>D<< die >>Developer Tools<<. Klicke auf den Reiter >>Network<<. Hier siehst du, wie das Frontend mit dem Server kommuniziert. Schreibe nun eine SQL-Injection, welche den Suchbegriff ignoriert und dir stattdessen alle Datensätze der Tabelle >>sqlite_master<< zurück gibt, sofern die Bedingung >>type='table'<< erfüllt ist. Kopiere dir bei erfolgreicher Rückmeldung des Backends die Namen der Tabellen in eine Text-Datei, damit wir für die kommenden Aufgaben die richtigen Namen der Tabellen angeben können.",
"descriptionEn": "todo"
"descriptionEn": "We'll now attempt to attack the database in the background. Currently, we don't yet know what the database looks like, or which tables it contains. However, we can use an SQL command to inject it. Go to the global search page. Open the Developer Tools using the keyboard shortcut Ctrl + D. Click on the Network tab. Here you can see how the frontend communicates with the server. Now write an SQL injection that ignores the search term and instead returns all records in the sqlite_master table, provided the type='table' condition is met. If the backend responds successfully, copy the table names into a text file so that we can specify the correct table names for future tasks."
},
{
"uuid": "sql-injection-all-accounts",
"nameDe": "Alle Accounts ausspähen",
"nameEn": "Get all accounts",
"exerciseNr": 2,
"descriptionDe": "Schreibe nun eine SQL-Injection, welche den Suchbegriff ignoriert und dir stattdessen alle Datensätze der Account-Tabelle zurück liefert. Führe den Angriff über das Suchfeld aus. Sieh dir die Rückmeldung des Servers an.",
"descriptionEn": "Execute an SQL-Injection on the Search page to get all datasets from >>Accounts<< table."
"descriptionEn": "Now write an SQL injection that ignores the search term and instead returns all records in the account table. Execute the attack using the search field. Watch the server's response."
},
{
"uuid": "sql-injection-account-roles",
"nameDe": "Alle Berechtigungsgruppen ausspähen",
"nameEn": "Get all account roles",
"exerciseNr": 3,
"descriptionDe": "Wir sehen nun alle Accounts. Jeder hat eine Berechtigungs-ID (accountRoleId) mit der Berechtigungen wie der Zugriff aufs Admin-Panel geregelt werden. Wir wissen aber nicht, was die ID's bedeuten. Schreibe darum eine SQL-Injection, welche den Suchbegriff ignoriert und dir stattdessen alle Datensätze der Tabelle >>AccountRoles<< zurück liefert. Führe den Angriff über das Suchfeld aus. Beobachte die Rückmeldung des Servers über den >>Network<<-Tab.",
"descriptionEn": "Execute an SQL-Injection on the Search page to get all datasets from >>AccountRoles<< table."
"descriptionEn": "We now see all the accounts. Each has an authorization ID (accountRoleId) that controls permissions such as access to the admin panel. However, we don't know what the IDs mean. Therefore, write an SQL injection that ignores the search term and instead returns all records in the >>AccountRoles<< table. Execute the attack via the search field. Observe the server's response via the >>Network<< tab."
},
{
"uuid": "sql-injection-upgrade-privileges",
"nameDe": "Eigene Berechtigungen erhöhen",
"nameEn": "Upgrade your privileges",
"exerciseNr": 4,
"descriptionDe": "Jetzt bearbeiten wir unseren eigenen Account. Schreibe hierfür einen >>UPDATE<<-SQL-Befehl, welcher die >>accountRoleId<< auf das Niveau eines >>Admin<< erhöht für deinen Account-Namen.",
"descriptionEn": "Change the privileges of your account"
"descriptionEn": "Now we'll edit our own account. To do this, write an >>UPDATE<< SQL command that elevates the >>accountRoleId<< to the level of >>Admin<< for your account name."
},
{
"uuid": "sql-injection-capture-account",
"nameDe": "Einen fremden Account übernehmen",
"nameEn": "Capture another account",
"exerciseNr": 5,
"descriptionDe": "Statt unsere eigenen Berechtigungen zu erhöhen, können wir auch einen Account übernehmen, welcher bereits ein >>Super-Admin<< ist. Suche dir dafür aus der Liste der in Aufgabe 2.1 erhaltenen Accounts einen aus, welcher die Rolle >>Super-Admin<< inne hat. Nur damit lässt sich die Dateiverwaltung öffnen, welche wir später brauchen. Hast du den Account-Namen gefunden, gehe ins Login-Menü (logge dich aus, falls du noch angemeldet bist). Führe nun einen SQL-Injektion durch um diesen Account zu übernehmen.",
"descriptionEn": "todo"
"descriptionEn": "Instead of increasing our own permissions, we can also take over an account that is already a >>super admin<<. To do this, select one from the list of accounts obtained in Task 2.1 that has the >>super admin<< role. Only then can we open the file manager, which we'll need later. Once you've found the account name, go to the login menu (log out if you're still logged in). Now perform an SQL injection to take over this account."
},
{
"uuid": "sql-injection-delete-rating",
"nameDe": "Bewertungen löschen",
"nameEn": "Delete ratings",
"exerciseNr": 6,
"descriptionDe": "Jede Band hat Bewertungen auf einer Skala von eins bis fünf Sternen erhalten. Wir wollen alle Fünf-Sterne Bewertungen aus der Datenbank löschen. Schreibe eine SQL Injection, welche in der Bewertungs-Tabelle alle Einträge mit der Bedingung >>rating = 5<< entfernt. Führe die Injection über die globale Suche aus.",
"descriptionEn": "todo"
"descriptionEn": "Each band has received ratings on a scale of one to five stars. We want to delete all five-star ratings from the database. Write an SQL injection that removes all entries in the ratings table with the condition >>rating = 5<<. Execute the injection via the global search."
}
]
},
{
"nameDe": "Cross-Site Scripting (XSS)",
"nameEn": "Cross-Site Scripting (XSS)",
"icon": "mdi-code-brackets",
"groupNr": 3,
"descriptionDe": "Als nächstes wollen wir Schadcode in die Web-Applikation einschleusen. Zunächst testen wir, ob die Webseite hierfür anfällig ist. Manipuliere die URL der Band-Seite so, dass du eine >>Hallo Welt!<<-Nachricht als >>alert<< siehst. Hinweis: Nutze einen image tag! Setze als >>src<< die Zahl >>1<<. Den Befehl kannst du im Tag >>onerror<< ausführen.",
"descriptionEn": "todo",
"descriptionEn": "Next, we want to inject malicious code into the web application. First, we'll test whether the website is vulnerable to this. Manipulate the URL of the band's page so that you see a >>Hello World!<< message as an >>alert<<. Note: Use an image tag! Set the number >>1<< as the >>src<<. You can execute the command in the >>onerror<< tag.",
"exercises": [
{
"uuid": "cross-site-scripting-hello-world",
"nameDe": "Hallo Welt!",
"nameEn": "Hello World!",
"exerciseNr": 1,
"descriptionDe": "Als nächstes wollen wir Schadcode in die Web-Applikation einschleusen. Zunächst testen wir, ob die Webseite hierfür anfällig ist. Gehe hierzu auf die Seite >>Alle Bands<< und filtere die Einträge nach einem beliebigen Genre deiner Wahl. In der URL-Leiste siehst du nun, dass hinter der URL und dem Ressourcen-Ziel ein Parameter angegeben ist (der Part hinter dem Fragezeichen). Wir tauschen diesen Parameter gegen einen HTML Tag aus. Der Trick hierbei: Als Quelle geben wir den Zahlenwert >>1<< an, wodurch automatisch das ausgeführt wird, was im >>onerror<<-Tag drinnen steht. Genau hier soll eine Alert-Meldung mit >>Hello World!<< als JavaScript Code eingefügt werden. Verändere die URL so, dass sie die Meldung ausgibt. Falls du nicht mit JavaScript vertraut bist, sieh dir die letzte Seite mit nützlichen Befehlen an.",
"descriptionEn": "Take an URL of the shop and extend it with JavaScript code so that a 'Hello World' message appears whent the link is opened"
"descriptionEn": "Next, we want to inject malicious code into the web application. First, we'll test whether the website is vulnerable to malicious code. To do this, go to the >>All Bands<< page and filter the entries by any genre of your choice. In the URL bar, you'll now see that a parameter is specified after the URL and the resource target (the part after the question mark). We'll replace this parameter with an HTML tag. The trick here: We specify the numeric value >>1<< as the source, which automatically executes what's contained in the >>onerror<< tag. This is exactly where we want to insert an alert message with >>Hello World!<< as JavaScript code. Change the URL so that it displays the message. If you're not familiar with JavaScript, check out the last page for useful commands."
},
{
"uuid": "cross-site-scripting-external-script",
"nameDe": "Ein externes Script aufrufen",
"nameEn": "Run an external script",
"exerciseNr": 2,
"descriptionDe": "Wir haben festgestellt, dass die Seite für Cross-Site-Scripting durch Reflected XSS angreifbar ist! Im zweiten Schritt binden wir nun das Script ein. Es wurde bereits auf den Server hochgeladen. Logge dich wahlweise mit einem Admin-Account (Aufgabe 2.5) oder deinem eigenen nun berechtigten Account (Aufgabe 2.4) ein. Öffne nun das Admin-Panel über den Button rechts oben. Suche über die Dateiverwaltung im Admin-Panel nach dem Skript und notiere dir die darunter angezeigte Adresse auf dem Backend-Server. Logge dich aus. Wir wollen das Skript auf der nun sichtbaren Login-Seite über eine veränderte URL einbinden. Nutze hierfür das gleiche Prinzip wie in Aufgabe 3.1. Statt >>genreName<< kannst du einen beliebigen anderen Parameter-Namen verwenden. Nutze die Konsole mit der Tastenkombination Strg + D vor dem Abschicken der URL.Logge dich nach erfolgreicher Aufgabenlösung ein und sieh in der Konsole, wie deine Login-Daten abgegriffen werden.",
"descriptionEn": "Create an URL of the shop, which calls the script"
"descriptionEn": "We've determined that the page is vulnerable to cross-site scripting through Reflected XSS! In the second step, we'll integrate the script. It's already been uploaded to the server. Log in either with an admin account (Task 2.5) or your own, now authorized account (Task 2.4). Now open the admin panel using the button in the top right. Search for the script using the file manager in the admin panel and note the address displayed below it on the backend server. Log out. We want to integrate the script into the now visible login page using a modified URL. Use the same principle as in Task 3.1. Instead of >>genreName<<, you can use any other parameter name. Use the console by pressing Ctrl + D before submitting the URL. After successfully completing the task, log in and watch in the console how your login data is being retrieved."
}
]
}

View File

@@ -3,6 +3,9 @@ import { ExerciseGroup } from "./exerciseGroup.model";
@Table({ timestamps: false })
export class Exercise extends Model {
@Column
uuid: string
@Column
nameDe: string

View File

@@ -9,6 +9,9 @@ export class ExerciseGroup extends Model {
@Column
nameEn: string
@Column
icon: string
@Column
groupNr: number

View File

@@ -36,7 +36,7 @@ app.use("/files", files)
// Add delay for more realistic response times
app.use((req, res, next) => {
setTimeout(next, Math.floor((Math.random() * 1000) + 100))
setTimeout(next, Math.floor((Math.random() * 500) + 100))
})
// Routes

View File

@@ -19,7 +19,7 @@
"icon": "public/logo-small.png"
},
"linux": {
"target": ["deb"],
"target": ["deb", "rpm"],
"maintainer": "Tobias Zoghaib",
"icon": "public/logo-small.png",
"category": "Education"
@@ -35,6 +35,8 @@
"!release",
"!src",
"!dist",
"!out"
"!out",
"!misc",
"!database.sqlite"
]
}

19
example-config.json Normal file
View File

@@ -0,0 +1,19 @@
{
"theme": "dark",
"language": "en",
"notAvailableExercises": [
"getting-known-register",
"getting-known-profile",
"getting-known-buy-ticket",
"broken-access-control-exercise-page",
"broken-access-control-hidden-concert",
"sql-injection-database-schema",
"sql-injection-all-accounts",
"sql-injection-account-roles",
"sql-injection-upgrade-privileges",
"sql-injection-capture-account",
"sql-injection-delete-rating",
"cross-site-scripting-hello-world",
"cross-site-scripting-external-script"
]
}

1085
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "eventmaster",
"version": "0.3.0",
"version": "0.4.0",
"author": "Tobias Zoghaib",
"description": "Hackable ticket store for educational purposes",
"license": "MIT",
"homepage": "www.uni-hannover.de",
"homepage": "https://www.itsec.uni-hannover.de/de/usec/forschung/eventmaster-learning-web-attacks",
"main": "build/src/electron/index.js",
"private": true,
"scripts": {
@@ -44,8 +44,8 @@
"exifreader": "^4.25.0",
"express": "^4.21.1",
"jsonwebtoken": "^9.0.2",
"jspdf": "^2.5.2",
"jspdf-autotable": "^3.8.4",
"jspdf": "^3.0.2",
"jspdf-autotable": "^5.0.2",
"moment": "^2.30.1",
"multer": "^1.4.5-lts.1",
"pinia": "^2.2.4",
@@ -81,7 +81,7 @@
"nodemon": "^3.1.7",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"vite": "^5.4.9",
"vite": "^7.1.4",
"vue-tsc": "^2.1.10"
}
}

View File

@@ -1,50 +1,63 @@
<script setup lang="ts">
import { useTheme } from 'vuetify/lib/framework.mjs';
import { i18n } from './plugins/i18n';
import { ref, watch } from 'vue';
import { usePreferencesStore } from './stores/preferences.store';
import { useFeedbackStore } from './stores/feedback.store';
import companyFooter from './components/navigation/companyFooter.vue';
import urlBar from './components/navigation/urlBar.vue';
import { useRouter } from 'vue-router';
import NavigationBar from './components/navigation/navigationBar.vue';
import { BannerStateEnum } from './data/enums/bannerStateEnum';
import { useTheme } from "vuetify/lib/framework.mjs";
import { i18n } from "./plugins/i18n";
import { ref, watch } from "vue";
import { usePreferencesStore } from "./stores/preferences.store";
import { useFeedbackStore } from "./stores/feedback.store";
import companyFooter from "./components/organisms/companyFooter.vue";
import urlBar from "./components/organisms/urlBar.vue";
import { useRouter } from "vue-router";
import navigationBar from "./components/organisms/navigationBar.vue";
import { BannerStateEnum } from "./data/enums/bannerStateEnum";
const preferencesStore = usePreferencesStore()
const feedbackStore = useFeedbackStore()
const theme = useTheme()
const router = useRouter()
const preferencesStore = usePreferencesStore();
const feedbackStore = useFeedbackStore();
const theme = useTheme();
const router = useRouter();
theme.global.name.value = preferencesStore.theme
theme.global.name.value = preferencesStore.theme;
// Global watcher
// Watch for language change
watch(() => preferencesStore.language, () => {
i18n.global.locale = preferencesStore.language
}, { immediate: true })
watch(
() => preferencesStore.language,
() => {
i18n.global.locale = preferencesStore.language;
},
{ immediate: true }
);
// Watch for theme change
watch(() => preferencesStore.theme, () => {
theme.global.name.value = preferencesStore.theme
})
watch(
() => preferencesStore.theme,
() => {
theme.global.name.value = preferencesStore.theme;
}
);
// Watch for 404 page directions
watch(() => feedbackStore.notFound, () => {
watch(
() => feedbackStore.notFound,
() => {
if (feedbackStore.notFound) {
feedbackStore.notFound = false
router.push("/404")
feedbackStore.notFound = false;
router.push("/404");
}
})
}
);
// Watch for snackbar disappear
watch(() => feedbackStore.showSnackbar, () => {
watch(
() => feedbackStore.showSnackbar,
() => {
if (!feedbackStore.showSnackbar) {
feedbackStore.snackbars = []
feedbackStore.snackbars = [];
}
})
}
);
function calcMargin(i) {
return (i * 60) + 10 + 'px'
return i * 60 + 10 + "px";
}
</script>
@@ -56,7 +69,6 @@ function calcMargin(i) {
<!-- Navigaion bar of page -->
<navigation-bar />
<v-main>
<!-- Snackbar in the top right corner for user feedback -->
<v-snackbar
@@ -75,21 +87,18 @@ function calcMargin(i) {
</v-snackbar>
<!-- Here changes the router the content -->
<v-container max-width="1400" min-height="1000" class="py-0" height="100%">
<v-container
max-width="1400"
min-height="1000"
class="py-0 px-0"
height="100%"
>
<v-sheet color="sheet" height="100%">
<router-view></router-view>
</v-sheet>
</v-container>
<v-btn
fab
dark
fixed
bottom
right
color="primary"
>
<v-btn fab dark fixed bottom right color="primary">
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
@@ -104,8 +113,8 @@ function calcMargin(i) {
location="bottom right"
class="pa-3 mb-12 mr-n16 text-center text-h5"
width="300"
style="rotate: 315deg; z-index: 1008;"
style="rotate: 315deg; z-index: 1008"
>
{{ $t('misc.testEnvironment') }}
{{ $t("misc.testEnvironment") }}
</v-sheet>
</template>

View File

@@ -1,6 +1,3 @@
<script setup lang="ts">
</script>
<template>
<v-progress-circular
size="128"

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
defineProps({
// Title text
title: String,
// Activate loading state (skeleton loader)
loading: {
default: false,
type: Boolean,
},
});
</script>
<template>
<v-skeleton-loader
type="heading"
:loading="loading"
width="300"
class="d-flex justify-center align-center text-h4"
>
{{ title }}
</v-skeleton-loader>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<v-sheet height="12" width="100%" color="primary" class="rounded-pill" />
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
const props = defineProps({
// Title string
title: String,
// Subtitle string below title
subtitle: String,
// Icon on the left side
prependIcon: String,
// Activate loading state (skeleton loader)
loading: {
type: Boolean,
default: false,
},
// Handle action if user taps on item
// Activates a icon on the right site
onTap: {
type: Function,
default: undefined,
},
});
function executeOnTapped() {
if (props.onTap != undefined) {
props.onTap();
}
}
</script>
<template>
<v-skeleton-loader :loading="loading" type="list-item">
<v-list-item
:title="title"
:subtitle="subtitle"
:prepend-icon="prependIcon"
:append-icon="onTap != undefined ? 'mdi-open-in-new' : ''"
width="100%"
@click="executeOnTapped()"
/>
</v-skeleton-loader>
</template>

View File

@@ -1,10 +1,16 @@
<script setup lang="ts">
defineProps({
// Icon displayed on the left side
prependIcon: String,
// Color of button, defaults to secondary
color: {
type: String,
default: "secondary"
}
},
// Activate loading state
loading: Boolean
})
</script>
@@ -13,6 +19,7 @@ defineProps({
:prepend-icon="prependIcon"
variant="outlined"
:color="color"
:loading="loading"
>
<slot></slot>
</v-btn>

View File

@@ -1,55 +0,0 @@
<script setup lang="ts">
defineProps({
title: String,
image: String,
loading: Boolean
})
</script>
<template>
<v-row class="pt-3 d-none d-md-flex">
<!-- Left line -->
<v-col class="d-flex justify-center align-center">
<v-sheet height="12" width="100%" color="primary" class="rounded-s-lg" />
</v-col>
<!-- Title -->
<v-col class="v-col-auto">
<v-skeleton-loader
type="heading"
:loading="loading"
width="300"
>
<v-sheet
class="text-h4"
color="sheet"
>
{{ title }}
</v-sheet>
</v-skeleton-loader>
</v-col>
<!-- Right line -->
<v-col class="d-flex justify-center align-center">
<v-sheet height="12" width="100%" color="primary" class="rounded-e-lg" />
</v-col>
</v-row>
<v-row class="d-md-none">
<v-col>
<v-skeleton-loader
type="heading"
:loading="loading"
class="d-flex justify-center align-center"
>
<span class="text-h4 text-center">{{ title }}</span>
</v-skeleton-loader>
</v-col>
</v-row>
<v-row class="d-md-none">
<v-col class="d-flex justify-center align-center">
<v-sheet height="12" width="80%" color="primary" class="rounded-pill" />
</v-col>
</v-row>
</template>

View File

@@ -3,17 +3,33 @@ defineProps({
/** Displayed smaller text on the left side */
descriptionText: {
type: String,
default: ""
default: "",
},
loading: {
type: Boolean,
default: false,
},
/** Displayed bigger text on the right side */
valueText: [ String, Number ]
})
valueText: [String, Number],
});
</script>
<template>
<v-card variant="outlined" class="my-1 px-2">
<v-row class="d-flex justify-center align-center">
<v-row v-if="loading">
<v-col>
<v-skeleton-loader
type="heading"
:loading="loading"
style="background-color: transparent"
>
sdasd
</v-skeleton-loader>
</v-col>
</v-row>
<v-row class="d-flex justify-center align-center" v-else>
<v-col class="text-caption text-left" v-if="descriptionText.length > 0">
{{ descriptionText }}
</v-col>

View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import { useAccountStore } from "@/stores/account.store";
import { useBasketStore } from "@/stores/basket.store";
import { useExerciseStore } from "@/stores/exercise.store";
import { ref, watch } from "vue";
const accountStore = useAccountStore();
const basketStore = useBasketStore();
const exerciseStore = useExerciseStore();
const basketItems = ref(0);
exerciseStore.getAllExercises();
watch(
() => basketStore.itemsInBasket,
() => {
basketItems.value = basketStore.itemsInBasket.reduce((tot, item) => {
return tot + item.seats.length;
}, 0);
}
);
</script>
<template>
<!-- Global search -->
<v-tooltip :text="$t('misc.search.globalsearch')" location="bottom">
<template #activator="{ props }">
<v-btn v-bind="props" variant="plain" icon="mdi-magnify" to="/search" />
</template>
</v-tooltip>
<!-- Account -->
<v-tooltip :text="$t('account.account')" location="bottom">
<template #activator="{ props }">
<v-btn
v-if="accountStore.userAccountToken == ''"
v-bind="props"
variant="plain"
icon="mdi-account"
to="/account/login"
/>
<v-btn
v-else
v-bind="props"
variant="plain"
icon="mdi-account-check"
to="/account/home"
/>
</template>
</v-tooltip>
<!-- Basket -->
<v-tooltip :text="$t('basket.basket')" location="bottom">
<template #activator="{ props }">
<v-badge
v-if="basketItems > 0"
:content="basketItems"
color="error"
offset-x="8"
offset-y="8"
>
<v-btn v-bind="props" variant="plain" icon="mdi-cart" to="/basket" />
</v-badge>
<v-btn
v-else
v-bind="props"
variant="plain"
icon="mdi-cart"
to="/basket"
/>
</template>
</v-tooltip>
<!-- Exercise page -->
<v-tooltip :text="$t('misc.firstStartup.exercises')" location="bottom">
<template #activator="{ props }">
<v-badge
v-if="exerciseStore.exercisePageVisible"
:content="
exerciseStore.exercises.reduce((tot, exercise) => {
if (exercise.available && !exercise.solved) {
return tot + 1;
} else {
return tot;
}
}, 0)
"
color="error"
offset-x="8"
offset-y="8"
>
<v-btn
v-bind="props"
variant="plain"
icon="mdi-book-open-blank-variant"
to="/help"
/>
</v-badge>
</template>
</v-tooltip>
<!-- Admin panel -->
<v-tooltip :text="$t('admin.adminpanel')" location="bottom">
<template #activator="{ props }">
<v-btn
v-if="accountStore.adminPanelVisible"
v-bind="props"
variant="plain"
icon="mdi-table-cog"
to="/admin"
/>
</template>
</v-tooltip>
<v-tooltip :text="$t('preferences.preferences')" location="bottom">
<template #activator="{ props }">
<v-btn v-bind="props" variant="plain" icon="mdi-cog" to="/preferences" />
</template>
</v-tooltip>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import horizontalLine from "../atoms/horizontalLine.vue";
import headerText from "../atoms/headerText.vue";
defineProps({
// Title text
title: String,
// Activate loading state
loading: Boolean,
});
</script>
<template>
<!-- Layout for displays >=md -->
<v-row class="pt-3 d-none d-md-flex">
<!-- Left line -->
<v-col class="d-flex justify-center align-center">
<horizontal-line />
</v-col>
<!-- Title -->
<v-col class="v-col-auto">
<header-text :loading="loading" :title="title" />
</v-col>
<!-- Right line -->
<v-col class="d-flex justify-center align-center">
<horizontal-line />
</v-col>
</v-row>
<!-- Layout for display <md -->
<v-row class="d-md-none">
<v-col>
<header-text :loading="loading" :title="title" />
</v-col>
</v-row>
<v-row class="d-md-none">
<v-col class="d-flex justify-center align-center">
<horizontal-line />
</v-col>
</v-row>
</template>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { useAccountStore } from '@/stores/account.store';
import { useBasketStore } from '@/stores/basket.store';
import { useExerciseStore } from '@/stores/exercise.store';
const accountStore = useAccountStore()
const basketStore = useBasketStore()
const exerciseStore = useExerciseStore()
exerciseStore.getAllExercises()
</script>
<template>
<v-btn variant="plain" icon="mdi-magnify" to="/search" />
<v-btn
v-if="accountStore.userAccountToken == ''"
variant="plain"
icon="mdi-account"
to="/account/login"
/>
<v-btn v-else variant="plain" icon="mdi-account-check" to="/account/home" />
<div>
<v-badge
:content="basketStore.itemsInBasket.reduce((tot, item) => {
return tot + item.seats.length
}, 0)"
color="error" offset-x="8" offset-y="8">
<v-btn variant="plain" icon="mdi-cart" to="/basket" />
</v-badge>
</div>
<v-btn
v-if="accountStore.adminPanelVisible"
variant="plain"
icon="mdi-table-cog"
to="/admin"
/>
<v-btn
v-if="exerciseStore.helpPageVisible"
variant="plain"
icon="mdi-help"
to="/help"
/>
<v-btn variant="plain" icon="mdi-cog" to="/preferences"/>
</template>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import cardView from "@/components/molecules/cardView.vue";
import packageJson from "../../../package.json";
import listItem from "@/components/atoms/listItem.vue";
function openExternal(url: string) {
window.open(url, "_blank");
}
function openRepository() {
openExternal(
"https://github.com/TobiZog/eventmaster"
);
}
function openWebsite() {
openExternal(
"https://www.itsec.uni-hannover.de/de/usec/forschung/eventmaster-learning-web-attacks"
);
}
</script>
<template>
<card-view :title="$t('preferences.aboutProject')" icon="mdi-information">
<template #borderless>
<v-list>
<list-item
:title="$t('misc.softwareVersion')"
:subtitle="packageJson.version"
prepend-icon="mdi-counter"
/>
<list-item
:title="$t('misc.license')"
subtitle="MIT"
prepend-icon="mdi-license"
/>
<list-item
:title="$t('misc.developer')"
subtitle="Tobias Zoghaib"
prepend-icon="mdi-account"
/>
<list-item
:title="$t('misc.developedFor')"
subtitle="Uni Hannover, Institut für IT-Sicherheit, Fachgebiet Usable Security and Privacy"
prepend-icon="mdi-school"
/>
<list-item
:title="$t('misc.copyright')"
subtitle="2024-2025"
prepend-icon="mdi-copyright"
/>
<list-item
:title="$t('misc.githubRepository')"
prepend-icon="mdi-web"
:onTap="openRepository"
/>
<list-item
:title="$t('misc.projectPage')"
prepend-icon="mdi-web"
:onTap="openWebsite"
/>
</v-list>
</template>
</card-view>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import cardView from '@/components/molecules/cardView.vue';
import { useAccountStore } from '@/stores/account.store';
import { useFeedbackStore } from '@/stores/feedback.store';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import confirmDialog from '@/components/basics/confirmDialog.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import cardView from '@/components/molecules/cardView.vue';
import confirmDialog from '@/components/organisms/confirmDialog.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { useAccountStore } from '@/stores/account.store';
import { ref } from 'vue';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ModelRef } from 'vue';
import cardView from './cardView.vue';
import cardView from '../molecules/cardView.vue';
const showDialog: ModelRef<boolean> = defineModel()

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import actionDialog from '@/components/organisms/actionDialog.vue';
import OutlinedButton from '@/components/atoms/outlinedButton.vue';
import { getIbanRules, getNumberStartRules, getPostalRules, getStringRules } from '@/scripts/validationRules';
import { useAccountStore } from '@/stores/account.store';
import cardViewOneLine from '@/components/basics/cardViewOneLine.vue';
import cardViewOneLine from '@/components/molecules/cardViewOneLine.vue';
import { ref } from 'vue';
const valid = ref(false)

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import actionDialog from '@/components/organisms/actionDialog.vue';
import OutlinedButton from '@/components/atoms/outlinedButton.vue';
import { GenreModel } from '@/data/models/acts/genreModel';
import { useBandStore } from '@/stores/band.store';
import { useGenreStore } from '@/stores/genre.store';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import cardView from '@/components/molecules/cardView.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { GenreModel } from '@/data/models/acts/genreModel';
import { useGenreStore } from '@/stores/genre.store';
import { useRouter } from 'vue-router';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { BandModel } from '@/data/models/acts/bandModel';
import { lowestTicketPrice } from '@/scripts/concertScripts';
import cardViewHorizontal from '@/components/basics/cardViewHorizontal.vue';
import cardViewHorizontal from '@/components/molecules/cardViewHorizontal.vue';
import { useRouter } from 'vue-router';
import { GenreModel } from '@/data/models/acts/genreModel';
import { ConcertModel } from '@/data/models/acts/concertModel';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import cardWithTopImage from '@/components/basics/cardViewTopImage.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import cardWithTopImage from '@/components/molecules/cardViewTopImage.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
import { useBandStore } from '@/stores/band.store';
const bandStore = useBandStore()
@@ -22,7 +22,7 @@ const bandStore = useBandStore()
<v-row>
<v-spacer />
<v-col v-for="member of bandStore.band.members" cols="6" md="3">
<v-col v-for="member of bandStore.band.members" cols="12" md="3">
<card-with-top-image
:title="member.name"
:image=" member.image"

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import cardView from '@/components/molecules/cardView.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { CityModel } from '@/data/models/locations/cityModel';
import { LocationModel } from '@/data/models/locations/locationModel';
import { useConcertStore } from '@/stores/concert.store';

View File

@@ -1,27 +1,27 @@
<script setup lang="ts">
import cardViewHorizontal from '@/components/basics/cardViewHorizontal.vue';
import { BandModel } from '@/data/models/acts/bandModel';
import { ConcertModel } from '@/data/models/acts/concertModel';
import { LocationModel } from '@/data/models/locations/locationModel';
import { useRouter } from 'vue-router';
import cardViewHorizontal from "@/components/molecules/cardViewHorizontal.vue";
import { BandModel } from "@/data/models/acts/bandModel";
import { ConcertModel } from "@/data/models/acts/concertModel";
import { LocationModel } from "@/data/models/locations/locationModel";
import { useRouter } from "vue-router";
const router = useRouter()
const router = useRouter();
defineProps({
/** Concert to display */
concert: {
type: ConcertModel,
required: true
required: true,
},
band: {
type: BandModel,
required: true
required: true,
},
location: {
type: LocationModel,
required: true
required: true,
},
/** Display text parts as skeleton */
@@ -30,9 +30,9 @@ defineProps({
/** Show or hide the button on the right side */
showButton: {
type: Boolean,
default: true
}
})
default: true,
},
});
</script>
<template>
@@ -40,7 +40,13 @@ defineProps({
:title="concert.name"
v-if="!loading"
:link="showButton && concert.inStock > 0"
@click="showButton && concert.inStock > 0 ? router.push('/concerts/booking/' + location.urlName + '/' + concert.date) : () => {}"
@click="console.log(concert.date);
showButton && concert.inStock > 0
? router.push(
'/concerts/booking/' + location.urlName + '/' + concert.date
)
: () => {}
"
>
<template #prepend>
<div>
@@ -49,7 +55,9 @@ defineProps({
</div>
<div class="text-h6">
{{ new Date(concert.date).toLocaleString('default', { month: 'long' }) }}
{{
new Date(concert.date).toLocaleString("default", { month: "long" })
}}
</div>
<div class="text-h6">
@@ -71,28 +79,23 @@ defineProps({
<template #append>
<div>
<div class="text-secondary font-weight-medium text-h6 pb-1">
{{ $t('misc.from') + ' ' + concert.price.toFixed(2) + '' }}
{{ $t("misc.from") + " " + concert.price.toFixed(2) + "" }}
</div>
<div v-if="concert.inStock == 0 && showButton" class="text-h6">
{{ $t('concert.concertSoldOut') }}
{{ $t("concert.concertSoldOut") }}
</div>
<div v-else-if="showButton">
<v-btn variant="flat" color="secondary">
{{ $t('concert.goToTheConcert') }}
{{ $t("concert.goToTheConcert") }}
</v-btn>
</div>
</div>
</template>
</card-view-horizontal>
<card-view-horizontal
v-else
:loading="loading"
>
<v-skeleton-loader
type="text" />
<card-view-horizontal v-else :loading="loading">
<v-skeleton-loader type="text" />
</card-view-horizontal>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import concertListItem from '@/components/pageParts/concertListItem.vue';
import CardViewHorizontal from '@/components/basics/cardViewHorizontal.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import concertListItem from '@/components/organisms/concertListItem.vue';
import CardViewHorizontal from '@/components/molecules/cardViewHorizontal.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
import { useBandStore } from '@/stores/band.store';
const bandStore = useBandStore()

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import { useConcertStore } from "@/stores/concert.store";
import concertListItem from "@/components/organisms/concertListItem.vue";
import cardViewHorizontal from "@/components/molecules/cardViewHorizontal.vue";
import sectionDivider from "@/components/molecules/sectionDivider.vue";
import concertFilterbar from "./concertFilterbar.vue";
const concertStore = useConcertStore();
</script>
<template>
<div v-if="concertStore.fetchInProgress">
<section-divider :loading="true" />
<v-row v-for="i in 3">
<v-col>
<card-view-horizontal :loading="true" />
</v-col>
</v-row>
</div>
<div
v-else-if="concertStore.concerts.length > 0"
v-for="(concert, index) of concertStore.concerts"
>
<div v-if="concert.offered">
<v-row
v-if="
index == 0 ||
new Date(concertStore.concerts[index - 1].date).getMonth() !=
new Date(concertStore.concerts[index].date).getMonth()
"
>
<v-col>
<section-divider
:title="
new Date(concert.date).toLocaleString('default', {
month: 'long',
}) +
' ' +
new Date(concert.date).getFullYear()
"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<concert-list-item
:concert="concert"
:band="concert.band"
:location="concert.location"
/>
</v-col>
</v-row>
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ModelRef } from 'vue';
import actionDialog from './../basics/actionDialog.vue';
import outlinedButton from './../basics/outlinedButton.vue';
import actionDialog from '../organisms/actionDialog.vue';
import outlinedButton from '../atoms/outlinedButton.vue';
const showDialog: ModelRef<boolean> = defineModel()

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import cardView from '@/components/molecules/cardView.vue';
import { useRouter } from 'vue-router';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
const router = useRouter()

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import actionDialog from "@/components/organisms/actionDialog.vue";
import OutlinedButton from "@/components/atoms/outlinedButton.vue";
import {
getExerciseGroupNameLanguage,
getExerciseNameLanguage,
} from "@/scripts/languageScripts";
import { useExerciseStore } from "@/stores/exercise.store";
import { usePreferencesStore } from "@/stores/preferences.store";
import { ModelRef } from "vue";
const showDialog: ModelRef<boolean> = defineModel();
const exerciseStore = useExerciseStore();
const preferencesStore = usePreferencesStore();
function saveConfig() {
preferencesStore.notAvailableExercises = []
for (let exercise of exerciseStore.exercises) {
if (!exercise.available) {
preferencesStore.notAvailableExercises.push(exercise.uuid)
}
}
showDialog.value = false
}
</script>
<template>
<action-dialog
v-model="showDialog"
:title="$t('preferences.exercises.edit')"
icon="mdi-pencil"
width="800"
persistent
>
<v-container>
<v-list>
<div v-for="exercise in exerciseStore.exercises">
<div
v-if="exercise.exerciseNr == 1"
>
<v-divider v-if="exercise.exerciseGroup.groupNr != 0"></v-divider>
<v-list-item
type="subheader"
:title="getExerciseGroupNameLanguage(exercise.exerciseGroup)"
/>
</div>
<v-list-item>
<v-checkbox
:label="getExerciseNameLanguage(exercise)"
v-model="exercise.available"
hide-details
density="compact"
/>
</v-list-item>
</div>
</v-list>
</v-container>
<template #actions>
<outlined-button color="warning" prepend-icon="mdi-close" @click="showDialog = false">
{{ $t('misc.actions.cancel') }}
</outlined-button>
<outlined-button color="success" prepend-icon="mdi-content-save" @click="saveConfig()">
{{ $t("misc.actions.save") }}
</outlined-button>
</template>
</action-dialog>
</template>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import cardView from "@/components/molecules/cardView.vue";
import OutlinedButton from "@/components/atoms/outlinedButton.vue";
import { ExerciseGroupModel } from "@/data/models/exercises/exerciseGroupModel";
import { ExerciseModel } from "@/data/models/exercises/exerciseModel";
import { getExerciseGroupNameLanguage } from "@/scripts/languageScripts";
import { useExerciseStore } from "@/stores/exercise.store";
import { ref, watch } from "vue";
import exerciseDialog from "./exerciseDialog.vue";
import listItem from "@/components/atoms/listItem.vue";
const exerciseStore = useExerciseStore();
const exerciseGroups = ref<Array<ExerciseGroupModel>>([]);
const showExerciseDialog = ref(false);
exerciseStore.getAllExercises();
/**
* Extract exercise groups from all exercises
*/
function groupExercises() {
exerciseStore.exercises.forEach((exercise) => {
if (
!exerciseGroups.value.find(
(exerciseGroup) => exerciseGroup.id == exercise.exerciseGroup.id
)
) {
exerciseGroups.value.push(exercise.exerciseGroup);
}
});
}
function filterByExerciseGroup(
exercises: Array<ExerciseModel>,
group: ExerciseGroupModel
) {
return exercises.filter((exercise) => exercise.exerciseGroup.id == group.id);
}
watch(
() => exerciseStore.exercises,
() => groupExercises()
);
</script>
<template>
<card-view
:title="$t('preferences.exercises.settings')"
icon="mdi-book-open-blank-variant"
>
<template #borderless>
<v-list>
<list-item
v-for="group in exerciseGroups"
:title="getExerciseGroupNameLanguage(group)"
hover
:subtitle="
$t('preferences.exercises.available', [
filterByExerciseGroup(exerciseStore.exercises, group).filter((exercise) => exercise.available)
.length,
filterByExerciseGroup(exerciseStore.exercises, group).length,
])
"
:prepend-icon="group.icon"
/>
</v-list>
</template>
<template #actions>
<outlined-button prepend-icon="mdi-pencil" @click="showExerciseDialog = true">
{{ $t("preferences.exercises.edit") }}
</outlined-button>
</template>
</card-view>
<exercise-dialog v-model="showExerciseDialog" />
</template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import actionDialog from '@/components/organisms/actionDialog.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { useFilesStore } from '@/stores/files.store';
import { ref } from 'vue';
@@ -46,7 +46,7 @@ const test = ref()
</v-row>
</v-container>
<v-btn type="submit">Submit</v-btn>
<v-btn type="submit">{{ $t('misc.submit') }}</v-btn>
</v-form>
<template #actions>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useBandStore } from '@/stores/band.store';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
const bandStore = useBandStore()
</script>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import actionDialog from '@/components/organisms/actionDialog.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { getStringRules } from '@/scripts/validationRules';
import { useGenreStore } from '@/stores/genre.store';
import { ref } from 'vue';

View File

@@ -50,7 +50,7 @@ defineProps({
</v-col>
<v-col cols="8">
<v-col cols="12" md="10">
<!-- Title -->
<v-skeleton-loader
type="heading"

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import cardView from '@/components/molecules/cardView.vue';
import OutlinedButton from "@/components/atoms/outlinedButton.vue";
</script>
<template>
<card-view
:title="$t('preferences.importExport.title')"
icon="mdi-swap-horizontal-bold"
>
<v-row>
<v-col>
<v-file-input
:label="$t('preferences.importExport.selectConfigFile')"
variant="outlined"
accept=".json"
hide-details
/>
</v-col>
</v-row>
<template #actions>
<outlined-button prepend-icon="mdi-export">
{{ $t("preferences.importExport.download") }}
</outlined-button>
<outlined-button prepend-icon="mdi-upload" color="green">
{{ $t("preferences.importExport.upload") }}
</outlined-button>
</template>
</card-view>
</template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { useLocationStore } from '@/stores/location.store';
import cardViewHorizontal from '@/components/basics/cardViewHorizontal.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import concertListItem from '@/components/pageParts/concertListItem.vue';
import cardViewHorizontal from '@/components/molecules/cardViewHorizontal.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
import concertListItem from '@/components/organisms/concertListItem.vue';
const locationStore = useLocationStore()
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import cardViewTopImage from '../basics/cardViewTopImage.vue';
import cardViewTopImage from '../molecules/cardViewTopImage.vue';
import { useRouter } from 'vue-router';
import { LocationModel } from '@/data/models/locations/locationModel';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useLocationStore } from '@/stores/location.store';
import seatPlanMap from '@/components/seatPlanMap/seatPlanMap.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import seatPlanMap from '@/components/organisms/seatPlanMap.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
const locationStore = useLocationStore()
</script>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import cardView from '@/components/molecules/cardView.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { useAccountStore } from '@/stores/account.store';
import { watch } from 'vue';
import { useRouter } from 'vue-router';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import navigationPrependItems from './navigationPrependItems.vue';
import navigationAppendItems from './navigationAppendItems.vue';
import navigationPrependItems from '../molecules/navigationPrependItems.vue';
import navigationAppendItems from '../molecules/navigationAppendItems.vue';
</script>
<template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import actionDialog from '@/components/organisms/actionDialog.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { useOrderStore } from '@/stores/order.store';
import moment from 'moment';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import ticketListItem from '@/components/pageParts/ticketListItem.vue';
import cardView from '@/components/molecules/cardView.vue';
import ticketListItem from '@/components/organisms/ticketListItem.vue';
import { OrderApiModel } from '@/data/models/apiEndpoints/orderApiModel';
import moment from 'moment';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import actionDialog from '@/components/organisms/actionDialog.vue';
import { useBasketStore } from '@/stores/basket.store';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { ModelRef, ref } from 'vue';
import { useAccountStore } from '@/stores/account.store';
import { AddressModel } from '@/data/models/user/addressModel';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ThemeEnum } from '@/data/enums/themeEnums';
import cardView from '@/components/basics/cardView.vue';
import cardView from '@/components/molecules/cardView.vue';
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import actionDialog from '@/components/organisms/actionDialog.vue';
import OutlinedButton from '@/components/atoms/outlinedButton.vue';
import { getIbanRules, getStringRules } from '@/scripts/validationRules';
import { useAccountStore } from '@/stores/account.store';
import cardViewOneLine from '@/components/basics/cardViewOneLine.vue';
import cardViewOneLine from '@/components/molecules/cardViewOneLine.vue';
import { ref } from 'vue';
const valid = ref(false)

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { RatingModel } from '@/data/models/acts/ratingModel';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
defineProps({
/**
@@ -26,7 +26,7 @@ defineProps({
</v-row>
<v-row>
<v-col>
<v-col cols="12" md="6">
<div class="d-flex align-center justify-center flex-column" style="height: 100%;">
<div class="text-h2 mt-5">
{{ rating.toFixed(1) }}
@@ -45,7 +45,7 @@ defineProps({
</div>
</v-col>
<v-col>
<v-col cols="12" md="6">
<v-list style="background-color: transparent;">
<v-list-item
v-for="ratingValue in ratings"

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import cardView from '@/components/molecules/cardView.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { useAccountStore } from '@/stores/account.store';
import { getEmailRules, getPasswordRules, getStringRules } from '@/scripts/validationRules';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import cardView from '@/components/molecules/cardView.vue';
import { useSearchStore } from '@/stores/search.store';
const searchStore = useSearchStore()

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { SeatGroupModel } from '@/data/models/locations/seatGroupModel';
import seatGroupTable from './seatGroupTable.vue';
import standingArea from './standingArea.vue';
import standingArea from '@/components/organisms/standingArea.vue';
import { ConcertModel } from '@/data/models/acts/concertModel';
let props = defineProps({

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import ServerStateText from '@/components/organisms/serverStateText.vue';
</script>
<template>
<v-container class="text-h4 text-center">
<v-row>
<v-col>
<v-icon icon="mdi-server" />
</v-col>
</v-row>
<v-row>
<v-col>
<div>
{{ $t('preferences.serverState') + ':' }}
</div>
<server-state-text />
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import { ThemeEnum } from '@/data/enums/themeEnums';
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()
const themeEnums = Object.values(ThemeEnum)
</script>
<template>
<v-container width="600" class="text-h4 text-center">
<v-row>
<v-col>
<v-icon icon="mdi-palette" />
</v-col>
</v-row>
<v-row>
<v-col>
{{ $t('misc.firstStartup.lookAndFeel') }}
</v-col>
</v-row>
<v-row>
<v-col>
<v-select
v-model="preferencesStore.theme"
:items="themeEnums"
:label="$t('preferences.selectedTheme')"
variant="outlined"
hide-details
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-select
v-model="preferencesStore.language"
:items="$i18n.availableLocales"
:label="$t('preferences.language')"
variant="outlined"
hide-details
/>
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()
</script>
<template>
<v-container width="600" class="text-h4 text-center">
<v-row>
<v-col>
<v-icon icon="mdi-database" />
</v-col>
</v-row>
<v-row>
<v-col>
{{ $t('misc.firstStartup.createDatabase') }}
</v-col>
</v-row>
<v-row>
<v-col v-if="preferencesStore.fetchInProgress">
<v-progress-linear indeterminate />
</v-col>
<v-col v-else class="text-green">
<v-icon icon="mdi-check" /> {{ $t('misc.firstStartup.finished') }}
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { getRegisterNumberRules, getStringRules } from '@/scripts/validationRules';
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()
</script>
<template>
<v-container class="px-0 py-2" width="600">
<v-row>
<v-col class="text-h4 text-center">
<v-icon icon="mdi-account" />
</v-col>
</v-row>
<v-row>
<v-col class="text-h4 text-center">
{{ $t('misc.firstStartup.userData') }}
</v-col>
</v-row>
<v-row>
<v-col>
<v-alert color="warning" icon="mdi-alert">
{{ $t('misc.firstStartup.enterYourPersonalData') }}
</v-alert>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="outlined"
:label="$t('misc.yourFullName')"
v-model="preferencesStore.studentName"
:rules="getStringRules(4)"
hide-details
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="outlined"
:label="$t('misc.registrationNumber')"
v-model="preferencesStore.registrationNumber"
:rules="getRegisterNumberRules()"
hide-details
/>
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -1,27 +1,24 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import confirmDialog from '@/components/basics/confirmDialog.vue';
import { ServerStateEnum } from '@/data/enums/serverStateEnum';
import { usePreferencesStore } from '@/stores/preferences.store';
import ServerStateText from '@/components/pageParts/serverStateText.vue';
import { useRouter } from 'vue-router';
import cardView from "@/components/molecules/cardView.vue";
import outlinedButton from "@/components/atoms/outlinedButton.vue";
import confirmDialog from "@/components/organisms/confirmDialog.vue";
import { ServerStateEnum } from "@/data/enums/serverStateEnum";
import { usePreferencesStore } from "@/stores/preferences.store";
import ServerStateText from "@/components/organisms/serverStateText.vue";
import { useRouter } from "vue-router";
const preferenceStore = usePreferencesStore()
const router = useRouter()
const preferenceStore = usePreferencesStore();
const router = useRouter();
preferenceStore.getServerState()
preferenceStore.getServerState();
</script>
<template>
<card-view
:title="$t('preferences.systemSetup')"
icon="mdi-engine"
>
<card-view :title="$t('preferences.systemSetup')" icon="mdi-engine">
<template #borderless>
<v-list>
<v-list-item class="text-h6 text-center">
{{ $t('preferences.serverState') }}: <server-state-text />
{{ $t("preferences.serverState") + ":" }} <server-state-text />
</v-list-item>
<v-list-item class="text-center">
@@ -29,9 +26,12 @@ preferenceStore.getServerState()
@click="preferenceStore.showDeleteDbDialog = true"
prepend-icon="mdi-database-refresh"
color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress"
:disabled="
preferenceStore.serverState != ServerStateEnum.ONLINE ||
preferenceStore.fetchInProgress
"
>
{{ $t('preferences.resetDatabase.resetDatabase') }}
{{ $t("preferences.resetDatabase.resetDatabase") }}
</outlined-button>
</v-list-item>
@@ -40,9 +40,12 @@ preferenceStore.getServerState()
@click="preferenceStore.showDeleteExerciseDialog = true"
prepend-icon="mdi-progress-close"
color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress"
:disabled="
preferenceStore.serverState != ServerStateEnum.ONLINE ||
preferenceStore.fetchInProgress
"
>
{{ $t('preferences.resetExerciseProgress.resetExerciseProgress') }}
{{ $t("preferences.resetExerciseProgress.resetExerciseProgress") }}
</outlined-button>
</v-list-item>
@@ -51,9 +54,12 @@ preferenceStore.getServerState()
@click="preferenceStore.showFactoryResetDialog = true"
prepend-icon="mdi-factory"
color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress"
:disabled="
preferenceStore.serverState != ServerStateEnum.ONLINE ||
preferenceStore.fetchInProgress
"
>
{{ $t('preferences.factoryReset.factoryReset') }}
{{ $t("preferences.factoryReset.factoryReset") }}
</outlined-button>
</v-list-item>
</v-list>
@@ -85,10 +91,12 @@ preferenceStore.getServerState()
:description="$t('preferences.factoryReset.dialog.description')"
v-model="preferenceStore.showFactoryResetDialog"
icon="mdi-factory"
:onConfirm="() => {
preferenceStore.resetToFactorySettings()
router.push('/')
}"
:onConfirm="
() => {
preferenceStore.resetToFactorySettings();
router.push('/');
}
"
:loading="preferenceStore.fetchInProgress"
/>
</template>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { ConcertModel } from '@/data/models/acts/concertModel';
import cardWithLeftImage from '../basics/cardViewHorizontal.vue';
import cardWithLeftImage from '../molecules/cardViewHorizontal.vue';
import { dateStringToHumanReadableString } from '@/scripts/dateTimeScripts';
import { BandModel } from '@/data/models/acts/bandModel';
import { LocationModel } from '@/data/models/locations/locationModel';
import { CityModel } from '@/data/models/locations/cityModel';
import cardViewOneLine from '../basics/cardViewOneLine.vue';
import cardViewOneLine from '../molecules/cardViewOneLine.vue';
defineProps({
concert: {

View File

@@ -44,19 +44,19 @@ function removeFromBasket(basketItem: BasketItemModel) {
<!-- Quantity -->
<td class="text-center">
{{ basketItem.seats.length }}x
{{ basketItem.seats.length + 'x' }}
</td>
<!-- Price per event -->
<td class="text-right">
<div v-if="basketItem.seats">
{{ basketItem.price.toFixed(2) }}
{{ basketItem.price.toFixed(2) + '€' }}
</div>
</td>
<!-- Total price -->
<td class="text-right">
{{ (calcPrice(basketItem.concert.price, basketItem.seats.length)).toFixed(2) }}
{{ (calcPrice(basketItem.concert.price, basketItem.seats.length)).toFixed(2) + '€' }}
</td>
<td class="text-right">

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import CardView from "@/components/molecules/cardView.vue";
import CardViewOneLine from "@/components/molecules/cardViewOneLine.vue";
import SectionDivider from "@/components/molecules/sectionDivider.vue";
import { GenreApiModel } from "@/data/models/acts/genreApiModel";
import { useGenreStore } from "@/stores/genre.store";
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
import outlinedButton from "@/components/atoms/outlinedButton.vue";
const genreStore = useGenreStore();
const genresByNumberOfBands = ref<Array<GenreApiModel>>([]);
const router = useRouter();
genreStore.getGenres();
watch(
() => genreStore.genres,
() => {
genresByNumberOfBands.value = genreStore.genres;
genresByNumberOfBands.value.sort((a, b) => {
return b.bands.length - a.bands.length;
});
}
);
</script>
<template>
<v-row>
<v-col>
<section-divider :title="$t('genre.popular')" />
</v-col>
</v-row>
<v-row>
<v-col v-if="genreStore.fetchInProgress" v-for="n in 4" cols="6" md="">
<v-skeleton-loader :loading="true" type="card" />
</v-col>
<v-col v-else v-for="genre in genreStore.topGenres" cols="6" md="3">
<card-view
@click="router.push({ path: '/bands', query: { genreName: genre.name }})"
:title="genre.name"
:subtitle="genre.bands.length + ' ' + $t('band.band', genre.bands.length)"
/>
</v-col>
</v-row>
<!-- todo?
<v-row>
<v-col>
<outlined-button
append-icon="mdi-chevron-right"
@click="router.push('/')"
block
>
{{ $t('genre.allGenres') }}
</outlined-button>
</v-col>
</v-row> -->
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import outlinedButton from '@/components/basics/outlinedButton.vue';
import cardViewTopImage from '@/components/basics/cardViewTopImage.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import cardViewTopImage from '@/components/molecules/cardViewTopImage.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
import { useRouter } from 'vue-router';
import { useLocationStore } from '@/stores/location.store';

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { useConcertStore } from '@/stores/concert.store';
import { useRouter } from 'vue-router';
import cardViewTopImage from '@/components/basics/cardViewTopImage.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import cardViewTopImage from '@/components/molecules/cardViewTopImage.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import sectionDivider from '@/components/molecules/sectionDivider.vue';
import moment from 'moment';
const concertStore = useConcertStore()

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import actionDialog from '@/components/organisms/actionDialog.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { useFeedbackStore } from '@/stores/feedback.store';
import { usePreferencesStore } from '@/stores/preferences.store';
import { ref, watch } from 'vue';
import step1 from './step1.vue';
import step2 from './step2.vue';
import step3 from './step3.vue';
import step4 from './step4.vue';
const preferencesStore = usePreferencesStore()
const feedbackStore = useFeedbackStore()
const showDialog = defineModel()
const currentStep = ref(1)
const databaseCreated = ref(false)
const steps = [
feedbackStore.i18n.t('misc.firstStartup.connectToServer'),
feedbackStore.i18n.t('misc.firstStartup.lookAndFeel'),
feedbackStore.i18n.t('misc.firstStartup.database'),
feedbackStore.i18n.t('misc.firstStartup.userData'),
]
preferencesStore.getServerState()
watch(() => currentStep.value, async () => {
if (currentStep.value == 3 && !databaseCreated.value) {
await preferencesStore.resetDb();
await preferencesStore.resetExerciseProg();
databaseCreated.value = true;
}
})
</script>
<template>
<action-dialog
v-model="showDialog"
:title="$t('misc.firstStartup.title')"
icon="mdi-human-greeting"
max-width="800"
persistent
>
<v-stepper
v-model="currentStep"
alt-labels
flat
>
<template #default="{ prev, next }">
<!-- Header items -->
<v-stepper-header>
<template v-for="(step, n) in steps">
<v-stepper-item
:complete="currentStep > n + 1"
:title="step"
:value="n + 1"
complete-icon="mdi-check"
color="success"
/>
<v-divider v-if="n < steps.length - 1" />
</template>
</v-stepper-header>
<!-- Content -->
<v-stepper-window>
<!-- Step 1: Check connection to backend server -->
<v-stepper-window-item
:value="1"
>
<step1 />
</v-stepper-window-item>
<!-- Step 2: Select theme and language -->
<v-stepper-window-item
:value="2"
>
<step2 />
</v-stepper-window-item>
<!-- Step 3: Reset the database -->
<v-stepper-window-item
:value="3"
>
<step3 />
</v-stepper-window-item>
<!-- Step 4: Personal data -->
<v-stepper-window-item
:value="4"
>
<step4 />
</v-stepper-window-item>
</v-stepper-window>
<!-- Next/Previous buttons -->
<v-stepper-actions
@click:next="next"
@click:prev="prev"
>
<template #prev="{ props }">
<outlined-button
@click="props.onClick()"
:disabled="currentStep == 1 || preferencesStore.fetchInProgress"
color="grey"
prepend-icon="mdi-arrow-left"
>
{{ $t('misc.actions.back') }}
</outlined-button>
</template>
<template #next="{ props }">
<outlined-button
v-if="currentStep < steps.length"
@click="props.onClick()"
:disabled="preferencesStore.fetchInProgress"
append-icon="mdi-arrow-right"
>
{{ $t('misc.actions.next') }}
</outlined-button>
<outlined-button
v-else
@click="showDialog = false; preferencesStore.firstStartup = false"
:disabled="preferencesStore.studentName.length < 5 ||
preferencesStore.registrationNumber.length < 8"
append-icon="mdi-check"
color="success"
>
{{ $t('misc.firstStartup.complete') }}
</outlined-button>
</template>
</v-stepper-actions>
</template>
</v-stepper>
</action-dialog>
</template>

View File

@@ -2,6 +2,7 @@ export class ExerciseGroupModel {
id = -1
nameDe: string = ""
nameEn: string = ""
icon: string = ""
groupNr: number = 0
descriptionDe: string = ""
descriptionEn: string = ""

View File

@@ -1,7 +1,8 @@
import { ExerciseGroupModel } from "./exerciseGroupModel"
export class ExerciseModel {
id = -1
id: number = -1
uuid: string = ""
nameDe: string = ""
nameEn: string = ""
exerciseNr: number = 0
@@ -9,4 +10,5 @@ export class ExerciseModel {
descriptionEn: string = ""
solved: boolean = false
exerciseGroup: ExerciseGroupModel
available: boolean = true
}

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
const router = useRouter()
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import outlinedButton from '@/components/basics/outlinedButton.vue';
import outlinedButton from '@/components/atoms/outlinedButton.vue';
import { useRouter } from 'vue-router';
const fetchInProgress = defineModel("fetchInProgress", { default: false })

View File

@@ -97,7 +97,8 @@
"postalCode": "Postleitzahl",
"placeOfResidence": "Wohnort",
"bankName": "Name der Bank",
"iban": "IBAN"
"iban": "IBAN",
"actions": "Aktionen"
},
"deleteAccount": {
"deleteAccount": "Account löschen",
@@ -130,7 +131,8 @@
"noOrdersText": "Bisher wurden keine Bestellungen von diesem Account getätigt. Gehe zum Warenkorb und bestelle!",
"ordersDescription": "Übersicht aller getätigten Bestellungen",
"order": "Bestellung | Bestellungen",
"notShipped": "noch nicht versendet"
"notShipped": "noch nicht versendet",
"orderState": "Bestellstatus"
},
"basket": {
"addToBasket": "Zum Warenkorb hinzufügen",
@@ -166,12 +168,26 @@
"title": "Auf Werkseinstellungen zurücksetzen?",
"description": "Sollen alle Einstellungen und Daten auf Werkseinstellungen zurückgesetzt werden? Alle Änderungen und Fortschritte gehen verloren!"
}
}
},
"exercises": {
"available": "{0} von {1} Aufgaben verfügbar",
"edit": "Verfügbare Aufgaben bearbeiten",
"settings": "Aufgaben-Konfiguration"
},
"importExport": {
"title": "Import/Export Konfiguration",
"selectConfigFile": "Konfigurations-Datei auswählen",
"download": "Konfiguration exportieren",
"upload": "Datei hochladen"
},
"preferences": "Einstellungen"
},
"help": {
"scoreBoard": {
"exerciseGroupNr": "Aufgabengruppe {0}: ",
"exerciseNr": "Aufgabe {0}.{1}: "
"exerciseNr": "Aufgabe {0}.{1}: ",
"generatePdf": "PDF generieren",
"personalSolutionKey": "Persönlicher Lösungsschlüssel"
}
},
"bannerMessages": {
@@ -225,6 +241,7 @@
"cancel": "Abbrechen",
"more": "Mehr",
"confirm": "Bestätigen",
"back": "Zurück",
"next": "Weiter"
},
"validation": {
@@ -241,7 +258,7 @@
"firstStartup": {
"title": "Ersteinrichtung",
"description": "Die Datenbank wird eingerichtet. Bitte warten...",
"createDatabase": "Erstelle Datenbank...",
"createDatabase": "Datenbank Einrichtung",
"complete": "Fertig",
"finished": "Abgeschlossen",
"createExercises": "Erstelle Aufgaben...",
@@ -249,6 +266,7 @@
"database": "Datenbank",
"exercises": "Aufgaben",
"userData": "Persönliche Daten",
"lookAndFeel": "Look and feel",
"enterYourPersonalData": "Bitte gebe nun deinen Namen und deine Matrikelnummer von der Universität ein. Überprüfe die Angaben vor dem Absenden genau! Die Angaben können später nicht ohne Verlust des Bearbeitungsfortschrittes geändert werden!"
},
"user": "Angaben zur Person",
@@ -263,10 +281,26 @@
"empty": {
"headline": "So leer hier..."
},
"searchterm": "Suchbegriff"
}
"searchterm": "Suchbegriff",
"globalsearch": "Globale Suche"
},
"submit": "Absenden",
"content": "Inhalt",
"source": "Quelle",
"softwareVersion": "Software Version",
"license": "Lizenz",
"developer": "Entwickler",
"developedFor": "Entwickelt im Auftrag",
"copyright": "Copyright",
"githubRepository": "GitHub Repository",
"projectPage": "Projektseite"
},
"genre": {
"withoutBand": "ohne Band"
"withoutBand": "ohne Band",
"popular": "Beliebte Genres",
"allGenres": "Alle Genres"
},
"admin": {
"adminpanel": "Admin Panel"
}
}

View File

@@ -97,7 +97,8 @@
"postalCode": "Postal code",
"placeOfResidence": "Place of residence",
"bankName": "Name of bank",
"iban": "IBAN"
"iban": "IBAN",
"actions": "Actions"
},
"deleteAccount": {
"deleteAccount": "Delete Account",
@@ -127,10 +128,11 @@
"takeOrder": "Execute order",
"noOrders": "No orders found",
"orderedAt": "Ordered at",
"noOrdersText": "Bisher wurden keine Bestellungen von diesem Account getätigt. Gehe zum Warenkorb und bestelle!",
"noOrdersText": "No orders have been placed with this account yet. Go to your shopping cart and place your order!",
"ordersDescription": "Overview of all placed orders",
"order": "Order | Orders",
"notShipped": "don't shipped"
"notShipped": "don't shipped",
"orderState": "Order state"
},
"basket": {
"addToBasket": "Add to basket",
@@ -166,12 +168,28 @@
"title": "Factory reset?",
"description": "Do you really want to reset everything? Every change will be lost!"
}
}
},
"exercises": {
"settings": "Exercise Configuration",
"available": "{0} of {1} exercises are available",
"uploadExerciseConfig": "Upload exercise config",
"edit": "Edit available exercises",
"upload": "Upload exercises config"
},
"importExport": {
"title": "Import/Export config",
"selectConfigFile": "Select config file",
"upload": "Upload file",
"download": "Export config"
},
"preferences": "Preferences"
},
"help": {
"scoreBoard": {
"exerciseGroupNr": "Exercise Group {0}: ",
"exerciseNr": "Exercise {0}.{1}: "
"exerciseNr": "Exercise {0}.{1}: ",
"generatePdf": "Generate PDF",
"personalSolutionKey": "Personal solution key"
}
},
"bannerMessages": {
@@ -225,6 +243,7 @@
"cancel": "Cancel",
"more": "More",
"confirm": "Confirm",
"back": "Back",
"next": "Next"
},
"validation": {
@@ -241,14 +260,13 @@
"firstStartup": {
"title": "First startup",
"description": "Creating database. Please wait...",
"createDatabase": "Create Database...",
"createDatabase": "Create Database",
"complete": "Complete",
"createExercises": "Create Exercises...",
"finished": "Finished",
"connectToServer": "Server",
"database": "Database",
"exercises": "Exercises",
"userData": "User data",
"lookAndFeel": "Look and feel",
"enterYourPersonalData": "Please enter your name and your Matrikel number from your university. Check it twice! You can't change it later without loosing your exercise progress!"
},
"user": "About person",
@@ -263,10 +281,26 @@
"empty": {
"headline": "So empty here..."
},
"searchterm": "Search term"
}
"searchterm": "Search term",
"globalsearch": "Global Search"
},
"submit": "Submit",
"content": "Content",
"source": "Source",
"softwareVersion": "Software Version",
"license": "License",
"developer": "Developer",
"developedFor": "Developed for",
"copyright": "Copyright",
"githubRepository": "GitHub Repository",
"projectPage": "Project page"
},
"genre": {
"withoutBand": "without Band"
"withoutBand": "without Band",
"popular": "Popular Genres",
"allGenres": "All Genres"
},
"admin": {
"adminpanel": "Admin Panel"
}
}

View File

@@ -2,7 +2,7 @@
import dataLayout from '@/layouts/dataLayout.vue';
import { useAccountStore } from '@/stores/account.store';
import { useFeedbackStore } from '@/stores/feedback.store';
import addressEditDialog from './addressEditDialog.vue';
import addressEditDialog from '@/components/organisms/addressEditDialog.vue';
const accountStore = useAccountStore()
const feedbackStore = useFeedbackStore()
@@ -12,7 +12,7 @@ const headers = [
{ title: feedbackStore.i18n.t('account.userData.houseNumber'), value: "houseNumber" },
{ title: feedbackStore.i18n.t('account.userData.postalCode'), value: "postalCode" },
{ title: feedbackStore.i18n.t('account.userData.placeOfResidence'), value: "city" },
{ title: "Aktionen", value: "actions", width: 130 }
{ title: feedbackStore.i18n.t('account.userData.actions'), value: "actions", width: 130 }
]
accountStore.refreshAccount()

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { useAccountStore } from '@/stores/account.store';
import dashboardCard from '@/components/pageParts/dashboardCard.vue';
import dashboardCard from '@/components/organisms/dashboardCard.vue';
import { useOrderStore } from '@/stores/order.store';
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import OutlinedButton from '@/components/atoms/outlinedButton.vue';
import { useRouter } from 'vue-router';
import moment from 'moment';
import { millisecondsToHumanReadableString } from '@/scripts/dateTimeScripts';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import loginForm from './loginForm.vue';
import registerForm from './registerForm.vue';
import loginForm from '@/components/organisms/loginForm.vue';
import registerForm from '@/components/organisms/registerForm.vue';
const showRegisterCard = ref(false)
</script>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { useAccountStore } from '@/stores/account.store';
import orderItem from './orderItem.vue';
import orderItem from '@/components/organisms/orderItem.vue';
import accountSubPageLayout from '@/layouts/accountSubPageLayout.vue';
import circularProgressIndeterminate from '@/components/basics/circularProgressIndeterminate.vue';
import circularProgressIndeterminate from '@/components/atoms/circularProgressIndeterminate.vue';
import { useOrderStore } from '@/stores/order.store';
const accountStore = useAccountStore()

View File

@@ -2,7 +2,7 @@
import dataLayout from '@/layouts/dataLayout.vue';
import { useAccountStore } from '@/stores/account.store';
import { useFeedbackStore } from '@/stores/feedback.store';
import PaymentEditDialog from './paymentEditDialog.vue';
import paymentEditDialog from '@/components/organisms/paymentEditDialog.vue';
const accountStore = useAccountStore()
const feedbackStore = useFeedbackStore()
@@ -10,7 +10,7 @@ const feedbackStore = useFeedbackStore()
const headers = [
{ title: feedbackStore.i18n.t('account.userData.bankName'), value: "bankName" },
{ title: feedbackStore.i18n.t('account.userData.iban'), value: "iban" },
{ title: "Aktionen", value: "actions", width: 130 }
{ title: feedbackStore.i18n.t('account.userData.actions'), value: "actions", width: 130 }
]
accountStore.refreshAccount()

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import accountDataCard from './accountDataCard.vue';
import accountManagingCard from './accountManagingCard.vue';
import accountDataCard from '@/components/organisms/accountDataCard.vue';
import accountManagingCard from '@/components/organisms/accountManagingCard.vue';
import { useRouter } from 'vue-router';
import accountSubPageLayout from '@/layouts/accountSubPageLayout.vue';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useBandStore } from '@/stores/band.store';
import bandEditDialog from './bandEditDialog.vue';
import bandEditDialog from '@/components/organisms/bandEditDialog.vue';
import dataLayout from '@/layouts/dataLayout.vue';
import { useFeedbackStore } from '@/stores/feedback.store';

View File

@@ -40,7 +40,7 @@ concertStore.getConcerts()
</template>
<template #item.price="{ item }">
{{ item.price.toFixed(2) }}
{{ item.price.toFixed(2) + '€' }}
</template>
<template #item.image="{ item }">

View File

@@ -5,7 +5,7 @@ import { useAccountStore } from '@/stores/account.store';
import { useLocationStore } from '@/stores/location.store';
import { useGenreStore } from '@/stores/genre.store';
import { usePreferencesStore } from '@/stores/preferences.store';
import dashboardCard from '../../../components/pageParts/dashboardCard.vue';
import dashboardCard from '@/components/organisms/dashboardCard.vue';
import { useOrderStore } from '@/stores/order.store';
import { useFilesStore } from '@/stores/files.store';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import dataLayout from '@/layouts/dataLayout.vue';
import { ref } from 'vue';
import FileUploadDialog from './fileUploadDialog.vue';
import fileUploadDialog from '@/components/organisms/fileUploadDialog.vue';
import { useFilesStore } from '@/stores/files.store';
const filesStore = useFilesStore()
@@ -67,7 +67,7 @@ filesStore.getStaticFolders()
v-if="filesStore.selectedFile != undefined && filesStore.selectedFile.name.endsWith('js')"
:model-value="filesStore.selectedFile.content"
variant="outlined"
label="Content"
:label="$t('misc.content')"
height="300"
rows="30"
/>
@@ -104,7 +104,7 @@ filesStore.getStaticFolders()
prepend-icon="mdi-web"
v-if="filesStore.selectedFile.copyright.url.length > 0"
>
<a :href="filesStore.selectedFile.copyright.url" target="_blank" >Quelle</a>
<a :href="filesStore.selectedFile.copyright.url" target="_blank" >{{ $t('misc.source') }}</a>
</v-list-item>
</template>
</v-list>

View File

@@ -1,13 +1,15 @@
<script setup lang="ts">
import dataLayout from '@/layouts/dataLayout.vue';
import genreEditDialog from './genreEditDialog.vue';
import genreEditDialog from '@/components/organisms/genreEditDialog.vue';
import { useGenreStore } from '@/stores/genre.store';
import { useFeedbackStore } from '@/stores/feedback.store';
const genreStore = useGenreStore()
const feedbackStore = useFeedbackStore()
const headers = [
{ title: "Name", value: "name" },
{ title: "Bands", value: "bands" },
{ title: feedbackStore.i18n.t('band.genre'), value: "name" },
{ title: feedbackStore.i18n.t('band.name'), value: "bands" },
{ title: "", value: "edit", width: 130 }
]

View File

@@ -2,18 +2,20 @@
import dataLayout from '@/layouts/dataLayout.vue';
import { useOrderStore } from '@/stores/order.store';
import moment from 'moment';
import OrderDetailDialog from './orderDetailDialog.vue';
import orderDetailDialog from '@/components/organisms/orderDetailDialog.vue';
import { useFeedbackStore } from '@/stores/feedback.store';
const orderStore = useOrderStore()
const feedbackStore = useFeedbackStore()
const headers = [
{ title: "Account", value: "account.username" },
{ title: "Name", value: "account" },
{ title: "Bestellt am", value: "orderedAt" },
{ title: "Adresse", value: "street" },
{ title: "Stadt", value: "city" },
{ title: "Versendet", value: "shipped" },
{ title: "Aktionen", value: "edit", width: 130 }
{ title: feedbackStore.i18n.t('account.userData.username'), value: "account.username" },
{ title: feedbackStore.i18n.t('account.userData.firstName'), value: "account" },
{ title: feedbackStore.i18n.t('order.orderedAt'), value: "orderedAt" },
{ title: feedbackStore.i18n.t('account.userData.address'), value: "street" },
{ title: feedbackStore.i18n.t('account.userData.placeOfResidence'), value: "city" },
{ title: feedbackStore.i18n.t('order.orderState'), value: "shipped" },
{ title: "", value: "edit", width: 130 }
]
orderStore.getAllOrders()

View File

@@ -1,15 +1,19 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import ratingSection from './ratingSection.vue';
import bandMemberSection from './bandMemberSection.vue';
import gallerySection from './gallerySection.vue';
import concertSection from './concertSection.vue';
import heroImage from '@/components/pageParts/heroImage.vue';
import ratingSection from '../components/organisms/ratingSection.vue';
import bandMemberSection from '../components/organisms/bandMemberSection.vue';
import gallerySection from '../components/organisms/gallerySection.vue';
import concertSection from '../components/organisms/concertSection.vue';
import heroImage from '@/components/organisms/heroImage.vue';
import { useBandStore } from '@/stores/band.store';
import { onMounted, watch } from 'vue';
import { useConcertStore } from '@/stores/concert.store';
const router = useRouter()
const bandStore = useBandStore()
const concertStore = useConcertStore()
concertStore.getConcerts()
onMounted(async () => {
bandStore.getBand(String(router.currentRoute.value.params.name).replaceAll('-', ' '))
@@ -34,7 +38,7 @@ watch(() => router.currentRoute.value.params.name, () => {
<v-row>
<v-spacer />
<v-col cols="10">
<v-col cols="12" md="10">
<concert-section />
<band-member-section />

Some files were not shown because too many files have changed in this diff Show More