feat: implement LiveTicker functionality with sound notifications and UI updates
This commit is contained in:
@@ -4,6 +4,14 @@
|
||||
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">
|
||||
<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="soundEnabled"
|
||||
@click="toggleSound"
|
||||
>
|
||||
{{ soundEnabled ? '🔔 Ton an' : '🔕 Ton aus' }}
|
||||
</button>
|
||||
<div
|
||||
v-for="match in matches"
|
||||
:key="match.meetingId"
|
||||
@@ -32,13 +40,72 @@ import { useAuthStore } from '~/stores/auth'
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const matches = ref([])
|
||||
const soundEnabled = ref(false)
|
||||
const isInternal = computed(() => authStore.isLoggedIn && (route.path.startsWith('/mitgliederbereich') || route.path.startsWith('/cms')))
|
||||
let timer
|
||||
async function load() {
|
||||
if (!isInternal.value) { matches.value = []; return }
|
||||
try { matches.value = (await $fetch('/api/mitgliederbereich/live-ticker')).matches || [] } catch { matches.value = [] }
|
||||
let audioContext
|
||||
let tickerLoaded = false
|
||||
let previousStates = new Map()
|
||||
|
||||
function stateByMeeting(items) {
|
||||
return new Map(items.map(match => [
|
||||
match.meetingId || `${match.homeTeam}-${match.awayTeam}-${match.startsAt}`,
|
||||
`${match.hasLiveData}|${match.score}|${match.completed}`
|
||||
]))
|
||||
}
|
||||
onMounted(() => { load(); timer = setInterval(load, 30_000) })
|
||||
|
||||
function hasRelevantChange(nextStates) {
|
||||
return [...nextStates].some(([meetingId, state]) => previousStates.get(meetingId) !== state)
|
||||
}
|
||||
|
||||
async function playPing() {
|
||||
try {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext
|
||||
if (!AudioContext) return
|
||||
audioContext ||= new AudioContext()
|
||||
await audioContext.resume()
|
||||
const oscillator = audioContext.createOscillator()
|
||||
const gain = audioContext.createGain()
|
||||
oscillator.type = 'sine'
|
||||
oscillator.frequency.setValueAtTime(880, audioContext.currentTime)
|
||||
gain.gain.setValueAtTime(0.0001, audioContext.currentTime)
|
||||
gain.gain.exponentialRampToValueAtTime(0.16, audioContext.currentTime + 0.01)
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + 0.17)
|
||||
oscillator.connect(gain).connect(audioContext.destination)
|
||||
oscillator.start()
|
||||
oscillator.stop(audioContext.currentTime + 0.18)
|
||||
} catch {
|
||||
soundEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSound() {
|
||||
soundEnabled.value = !soundEnabled.value
|
||||
localStorage.setItem('live-ticker-sound-enabled', String(soundEnabled.value))
|
||||
if (soundEnabled.value) playPing()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!isInternal.value) {
|
||||
matches.value = []
|
||||
tickerLoaded = false
|
||||
previousStates = new Map()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const nextMatches = (await $fetch('/api/mitgliederbereich/live-ticker')).matches || []
|
||||
const nextStates = stateByMeeting(nextMatches)
|
||||
if (tickerLoaded && soundEnabled.value && hasRelevantChange(nextStates)) playPing()
|
||||
previousStates = nextStates
|
||||
tickerLoaded = true
|
||||
matches.value = nextMatches
|
||||
} catch { matches.value = [] }
|
||||
}
|
||||
onMounted(() => {
|
||||
soundEnabled.value = localStorage.getItem('live-ticker-sound-enabled') === 'true'
|
||||
load()
|
||||
timer = setInterval(load, 30_000)
|
||||
})
|
||||
onUnmounted(() => clearInterval(timer))
|
||||
watch(isInternal, load)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user