feat: replace success modal with non-blocking toast notification
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:
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user