Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions resources/js/components/AnimatedScore.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { ref, watch } from 'vue';

/**
* A score number that pops and flashes whenever its value changes — used by
* the gamecast header and the live scoreboard so a goal landing over the
* realtime channel is obvious rather than a silent digit swap.
*
* The animation only fires on a change (never on the initial render), and the
* flash colour is themeable so it stays legible on both the dark pitch header
* and the light scoreboard cards.
*/
const props = withDefaults(
defineProps<{ value: number | null; flash?: string }>(),
{ flash: 'var(--volt)' },
);

const popping = ref(false);

watch(
() => props.value,
() => {
// Drop the class, then re-add it next frame so a second goal in quick
// succession restarts the animation instead of being ignored.
popping.value = false;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
popping.value = true;
});
});
},
);

function onAnimationEnd(): void {
popping.value = false;
}
</script>

<template>
<span
class="animated-score"
:class="{ 'animated-score--pop': popping }"
:style="{ '--score-flash': flash }"
@animationend="onAnimationEnd"
>{{ value ?? '–' }}</span
>
</template>

<style scoped>
.animated-score {
display: inline-block;
}

.animated-score--pop {
animation: score-pop 0.6s ease-out;
}

@keyframes score-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(1.4);
color: var(--score-flash);
}
100% {
transform: scale(1);
}
}

@media (prefers-reduced-motion: reduce) {
.animated-score--pop {
animation: none;
}
}
</style>
3 changes: 2 additions & 1 deletion resources/js/pages/Gamecast/Show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Head, Link, router } from '@inertiajs/vue3';
import { useEchoPublicClient } from '@/composables/useEchoPublicClient';
import { computed, nextTick, ref, watch } from 'vue';
import AnimatedScore from '@/components/AnimatedScore.vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import GamecastEditor from '@/components/GamecastEditor.vue';
import { matchClockMinute, useNow } from '@/composables/useMatchClock';
Expand Down Expand Up @@ -205,7 +206,7 @@ const matchTitle = computed(() => `${props.game.home_team?.name ?? 'TBD'} vs ${p
<div class="truncate text-sm font-medium opacity-90">{{ game.home_team?.name ?? 'TBD' }}</div>
</div>
<div class="text-center font-display text-6xl font-bold tabular-nums leading-none">
{{ game.home_team_score ?? '–' }}<span class="px-2 opacity-50">:</span>{{ game.away_team_score ?? '–' }}
<AnimatedScore :value="game.home_team_score" /><span class="px-2 opacity-50">:</span><AnimatedScore :value="game.away_team_score" />
</div>
<div class="text-center">
<div class="font-display text-lg font-bold uppercase tracking-wider text-volt">{{ game.away_team?.acronym ?? '—' }}</div>
Expand Down
5 changes: 3 additions & 2 deletions resources/js/pages/Scoreboard/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Head, Link, router } from '@inertiajs/vue3';
import { useEchoPublicClient } from '@/composables/useEchoPublicClient';
import { ref } from 'vue';
import AnimatedScore from '@/components/AnimatedScore.vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import { matchClockMinute, useNow } from '@/composables/useMatchClock';
import { show as gamecastShow } from '@/routes/games';
Expand Down Expand Up @@ -168,7 +169,7 @@ const pageBreadcrumbs: BreadcrumbItem[] = [{ title: 'Scoreboard', href: index().
</span>
<span class="truncate font-medium">{{ game.home_team.name ?? 'TBD' }}</span>
</span>
<span class="font-display text-3xl font-bold tabular-nums leading-none">{{ game.home_team_score ?? '–' }}</span>
<AnimatedScore :value="game.home_team_score" flash="var(--primary)" class="font-display text-3xl font-bold tabular-nums leading-none" />
</div>
<div class="flex items-center justify-between gap-2">
<span class="flex min-w-0 items-center gap-2">
Expand All @@ -177,7 +178,7 @@ const pageBreadcrumbs: BreadcrumbItem[] = [{ title: 'Scoreboard', href: index().
</span>
<span class="truncate font-medium">{{ game.away_team.name ?? 'TBD' }}</span>
</span>
<span class="font-display text-3xl font-bold tabular-nums leading-none">{{ game.away_team_score ?? '–' }}</span>
<AnimatedScore :value="game.away_team_score" flash="var(--primary)" class="font-display text-3xl font-bold tabular-nums leading-none" />
</div>
</div>
</component>
Expand Down
Loading