4 Commits

14 changed files with 162 additions and 107 deletions

View File

@@ -1,4 +1,4 @@
# v.0.4.0 (2025-08-30) # v.0.4.0 MuC-Edition (2025-09-01)
## 🚀 Features ## 🚀 Features
@@ -12,7 +12,13 @@
- Icons on exercise groups on help page - Icons on exercise groups on help page
- Welcome dialog: New page for look and feel, merge database and exercise creation in one step - 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 - 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) # v.0.3.0 (2025-02-28)
## 🚀 Features ## 🚀 Features

View File

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

View File

@@ -37,7 +37,6 @@
"!dist", "!dist",
"!out", "!out",
"!misc", "!misc",
"!database.sqlite", "!database.sqlite"
"!node_modules"
] ]
} }

View File

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

View File

@@ -297,7 +297,8 @@
}, },
"genre": { "genre": {
"withoutBand": "ohne Band", "withoutBand": "ohne Band",
"popular": "Beliebte Genres" "popular": "Beliebte Genres",
"allGenres": "Alle Genres"
}, },
"admin": { "admin": {
"adminpanel": "Admin Panel" "adminpanel": "Admin Panel"

View File

@@ -297,7 +297,8 @@
}, },
"genre": { "genre": {
"withoutBand": "without Band", "withoutBand": "without Band",
"popular": "Popular Genres" "popular": "Popular Genres",
"allGenres": "All Genres"
}, },
"admin": { "admin": {
"adminpanel": "Admin Panel" "adminpanel": "Admin Panel"

View File

@@ -7,9 +7,13 @@ import concertSection from './concertSection.vue';
import heroImage from '@/components/pageParts/heroImage.vue'; import heroImage from '@/components/pageParts/heroImage.vue';
import { useBandStore } from '@/stores/band.store'; import { useBandStore } from '@/stores/band.store';
import { onMounted, watch } from 'vue'; import { onMounted, watch } from 'vue';
import { useConcertStore } from '@/stores/concert.store';
const router = useRouter() const router = useRouter()
const bandStore = useBandStore() const bandStore = useBandStore()
const concertStore = useConcertStore()
concertStore.getConcerts()
onMounted(async () => { onMounted(async () => {
bandStore.getBand(String(router.currentRoute.value.params.name).replaceAll('-', ' ')) bandStore.getBand(String(router.currentRoute.value.params.name).replaceAll('-', ' '))

View File

@@ -39,8 +39,8 @@ watch(() => router.currentRoute.value.query, () => {
</v-row> </v-row>
<v-row <v-row
v-else-if="bandStore.bands.length > 0" v-else-if="bandStore.filteredBands.length > 0"
v-for="band in bandStore.bands" v-for="band in bandStore.filteredBands"
> >
<v-col> <v-col>
<band-list-item <band-list-item

View File

@@ -1,17 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useConcertStore } from '@/stores/concert.store'; import { useConcertStore } from "@/stores/concert.store";
import concertListItem from '@/components/pageParts/concertListItem.vue'; import concertListItem from "@/components/pageParts/concertListItem.vue";
import cardViewHorizontal from '@/components/basics/cardViewHorizontal.vue'; import cardViewHorizontal from "@/components/basics/cardViewHorizontal.vue";
import sectionDivider from '@/components/basics/sectionDivider.vue'; import sectionDivider from "@/components/basics/sectionDivider.vue";
import concertFilterbar from './concertFilterbar.vue'; import concertFilterbar from "./concertFilterbar.vue";
const concertStore = useConcertStore() const concertStore = useConcertStore();
</script> </script>
<template> <template>
<div <div v-if="concertStore.fetchInProgress">
v-if="concertStore.fetchInProgress"
>
<section-divider :loading="true" /> <section-divider :loading="true" />
<v-row v-for="i in 3"> <v-row v-for="i in 3">
<v-col> <v-col>
@@ -26,13 +24,21 @@ const concertStore = useConcertStore()
> >
<div v-if="concert.offered"> <div v-if="concert.offered">
<v-row <v-row
v-if="index == 0 || v-if="
index == 0 ||
new Date(concertStore.concerts[index - 1].date).getMonth() != new Date(concertStore.concerts[index - 1].date).getMonth() !=
new Date(concertStore.concerts[index].date).getMonth()" new Date(concertStore.concerts[index].date).getMonth()
"
> >
<v-col> <v-col>
<section-divider <section-divider
:title="new Date(concert.date).toLocaleString('default', { month: 'long' }) + ' ' + new Date(concert.date).getFullYear()" :title="
new Date(concert.date).toLocaleString('default', {
month: 'long',
}) +
' ' +
new Date(concert.date).getFullYear()
"
/> />
</v-col> </v-col>
</v-row> </v-row>

View File

@@ -7,7 +7,7 @@ import TopLocationsSection from "./topLocationsSection.vue";
import { usePreferencesStore } from "@/stores/preferences.store"; import { usePreferencesStore } from "@/stores/preferences.store";
import welcomeDialog from "./welcomeDialog/dialog.vue"; import welcomeDialog from "./welcomeDialog/dialog.vue";
import { ref } from "vue"; import { ref } from "vue";
import genresSection from "./genresSection.vue"; import genresSection from "./topGenresSection.vue";
const concertStore = useConcertStore(); const concertStore = useConcertStore();
const locationStore = useLocationStore(); const locationStore = useLocationStore();

View File

@@ -6,6 +6,7 @@ import { GenreApiModel } from "@/data/models/acts/genreApiModel";
import { useGenreStore } from "@/stores/genre.store"; import { useGenreStore } from "@/stores/genre.store";
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import outlinedButton from "@/components/basics/outlinedButton.vue";
const genreStore = useGenreStore(); const genreStore = useGenreStore();
const genresByNumberOfBands = ref<Array<GenreApiModel>>([]); const genresByNumberOfBands = ref<Array<GenreApiModel>>([]);
@@ -37,7 +38,7 @@ watch(
<v-skeleton-loader :loading="true" type="card" /> <v-skeleton-loader :loading="true" type="card" />
</v-col> </v-col>
<v-col v-else v-for="genre in genresByNumberOfBands" cols="6" md="3"> <v-col v-else v-for="genre in genreStore.topGenres" cols="6" md="3">
<card-view <card-view
@click="router.push({ path: '/bands', query: { genreName: genre.name }})" @click="router.push({ path: '/bands', query: { genreName: genre.name }})"
:title="genre.name" :title="genre.name"
@@ -45,4 +46,17 @@ watch(
/> />
</v-col> </v-col>
</v-row> </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> </template>

View File

@@ -12,6 +12,9 @@ export const useBandStore = defineStore("bandStore", {
/** All available bands */ /** All available bands */
bands: ref<Array<BandApiModel>>([]), bands: ref<Array<BandApiModel>>([]),
/** Available bands filtered by parameters */
filteredBands: ref<Array<BandApiModel>>([]),
/** All information about a single band */ /** All information about a single band */
band: ref<BandDetailsApiModel>(new BandDetailsApiModel()), band: ref<BandDetailsApiModel>(new BandDetailsApiModel()),
@@ -32,7 +35,9 @@ export const useBandStore = defineStore("bandStore", {
await fetchAllBands() await fetchAllBands()
.then(result => { .then(result => {
this.bands = result.data.filter((band: BandApiModel) => { this.bands = result.data
this.filteredBands = result.data.filter((band: BandApiModel) => {
if (genreStore.genre == null) { if (genreStore.genre == null) {
return true return true
} }

View File

@@ -64,6 +64,9 @@ export const useConcertStore = defineStore("concertStore", {
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore()
this.fetchInProgress = true this.fetchInProgress = true
console.log("LOcation & Date:")
console.log(this.concerts)
let id = this.concerts.find((concert: ConcertApiModel) => { let id = this.concerts.find((concert: ConcertApiModel) => {
return (concert.location.urlName == location && concert.date == date) return (concert.location.urlName == location && concert.date == date)
}).id }).id
@@ -75,6 +78,7 @@ export const useConcertStore = defineStore("concertStore", {
}) })
.catch(res => { .catch(res => {
feedbackStore.notFound = true feedbackStore.notFound = true
this.fetchInProgress = false
}) })
}, },

View File

@@ -1,4 +1,9 @@
import { deleteGenre, fetchAllGenres, patchGenre, postGenre } from "@/data/api/genreApi"; import {
deleteGenre,
fetchAllGenres,
patchGenre,
postGenre,
} from "@/data/api/genreApi";
import { GenreApiModel } from "@/data/models/acts/genreApiModel"; import { GenreApiModel } from "@/data/models/acts/genreApiModel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref } from "vue";
@@ -10,6 +15,8 @@ export const useGenreStore = defineStore("genreStore", {
/** All available genres from server */ /** All available genres from server */
genres: ref<Array<GenreApiModel>>([]), genres: ref<Array<GenreApiModel>>([]),
topGenres: ref<Array<GenreApiModel>>([]),
/** Currently selected genre */ /** Currently selected genre */
genre: ref<GenreApiModel>(null), genre: ref<GenreApiModel>(null),
@@ -17,7 +24,7 @@ export const useGenreStore = defineStore("genreStore", {
showEditDialog: ref(false), showEditDialog: ref(false),
/** Request to server sent, waiting for data response */ /** Request to server sent, waiting for data response */
fetchInProgress: ref(false) fetchInProgress: ref(false),
}), }),
actions: { actions: {
@@ -25,21 +32,29 @@ export const useGenreStore = defineStore("genreStore", {
* Get all genres from the database * Get all genres from the database
*/ */
getGenres() { getGenres() {
this.fetchInProgress = true this.fetchInProgress = true;
fetchAllGenres() fetchAllGenres().then((response) => {
.then(response => { this.genres = response.data;
this.genres = response.data
this.fetchInProgress = false let genresByNumberOfBands = this.genres.slice();
})
genresByNumberOfBands.sort((a, b) => {
return b.bands.length - a.bands.length;
});
this.topGenres = genresByNumberOfBands.splice(0, 8)
this.fetchInProgress = false;
});
}, },
/** /**
* Prepare edit dialog for new genre, opens it * Prepare edit dialog for new genre, opens it
*/ */
newGenre() { newGenre() {
this.genre = new GenreApiModel() this.genre = new GenreApiModel();
this.showEditDialog = true this.showEditDialog = true;
}, },
/** /**
@@ -48,41 +63,39 @@ export const useGenreStore = defineStore("genreStore", {
* @param genre Selected Genre object * @param genre Selected Genre object
*/ */
editGenre(genre: GenreApiModel) { editGenre(genre: GenreApiModel) {
this.genre = genre this.genre = genre;
this.showEditDialog = true this.showEditDialog = true;
}, },
/** /**
* Save edited genre to the backend server * Save edited genre to the backend server
*/ */
saveGenre() { saveGenre() {
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore();
this.fetchInProgress = true this.fetchInProgress = true;
if (this.genre.id == undefined) { if (this.genre.id == undefined) {
// Creating new Genre // Creating new Genre
postGenre(this.genre) postGenre(this.genre).then((response) => {
.then(response => {
if (response.status == 200) { if (response.status == 200) {
feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDSUCCESSFUL) feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDSUCCESSFUL);
this.getGenres() this.getGenres();
this.showEditDialog = false this.showEditDialog = false;
} else { } else {
feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDERROR) feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDERROR);
} }
}) });
} else { } else {
// Update existing Genre // Update existing Genre
patchGenre(this.genre) patchGenre(this.genre).then((response) => {
.then(response => {
if (response.status == 200) { if (response.status == 200) {
feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDSUCCESSFUL) feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDSUCCESSFUL);
this.getGenres() this.getGenres();
this.showEditDialog = false this.showEditDialog = false;
} else { } else {
feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDERROR) feedbackStore.addSnackbar(BannerStateEnum.GENRESAVEDERROR);
} }
}) });
} }
}, },
@@ -92,31 +105,30 @@ export const useGenreStore = defineStore("genreStore", {
* @param genre Genre to delete * @param genre Genre to delete
*/ */
deleteGenre(genre: GenreApiModel) { deleteGenre(genre: GenreApiModel) {
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore();
this.fetchInProgress = true this.fetchInProgress = true;
deleteGenre(genre) deleteGenre(genre).then((response) => {
.then(response => {
if (response.status == 200) { if (response.status == 200) {
feedbackStore.addSnackbar(BannerStateEnum.GENREDELETESUCCESSFUL) feedbackStore.addSnackbar(BannerStateEnum.GENREDELETESUCCESSFUL);
this.getGenres() this.getGenres();
} else { } else {
feedbackStore.addSnackbar(BannerStateEnum.GENREDELETEERROR) feedbackStore.addSnackbar(BannerStateEnum.GENREDELETEERROR);
} }
}) });
}, },
setGenreByName(name: string) { setGenreByName(name: string) {
this.genre = null this.genre = null;
name = name.replace("+", " ") name = name.replace("+", " ");
let newGenre = this.genres.find(genre => { let newGenre = this.genres.find((genre) => {
return genre.name == name return genre.name == name;
}) });
if (newGenre != undefined) { if (newGenre != undefined) {
this.genre = newGenre this.genre = newGenre;
} }
} },
} },
}) });