Merge pull request 'dev' (#49) from dev into main
All checks were successful
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Successful in 3m1s
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

Reviewed-on: #49
This commit was merged in pull request #49.
This commit is contained in:
2026-08-26 14:42:08 +02:00
10 changed files with 91 additions and 21 deletions

View File

@@ -16,6 +16,13 @@ jobs:
# Der Analyse-Workflow benötigt keine Historie. Ein flacher Checkout # Der Analyse-Workflow benötigt keine Historie. Ein flacher Checkout
# verhindert, dass der Runner das große Repository-Pack vollständig lädt. # verhindert, dass der Runner das große Repository-Pack vollständig lädt.
fetch-depth: 1 fetch-depth: 1
# Der Web-Workflow braucht keine Android-Artefakte. Insbesondere liegt
# dort ein großer Heap-Dump im aktuellen Commit, der auch bei einem
# flachen Checkout übertragen würde und den Runner-Checkout abbricht.
sparse-checkout: |
/*
!/android-app/
sparse-checkout-cone-mode: false
- name: Ensure clean workspace - name: Ensure clean workspace
run: | run: |

2
.gitignore vendored
View File

@@ -94,6 +94,8 @@ dist
/android-app/**/build/ /android-app/**/build/
/android-app/local.properties /android-app/local.properties
/android-app/gradle-local.properties /android-app/gradle-local.properties
# JVM Heap-Dumps sind lokale Diagnoseartefakte und dürfen nie ins Repository.
*.hprof
# Build output (but keep production data!) # Build output (but keep production data!)
.output .output

Binary file not shown.

View File

@@ -5,11 +5,11 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full">
<div class="flex flex-col justify-between h-full py-2"> <div class="flex flex-col justify-between h-full py-2">
<!-- Hauptmenü --> <!-- Hauptmenü -->
<div class="flex justify-between items-center"> <div class="flex min-w-0 items-center gap-3">
<!-- Logo --> <!-- Logo -->
<NuxtLink <NuxtLink
to="/" to="/"
class="flex items-center space-x-3 hover:scale-105 transition-transform" class="flex shrink-0 items-center space-x-3 origin-left hover:scale-[1.02] transition-transform"
> >
<img <img
src="~/assets/images/logos/Harheimer TC.svg" src="~/assets/images/logos/Harheimer TC.svg"
@@ -23,9 +23,12 @@
</div> </div>
</NuxtLink> </NuxtLink>
<div style="display:flex;flex-direction:column;"> <div class="flex min-w-0 flex-1 flex-col">
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden lg:flex items-center space-x-1"> <div
class="hidden min-w-0 max-w-full lg:flex lg:items-center lg:gap-1 lg:overflow-x-auto lg:whitespace-nowrap [scrollbar-color:theme(colors.primary.700)_transparent] [scrollbar-width:thin]"
aria-label="Hauptmenü horizontal scrollbar"
>
<NuxtLink <NuxtLink
to="/" to="/"
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50" class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
@@ -111,10 +114,10 @@
</NuxtLink> </NuxtLink>
</div> </div>
<div class="hidden lg:flex items-center h-6 min-w-0 border-t border-primary-700/20"> <div class="hidden min-w-0 flex-1 lg:flex items-center h-6 border-t border-primary-700/20">
<div <div
v-if="currentSubmenu" v-if="currentSubmenu"
:class="currentSubmenu === 'mannschaften' ? 'relative min-w-0 flex-1' : 'flex items-center space-x-1'" :class="currentSubmenu === 'mannschaften' ? 'relative min-w-0 flex-1' : 'flex min-w-0 items-center gap-1 overflow-x-auto whitespace-nowrap'"
> >
<!-- Newsletter Submenu --> <!-- Newsletter Submenu -->
<template v-if="currentSubmenu === 'newsletter'"> <template v-if="currentSubmenu === 'newsletter'">
@@ -189,9 +192,11 @@
<!-- Mannschaften Submenu --> <!-- Mannschaften Submenu -->
<template v-if="currentSubmenu === 'mannschaften'"> <template v-if="currentSubmenu === 'mannschaften'">
<div <div
class="flex min-w-0 items-center gap-1 overflow-x-auto whitespace-nowrap scroll-smooth pr-7 [scrollbar-color:theme(colors.primary.700)_transparent] [scrollbar-width:thin]" ref="mannschaftenSubmenuRail"
class="flex min-w-0 items-center gap-1 overflow-x-auto whitespace-nowrap scroll-smooth px-7 [scrollbar-color:theme(colors.primary.700)_transparent] [scrollbar-width:thin]"
tabindex="0" tabindex="0"
aria-label="Mannschaften-Untermenü horizontal scrollbar" aria-label="Mannschaften-Untermenü horizontal scrollbar"
@scroll.passive="updateMannschaftenSubmenuControls"
> >
<NuxtLink <NuxtLink
to="/mannschaften" to="/mannschaften"
@@ -207,7 +212,7 @@
> >
<NuxtLink <NuxtLink
:to="`/mannschaften/${mannschaft.slug}`" :to="`/mannschaften/${mannschaft.slug}`"
class="shrink-0 px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" class="shrink-0 whitespace-nowrap px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600" active-class="text-white bg-primary-600"
> >
{{ mannschaft.mannschaft }} {{ mannschaft.mannschaft }}
@@ -223,18 +228,32 @@
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/spielsysteme" to="/spielsysteme"
class="shrink-0 px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" class="shrink-0 whitespace-nowrap px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600" active-class="text-white bg-primary-600"
> >
Spielsysteme Spielsysteme
</NuxtLink> </NuxtLink>
</div> </div>
<span <button
aria-hidden="true" v-if="showMannschaftenSubmenuLeftControl"
class="pointer-events-none absolute inset-y-0 right-0 flex w-9 items-center justify-end bg-gradient-to-l from-primary-900 via-primary-900/90 to-transparent text-primary-300" type="button"
class="absolute inset-y-0 left-0 z-10 flex w-9 items-center justify-start bg-gradient-to-r from-primary-900 via-primary-900/90 to-transparent pl-1 text-primary-300 transition-colors hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-300"
aria-label="Vorherige Mannschaften anzeigen"
@pointerdown.stop
@click.stop.prevent="scrollMannschaftenSubmenuLeft"
>
</button>
<button
v-if="showMannschaftenSubmenuRightControl"
type="button"
class="absolute inset-y-0 right-0 z-10 flex w-9 items-center justify-end bg-gradient-to-l from-primary-900 via-primary-900/90 to-transparent pr-1 text-primary-300 transition-colors hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-300"
aria-label="Weitere Mannschaften anzeigen"
@pointerdown.stop
@click.stop.prevent="scrollMannschaftenSubmenuRight"
> >
</span> </button>
</template> </template>
<!-- Training Submenu --> <!-- Training Submenu -->
@@ -865,7 +884,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { Menu, X, ChevronDown } from 'lucide-vue-next' import { Menu, X, ChevronDown } from 'lucide-vue-next'
@@ -873,6 +892,9 @@ const route = useRoute()
const isMobileMenuOpen = ref(false) const isMobileMenuOpen = ref(false)
const mobileSubmenu = ref(null) const mobileSubmenu = ref(null)
const mannschaften = ref([]) const mannschaften = ref([])
const mannschaftenSubmenuRail = ref(null)
const showMannschaftenSubmenuLeftControl = ref(false)
const showMannschaftenSubmenuRightControl = ref(false)
const hasGalleryImages = ref(false) const hasGalleryImages = ref(false)
const showCmsDropdown = ref(false) const showCmsDropdown = ref(false)
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -910,6 +932,38 @@ const toggleMobileSubmenu = (menu) => {
mobileSubmenu.value = mobileSubmenu.value === menu ? null : menu mobileSubmenu.value = mobileSubmenu.value === menu ? null : menu
} }
const updateMannschaftenSubmenuControls = () => {
const rail = mannschaftenSubmenuRail.value
if (!rail) return
const canScroll = rail.scrollWidth > rail.clientWidth + 1
const atStart = rail.scrollLeft <= 1
const atEnd = rail.scrollLeft + rail.clientWidth >= rail.scrollWidth - 1
showMannschaftenSubmenuLeftControl.value = canScroll && !atStart
showMannschaftenSubmenuRightControl.value = canScroll && !atEnd
}
const scrollMannschaftenSubmenuLeft = () => {
const rail = mannschaftenSubmenuRail.value
if (!rail) return
rail.scrollBy({
left: -Math.max(rail.clientWidth * 0.8, 160),
behavior: 'smooth'
})
}
const scrollMannschaftenSubmenuRight = () => {
const rail = mannschaftenSubmenuRail.value
if (!rail) return
rail.scrollBy({
left: Math.max(rail.clientWidth * 0.8, 160),
behavior: 'smooth'
})
}
const loadMannschaften = async () => { const loadMannschaften = async () => {
try { try {
const attempt = async () => { const attempt = async () => {
@@ -994,16 +1048,23 @@ onMounted(() => {
// Listen for global updates to mannschaften (e.g., CMS saved) // Listen for global updates to mannschaften (e.g., CMS saved)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.addEventListener('mannschaften:changed', loadMannschaften) window.addEventListener('mannschaften:changed', loadMannschaften)
window.addEventListener('resize', updateMannschaftenSubmenuControls)
} }
nextTick(updateMannschaftenSubmenuControls)
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleDocumentClick) document.removeEventListener('click', handleDocumentClick)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.removeEventListener('mannschaften:changed', loadMannschaften) window.removeEventListener('mannschaften:changed', loadMannschaften)
window.removeEventListener('resize', updateMannschaftenSubmenuControls)
} }
}) })
watch([currentSubmenu, mannschaften], () => {
nextTick(updateMannschaftenSubmenuControls)
}, { flush: 'post' })
const toggleSubmenu = (menu) => { const toggleSubmenu = (menu) => {
// Wenn wir schon im richtigen Bereich sind, nichts tun (Submenu bleibt offen) // Wenn wir schon im richtigen Bereich sind, nichts tun (Submenu bleibt offen)
// Wenn nicht, zur Hauptseite navigieren // Wenn nicht, zur Hauptseite navigieren

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.6", "version": "1.8.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.6", "version": "1.8.7",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.11.2", "@pinia/nuxt": "^0.11.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.6", "version": "1.8.7",
"description": "Moderne Webseite für den Harheimer Tischtennis Club", "description": "Moderne Webseite für den Harheimer Tischtennis Club",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -362,7 +362,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { rowMatchesTeamFilter } from '../../utils/spielplan-filter.js' import { rowMatchesTeamFilter } from '~/utils/spielplan-filter.js'
const route = useRoute() const route = useRoute()

View File

@@ -360,7 +360,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { filterSpielplanRows, toApiTeamParam } from '../../utils/spielplan-filter.js' import { filterSpielplanRows, toApiTeamParam } from '~/utils/spielplan-filter.js'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -1,5 +1,5 @@
import { listSpielplanSeasons, readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js' import { listSpielplanSeasons, readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js'
import { formatTeamDisplayName } from '../../../utils/team-display.js' import { formatTeamDisplayName } from '~/utils/team-display.js'
function teamLabel(teamName, teamAgeGroup, competitionAgeGroup) { function teamLabel(teamName, teamAgeGroup, competitionAgeGroup) {
return formatTeamDisplayName(teamName, teamAgeGroup, competitionAgeGroup) return formatTeamDisplayName(teamName, teamAgeGroup, competitionAgeGroup)

View File

@@ -2,7 +2,7 @@ import fs from 'fs/promises'
import path from 'path' import path from 'path'
import { readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js' import { readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js'
import { info as loggerInfo, error as loggerError } from '../../utils/logger.js' import { info as loggerInfo, error as loggerError } from '../../utils/logger.js'
import { filterSpielplanRows } from '../../../utils/spielplan-filter.js' import { filterSpielplanRows } from '~/utils/spielplan-filter.js'
function seasonSlugToLabel(slug) { function seasonSlugToLabel(slug) {
const match = String(slug || '').match(/^(\d{2})--(\d{2})$/) const match = String(slug || '').match(/^(\d{2})--(\d{2})$/)