217 lines
5.6 KiB
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>
|