Files
XIP/frontend/src/components/StatsTicker.vue

217 lines
5.6 KiB
Vue

<!-- Bandeau de stats permanent façon téléscripteur néon (casino / bourse). -->
<template>
<div class="ticker" :class="{ 'is-off': !connected }">
<!-- Badge LIVE fixe à gauche -->
<div class="ticker-badge">
<span class="ticker-dot" />
<span class="ticker-badge-txt">{{ connected ? 'LIVE' : '···' }}</span>
</div>
<!-- Piste défilante (2 groupes identiques pour une boucle sans couture) -->
<div class="ticker-viewport">
<div class="ticker-track">
<div
v-for="copy in 2"
:key="copy"
class="ticker-group"
:aria-hidden="copy === 2 ? 'true' : undefined"
>
<span
v-for="item in items"
:key="item.key + '-' + copy"
class="chip"
:class="`chip--${item.tone}`"
>
<span class="chip-val">
<AnimatedNumber :value="item.value" :decimals="item.decimals ?? 0" />
<span v-if="item.unit" class="chip-unit">{{ item.unit }}</span>
</span>
<span class="chip-label">{{ item.label }}</span>
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import AnimatedNumber from './AnimatedNumber.vue';
import type { Stats } from '@/composables/useRealtime';
const props = defineProps<{ stats: Stats | null; connected: boolean }>();
type Tone = 'cyan' | 'green' | 'magenta' | 'orange' | 'plain';
interface Chip {
key: string;
label: string;
value: number;
tone: Tone;
unit?: string;
decimals?: number;
}
const ZERO: Stats = {
connectedTabs: 0,
typingNow: 0,
lettersPerSec: 0,
msgsPerMin: 0,
messages: 0,
replies: 0,
charsSent: 0,
lettersTyped: 0,
uniqueIps: 0,
longestMsg: 0,
abandonRate: 0,
avgLength: 0,
moneyExtorted: 0,
};
const items = computed<Chip[]>(() => {
const s = props.stats ?? ZERO;
return [
{ key: 'tabs', label: 'onglets connectés', value: s.connectedTabs, tone: 'cyan' },
{ key: 'typing', label: 'écrivent là', value: s.typingNow, tone: 'green' },
{ key: 'lps', label: 'lettres / s', value: s.lettersPerSec, decimals: 1, tone: 'green' },
{ key: 'mpm', label: 'messages / min', value: s.msgsPerMin, tone: 'green' },
{ key: 'msgs', label: 'messages', value: s.messages, tone: 'cyan' },
{ key: 'replies', label: 'réponses', value: s.replies, tone: 'plain' },
{ key: 'chars', label: 'caractères envoyés', value: s.charsSent, tone: 'plain' },
{ key: 'letters', label: 'lettres tapées', value: s.lettersTyped, tone: 'magenta' },
{ key: 'ips', label: 'IP uniques', value: s.uniqueIps, tone: 'cyan' },
{ key: 'longest', label: 'le + long', value: s.longestMsg, unit: ' car', tone: 'plain' },
{ key: 'abandon', label: "taux d'abandon", value: s.abandonRate, decimals: 1, unit: ' %', tone: 'orange' },
{ key: 'avg', label: 'longueur moy.', value: s.avgLength, decimals: 1, unit: ' car', tone: 'plain' },
{ key: 'money', label: 'argent extorqué', value: s.moneyExtorted, decimals: 2, unit: ' €', tone: 'orange' },
];
});
</script>
<style scoped>
.ticker {
position: relative;
flex-shrink: 0;
height: 40px;
display: flex;
align-items: stretch;
background: #0a0a12;
border-bottom: 1px solid #1a1a2a;
box-shadow: 0 2px 8px #00000066;
overflow: hidden;
}
/* ── Badge LIVE fixe ── */
.ticker-badge {
position: relative;
z-index: 2;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 7px;
padding: 0 14px;
background: #0e0e18;
border-right: 1px solid #1a1a2a;
box-shadow: 4px 0 8px #0a0a12;
}
.ticker-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #44996655;
animation: blink 1.5s ease-in-out infinite;
}
.ticker-badge-txt {
font-family: 'Courier New', monospace;
font-size: 11px;
font-weight: bold;
letter-spacing: 2px;
color: #448866;
}
.ticker.is-off .ticker-dot {
background: #884444;
animation: none;
}
.ticker.is-off .ticker-badge-txt {
color: #885555;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* ── Piste défilante ── */
.ticker-viewport {
flex: 1;
min-width: 0;
overflow: hidden;
display: flex;
align-items: center;
}
.ticker-track {
display: inline-flex;
white-space: nowrap;
will-change: transform;
animation: ticker-scroll 48s linear infinite;
}
.ticker:hover .ticker-track {
animation-play-state: paused;
}
.ticker-group {
display: inline-flex;
align-items: center;
}
@keyframes ticker-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
/* ── Chips ── */
.chip {
position: relative;
display: inline-flex;
align-items: baseline;
gap: 7px;
padding: 0 22px;
}
.chip::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 16px;
width: 1px;
background: #ffffff14;
}
.chip-val {
font-family: 'Courier New', monospace;
font-size: 15px;
font-weight: bold;
color: #d8d8e8;
}
.chip-unit {
font-size: 10px;
font-weight: normal;
opacity: 0.6;
}
.chip-label {
font-family: Arial, sans-serif;
font-size: 10px;
letter-spacing: 0.5px;
text-transform: uppercase;
color: #50506e;
}
.chip--cyan .chip-val { color: #5599aa; }
.chip--green .chip-val { color: #447755; }
.chip--magenta .chip-val { color: #885588; }
.chip--orange .chip-val { color: #997744; }
/* Accessibilité : pas de défilement si l'utilisateur le refuse */
@media (prefers-reduced-motion: reduce) {
.ticker-track { animation: none; }
.ticker-viewport { overflow-x: auto; scrollbar-width: none; }
.ticker-viewport::-webkit-scrollbar { display: none; }
}
</style>