--- /dev/null
+import axios from 'axios';
+import moment from 'moment';
+import React from 'react';
+import { Container } from 'react-bootstrap';
+import { Helmet } from 'react-helmet';
+import { useParams } from 'react-router-dom';
+
+import {
+ hasActiveGuessing,
+ isAcceptingGuesses,
+ patchGuess,
+ patchWinner,
+} from '../helpers/Channel';
+import ErrorBoundary from '../components/common/ErrorBoundary';
+import Icon from '../components/common/Icon';
+import Slider from '../components/common/Slider';
+
+export const Component = () => {
+ const [channel, setChannel] = React.useState({});
+ const [guesses, setGuesses] = React.useState([]);
+ const [winnerExpiry, setWinnerExpiry] = React.useState(moment().subtract(15, 'second'));
+ const [winners, setWinners] = React.useState([]);
+
+ const params = useParams();
+ const { key } = params;
+
+ React.useEffect(() => {
+ if (!key) return;
+ axios.get(`/api/guessing-game-monitor/${key}`)
+ .then(res => {
+ setChannel(res.data.channel);
+ res.data.guesses.forEach(g => {
+ setGuesses(gs => patchGuess(gs, g));
+ });
+ res.data.winners.forEach(w => {
+ setWinners(ws => patchGuess(ws, w));
+ });
+ });
+ window.Echo.channel(`ChannelKey.${key}`)
+ .listen('.GuessingGuessCreated', (e) => {
+ setGuesses(gs => patchGuess(gs, e.model));
+ })
+ .listen('.GuessingWinnerCreated', (e) => {
+ setWinners(ws => patchWinner(ws, e.model));
+ })
+ .listen('.ChannelUpdated', (e) => {
+ setChannel(c => ({ ...c, ...e.model }));
+ });
+ return () => {
+ window.Echo.leave(`ChannelKey.${key}`);
+ };
+ }, [key]);
+
+ React.useEffect(() => {
+ if (isAcceptingGuesses(channel)) {
+ setGuesses(gs => gs.filter(g => g.created_at >= channel.guessing_start));
+ setWinners([]);
+ }
+ }, [channel]);
+
+ React.useEffect(() => {
+ const interval = setInterval(() => {
+ setWinnerExpiry(moment().subtract(15, 'second'));
+ }, 1000);
+ return () => {
+ clearInterval(interval);
+ };
+ }, []);
+
+ const guessingStats = React.useMemo(() => {
+ const stats = {
+ counts: [],
+ lastWin: null,
+ max: 0,
+ wins: [],
+ winners: [],
+ };
+ for (let i = 0; i < 22; ++i) {
+ stats.counts.push(0);
+ stats.wins.push(false);
+ }
+ const seen = [];
+ guesses.forEach(guess => {
+ if (seen[guess.uid]) {
+ --stats.counts[parseInt(seen[guess.uid].guess, 10) - 1];
+ }
+ ++stats.counts[parseInt(guess.guess, 10) - 1];
+ seen[guess.uid] = guess;
+ });
+ winners.forEach(winner => {
+ if (winner.score) {
+ stats.wins[parseInt(winner.guess, 10) - 1] = true;
+ stats.winners.push(winner.uname);
+ }
+ if (!stats.lastWin || stats.lastWin < winner.created_at) {
+ stats.lastWin = winner.created_at;
+ }
+ });
+ for (let i = 0; i < 22; ++i) {
+ if (stats.counts[i] > stats.max) {
+ stats.max = stats.counts[i];
+ }
+ }
+ return stats;
+ }, [guesses, winners]);
+
+ const getNumberHeight = React.useCallback((number) => {
+ if (!guessingStats || !guessingStats.max) return 3;
+ if (!number) return 3;
+ return Math.max(0.05, number / guessingStats.max) * 100;
+ }, [guessingStats]);
+
+ const getStatClass = React.useCallback((index) => {
+ const names = ['guessing-stat'];
+ if (guessingStats.wins[index]) {
+ names.push('has-won');
+ }
+ return names.join(' ');
+ }, [guessingStats]);
+
+ const showOpen = React.useMemo(() => {
+ return isAcceptingGuesses(channel);
+ }, [channel]);
+
+ const showClosed = React.useMemo(() => {
+ return hasActiveGuessing(channel) && !isAcceptingGuesses(channel);
+ }, [channel]);
+
+ const showWinners = React.useMemo(() => {
+ return !hasActiveGuessing(channel) && (
+ guessingStats?.lastWin &&
+ moment(guessingStats.lastWin).isAfter(winnerExpiry));
+ }, [channel, guessingStats, winnerExpiry]);
+
+ return <ErrorBoundary>
+ <Helmet>
+ <title>Guessing Game</title>
+ </Helmet>
+ <Container className="guessing-game-monitor" fluid>
+ {showOpen || showClosed || showWinners ?
+ <div className="message-box">
+ {showOpen ?
+ <div className="message-title accepting-guesses">
+ <Icon.WARNING className="message-icon" />
+ <div className="message-text">
+ <Slider duration={3500}>
+ <Slider.Slide>GT Big Key Guessing Game</Slider.Slide>
+ <Slider.Slide>Zahlen von 1 bis 22 in den Chat!</Slider.Slide>
+ </Slider>
+ </div>
+ <Icon.WARNING className="message-icon" />
+ </div>
+ : null}
+ {showClosed ?
+ <div className="message-title guessing-closed">
+ <div className="message-text">
+ Anmeldung geschlossen
+ </div>
+ </div>
+ : null}
+ {showWinners ?
+ <div className="message-title guessing-winners">
+ <div className="message-text">
+ {guessingStats.winners.length ?
+ <Slider duration={2500}>
+ <Slider.Slide>Herzlichen Glückwunsch!</Slider.Slide>
+ {guessingStats.winners.map(winner =>
+ <Slider.Slide key={winner}>{winner}</Slider.Slide>
+ )}
+ </Slider>
+ :
+ 'Leider keiner richtig'
+ }
+ </div>
+ </div>
+ : null}
+ <div className="guessing-stats">
+ {guessingStats.counts.map((number, index) =>
+ <div className={getStatClass(index)} key={index}>
+ <div className="guessing-box">
+ <div
+ className="guessing-box-bar"
+ style={{ height: `${getNumberHeight(number)}%` }}
+ />
+ </div>
+ <div className="guessing-number">{index + 1}</div>
+ </div>
+ )}
+ </div>
+ </div>
+ : null}
+ </Container>
+ </ErrorBoundary>;
+};