feat: replace success modal with non-blocking toast notification
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 5m10s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m14s

feat: add global event listener for mannschaften updates in Navigation component

feat: notify app of mannschaften changes after CSV save and handle visibility changes

refactor: remove unused anlagen page

fix: update CmsMannschaften reference in sportbetrieb page for reactivity

fix: enhance authentication token retrieval in passkey API endpoints

feat: implement refresh session and access token generation for Android clients in passkey login

fix: unify token retrieval method across passkey API endpoints

feat: add MediaTypes utility for JSON content type in Android app

feat: create PasskeyRepository for handling passkey authentication and registration in Android app

feat: add validated text field and rich text components for Android UI

feat: implement newsletter subscription and unsubscription screens in Android app

feat: create public pages including Impressum with dynamic content loading
This commit is contained in:
Torsten Schulz (local)
2026-05-28 08:33:28 +02:00
parent e033d716dd
commit 0528334eb4
37 changed files with 1297 additions and 364 deletions

View File

@@ -1,113 +0,0 @@
<template>
<section
id="facilities"
class="py-16 sm:py-20 bg-white"
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
Unsere Anlagen
</h2>
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Moderne Ausstattung und erstklassige Einrichtungen für ein perfektes Tischtenniserlebnis
</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
<div
v-for="facility in facilities"
:key="facility.title"
class="group relative bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden border border-gray-100"
>
<div :class="['absolute top-0 left-0 right-0 h-1 bg-gradient-to-r opacity-0 group-hover:opacity-100 transition-opacity', facility.color]" />
<div class="p-8">
<div :class="['w-16 h-16 bg-gradient-to-br rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform', facility.color]">
<component
:is="facility.icon"
:size="32"
class="text-white"
/>
</div>
<h3 class="text-2xl font-display font-bold text-gray-900 mb-3">
{{ facility.title }}
</h3>
<p class="text-gray-600 leading-relaxed">
{{ facility.description }}
</p>
</div>
</div>
</div>
<!-- Image Gallery -->
<div class="grid md:grid-cols-2 gap-6">
<div class="relative h-[300px] rounded-2xl overflow-hidden shadow-xl group">
<div
class="w-full h-full bg-cover bg-center group-hover:scale-110 transition-transform duration-700"
style="background-image: url('https://images.unsplash.com/photo-1534438097545-77fef53fe2e8?q=80&w=2070')"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end">
<p class="text-white font-semibold text-xl p-6">
Hochwertige Wettkampftische
</p>
</div>
</div>
<div class="relative h-[300px] rounded-2xl overflow-hidden shadow-xl group">
<div
class="w-full h-full bg-cover bg-center group-hover:scale-110 transition-transform duration-700"
style="background-image: url('https://images.unsplash.com/photo-1611004275469-8583ed5d7b8d?q=80&w=2070')"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end">
<p class="text-white font-semibold text-xl p-6">
Moderne Tischtennishalle
</p>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { Sun, CloudRain, Dumbbell, Utensils, Wifi, Droplets } from 'lucide-vue-next'
const facilities = [
{
icon: Sun,
title: '8 Tischtennisplatten',
description: 'Hochwertige Wettkampftische für optimales Spielvergnügen',
color: 'from-yellow-400 to-orange-500',
},
{
icon: CloudRain,
title: 'Klimatisierte Halle',
description: 'Optimale Bedingungen bei jedem Wetter in unserer modernen Halle',
color: 'from-blue-400 to-blue-600',
},
{
icon: Dumbbell,
title: 'Trainingsbereich',
description: 'Ballmaschinen und Trainingsgeräte für gezieltes Training',
color: 'from-red-400 to-red-600',
},
{
icon: Utensils,
title: 'Clubhaus',
description: 'Gemütliches Clubhaus mit Aufenthaltsraum und Küche',
color: 'from-green-400 to-green-600',
},
{
icon: Wifi,
title: 'Kostenloses WLAN',
description: 'Schnelles Internet auf der gesamten Anlage',
color: 'from-purple-400 to-purple-600',
},
{
icon: Droplets,
title: 'Umkleiden & Duschen',
description: 'Moderne, saubere Umkleideräume mit Duschen',
color: 'from-cyan-400 to-cyan-600',
},
]
</script>

View File

@@ -1,43 +1,14 @@
<template>
<!-- Success Modal -->
<!-- Success Toast (bottom) -->
<div
v-if="showSuccess"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closeSuccess"
v-if="showSuccessToast"
class="fixed left-0 right-0 mx-auto max-w-3xl z-50 pointer-events-none"
style="bottom:72px;"
>
<div class="bg-white rounded-lg max-w-md w-full p-6">
<div class="flex items-center mb-4">
<div class="flex-shrink-0 w-10 h-10 mx-auto bg-green-100 rounded-full flex items-center justify-center">
<svg
class="w-6 h-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div>
<div class="text-center">
<h3 class="text-lg font-medium text-gray-900 mb-2">
{{ successTitle }}
</h3>
<p class="text-sm text-gray-600 mb-6">
{{ successMessage }}
</p>
<div class="flex justify-center">
<button
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg transition-colors"
@click="closeSuccess"
>
OK
</button>
</div>
<div class="pointer-events-auto mx-4 shadow-lg rounded-md overflow-hidden">
<div class="px-4 py-3 bg-green-50 text-green-800 text-sm">
<div class="font-medium">{{ toastTitle }}</div>
<div class="mt-1">{{ toastMessage }}</div>
</div>
</div>
</div>
@@ -138,14 +109,14 @@
<script setup>
import { ref } from 'vue'
// Modal States
const showSuccess = ref(false)
// Modal / Toast States
const showSuccessToast = ref(false)
const showError = ref(false)
const showConfirm = ref(false)
// Modal Content
const successTitle = ref('')
const successMessage = ref('')
// Modal / Toast Content
const toastTitle = ref('')
const toastMessage = ref('')
const errorTitle = ref('')
const errorMessage = ref('')
const confirmTitle = ref('')
@@ -153,10 +124,14 @@ const confirmMessage = ref('')
const confirmAction = ref(null)
// Modal Functions
let toastTimeout = null
const showSuccessModal = (title, message) => {
successTitle.value = title
successMessage.value = message
showSuccess.value = true
// Show non-blocking toast at bottom instead of modal dialog
toastTitle.value = title || 'Erfolg'
toastMessage.value = message || ''
showSuccessToast.value = true
if (toastTimeout) clearTimeout(toastTimeout)
toastTimeout = setTimeout(() => { showSuccessToast.value = false; toastTimeout = null }, 3500)
}
const showErrorModal = (title, message) => {
@@ -173,7 +148,8 @@ const showConfirmModal = (title, message, action) => {
}
const closeSuccess = () => {
showSuccess.value = false
showSuccessToast.value = false
if (toastTimeout) { clearTimeout(toastTimeout); toastTimeout = null }
}
const closeError = () => {

View File

@@ -989,10 +989,17 @@ onMounted(() => {
// Close CMS dropdown when clicking outside
document.addEventListener('click', handleDocumentClick)
// Listen for global updates to mannschaften (e.g., CMS saved)
if (typeof window !== 'undefined') {
window.addEventListener('mannschaften:changed', loadMannschaften)
}
})
onUnmounted(() => {
document.removeEventListener('click', handleDocumentClick)
if (typeof window !== 'undefined') {
window.removeEventListener('mannschaften:changed', loadMannschaften)
}
})
const toggleSubmenu = (menu) => {

View File

@@ -404,7 +404,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, onUnmounted } from 'vue'
import { Plus, Trash2, Loader2, AlertCircle, Pencil, Users, ChevronUp, ChevronDown, ArrowRight } from 'lucide-vue-next'
const isLoading = ref(true)
@@ -591,6 +591,12 @@ const saveCSV = async () => {
content: [header, ...rows].join('\n')
}
})
// Notify other parts of the app that mannschaften changed
try {
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('mannschaften:changed'))
}
} catch (e) { /* no-op */ }
}
const moveMannschaft = async (index, delta) => {
@@ -674,4 +680,30 @@ onMounted(async () => {
await loadSeasons()
await loadMannschaften().catch(() => {})
})
// Expose load function to parent components
try { defineExpose({ loadMannschaften }) } catch (e) { /* noop if not supported in SSR context */ }
// Reload when tab/window becomes visible or window gains focus
const handleVisibilityOrFocus = () => {
try {
if (document.visibilityState === 'visible') {
loadMannschaften().catch(() => {})
}
} catch (e) {
// ignore
}
}
if (typeof window !== 'undefined') {
window.addEventListener('visibilitychange', handleVisibilityOrFocus)
window.addEventListener('focus', handleVisibilityOrFocus)
}
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('visibilitychange', handleVisibilityOrFocus)
window.removeEventListener('focus', handleVisibilityOrFocus)
}
})
</script>