fix: animiere Live-Ticker und stabilisiere Benachrichtigungsschalter
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
v-if="isInternal && matches.length"
|
||||
class="fixed top-20 z-40 w-full border-y border-primary-200 bg-primary-50 text-primary-950 shadow-sm"
|
||||
>
|
||||
<div class="mx-auto flex max-w-7xl items-center gap-3 overflow-x-auto px-4 py-2 text-sm sm:px-6">
|
||||
<div class="mx-auto flex max-w-7xl items-center gap-3 px-4 py-2 text-sm sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded border border-primary-300 bg-white px-2 py-1 text-xs font-semibold text-primary-900 hover:bg-primary-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||
@@ -12,21 +12,48 @@
|
||||
>
|
||||
{{ soundEnabled ? '🔔 Ton an' : '🔕 Ton aus' }}
|
||||
</button>
|
||||
<div
|
||||
v-for="match in matches"
|
||||
:key="match.meetingId"
|
||||
:class="['shrink-0 whitespace-nowrap', match.completed ? 'italic text-gray-600' : 'font-semibold']"
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded border border-primary-300 bg-white px-2 py-1 text-xs font-semibold text-primary-900 hover:bg-primary-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||
:aria-pressed="paused"
|
||||
:aria-label="paused ? 'Laufschrift fortsetzen' : 'Laufschrift anhalten'"
|
||||
@click="paused = !paused"
|
||||
>
|
||||
<span v-if="match.completed">✓ beendet</span>
|
||||
<span
|
||||
v-else-if="match.hasLiveData"
|
||||
class="text-red-700"
|
||||
>● LIVE</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-gray-600"
|
||||
>◌ Keine Live-Daten</span>
|
||||
<span class="mx-2">{{ match.homeTeam }} <strong>{{ match.score }}</strong> {{ match.awayTeam }}</span>
|
||||
{{ paused ? '▶' : 'Ⅱ' }}
|
||||
</button>
|
||||
<div
|
||||
class="ticker-viewport min-w-0 flex-1"
|
||||
:class="{ 'ticker-paused': paused }"
|
||||
>
|
||||
<div
|
||||
ref="tickerTrack"
|
||||
class="ticker-track"
|
||||
:style="{ '--ticker-duration': `${tickerDuration}s` }"
|
||||
>
|
||||
<div
|
||||
v-for="copy in 2"
|
||||
:key="copy"
|
||||
class="ticker-group"
|
||||
:aria-hidden="copy === 2 ? 'true' : undefined"
|
||||
>
|
||||
<div
|
||||
v-for="match in matches"
|
||||
:key="match.meetingId"
|
||||
:class="['shrink-0 whitespace-nowrap', match.completed ? 'italic text-gray-600' : 'font-semibold']"
|
||||
>
|
||||
<span v-if="match.completed">✓ beendet</span>
|
||||
<span
|
||||
v-else-if="match.hasLiveData"
|
||||
class="text-red-700"
|
||||
>● LIVE</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-gray-600"
|
||||
>◌ Keine Live-Daten</span>
|
||||
<span class="mx-2">{{ match.homeTeam }} <strong>{{ match.score }}</strong> {{ match.awayTeam }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -41,12 +68,26 @@ const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const matches = ref([])
|
||||
const soundEnabled = ref(false)
|
||||
const paused = ref(false)
|
||||
const tickerTrack = ref(null)
|
||||
const tickerDuration = ref(30)
|
||||
const isInternal = computed(() => authStore.isLoggedIn && (route.path.startsWith('/mitgliederbereich') || route.path.startsWith('/cms')))
|
||||
let timer
|
||||
let audioContext
|
||||
let tickerLoaded = false
|
||||
let previousStates = new Map()
|
||||
|
||||
// Two equal copies let the track wrap without a visible jump. ResizeObserver
|
||||
// keeps the speed at 36 px/s when results, fonts or the viewport change.
|
||||
watch(tickerTrack, (track, _previous, onCleanup) => {
|
||||
if (!track) return
|
||||
const observer = new ResizeObserver(() => {
|
||||
tickerDuration.value = Math.max(1, track.getBoundingClientRect().width / 2 / 36)
|
||||
})
|
||||
observer.observe(track)
|
||||
onCleanup(() => observer.disconnect())
|
||||
}, { flush: 'post' })
|
||||
|
||||
function stateByMeeting(items) {
|
||||
return new Map(items.map(match => [
|
||||
match.meetingId || `${match.homeTeam}-${match.awayTeam}-${match.startsAt}`,
|
||||
@@ -109,3 +150,44 @@ onMounted(() => {
|
||||
onUnmounted(() => clearInterval(timer))
|
||||
watch(isInternal, load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ticker-viewport {
|
||||
container-type: inline-size;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticker-track {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
animation: ticker-scroll var(--ticker-duration) linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.ticker-group {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
min-width: 100cqw;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.ticker-paused .ticker-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.ticker-viewport:hover .ticker-track { animation-play-state: paused; }
|
||||
}
|
||||
|
||||
@keyframes ticker-scroll {
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ticker-viewport { overflow-x: auto; }
|
||||
.ticker-track { animation: none; will-change: auto; }
|
||||
.ticker-group[aria-hidden="true"] { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user