]> git.localhorst.tv Git - alttp.git/commitdiff
add simple guessing game browser source
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 1 Mar 2024 17:35:49 +0000 (18:35 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 1 Mar 2024 17:35:49 +0000 (18:35 +0100)
15 files changed:
app/Http/Controllers/ChannelController.php
app/Models/Channel.php
app/Models/GuessingGuess.php
app/Models/GuessingWinner.php
database/migrations/2024_03_01_090259_channel_access_key.php [new file with mode: 0644]
resources/js/app/Routes.js
resources/js/components/common/Icon.js
resources/js/components/common/Slider.js [new file with mode: 0644]
resources/js/components/twitch-bot/Controls.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/js/pages/GuessingGameMonitor.js [new file with mode: 0644]
resources/sass/channels.scss
resources/sass/common.scss
routes/api.php

index 4cc49c1981fa1c5c6e06be05f43ea5eb3570aa98..78648af8dea8ece74e65e2987ea06a37e7479a58 100644 (file)
@@ -6,6 +6,8 @@ use App\Models\Channel;
 use App\Models\TwitchBotCommand;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Http\Request;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Gate;
 
 class ChannelController extends Controller {
 
@@ -34,12 +36,12 @@ class ChannelController extends Controller {
                                ->orWhere('short_name', 'LIKE', '%'.$validatedData['phrase'].'%');
                }
                $channels = $channels->limit(5);
-               return $channels->get()->toJson();
+               return $this->sendChannels($channels->get());
        }
 
        public function single(Request $request, Channel $channel) {
                $this->authorize('view', $channel);
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function chat(Request $request, Channel $channel) {
@@ -53,7 +55,7 @@ class ChannelController extends Controller {
                $this->authorize('editRestream', $channel);
                $nick = empty($validatedData['bot_nick']) ? 'localhorsttv' : $validatedData['bot_nick'];
                TwitchBotCommand::chat($channel->twitch_chat, $validatedData['text'], $request->user(), $nick);
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function chatSettings(Request $request, Channel $channel) {
@@ -69,7 +71,7 @@ class ChannelController extends Controller {
                $this->authorize('editRestream', $channel);
                $channel->chat_settings = $validatedData;
                $channel->save();
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function join(Request $request, Channel $channel) {
@@ -88,7 +90,7 @@ class ChannelController extends Controller {
                }
                $channel->save();
                TwitchBotCommand::join($channel->twitch_chat, $request->user(), $nick);
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function part(Request $request, Channel $channel) {
@@ -107,7 +109,7 @@ class ChannelController extends Controller {
                }
                $channel->save();
                TwitchBotCommand::part($channel->twitch_chat, $request->user(), $nick);
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function deleteCommand(Channel $channel, $command) {
@@ -118,7 +120,7 @@ class ChannelController extends Controller {
                        $channel->chat_commands = $cmds;
                        $channel->save();
                }
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function saveCommand(Request $request, Channel $channel, $command) {
@@ -135,7 +137,7 @@ class ChannelController extends Controller {
                $channel->chat_commands = $cmds;
                $channel->save();
 
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function controlGuessingGame(Request $request, Channel $channel, $name) {
@@ -195,7 +197,7 @@ class ChannelController extends Controller {
                                break;
                }
 
-               return $channel->toJson();
+               return $this->sendChannel($channel);
        }
 
        public function getGuessingGame(Channel $channel, $name) {
@@ -214,6 +216,23 @@ class ChannelController extends Controller {
                ];
        }
 
+       public function getGuessingGameMonitor($key) {
+               $channel = Channel::where('access_key', '=', $key)->firstOrFail();
+
+               $cutoff = $channel->guessing_start;
+               if (is_null($cutoff)) {
+                       $last = $channel->winners()->latest()->first();
+                       $cutoff = $last->pod;
+               }
+               $guesses = $channel->guesses()->where('created_at', '>=', $cutoff)->orderBy('created_at')->get();
+               $winners = $channel->winners()->where('created_at', '>=', $cutoff)->orderBy('created_at')->get();
+               return [
+                       'channel' => $channel->toArray(),
+                       'guesses' => $guesses->toArray(),
+                       'winners' => $winners->toArray(),
+               ];
+       }
+
        public function saveGuessingGame(Request $request, Channel $channel, $name) {
                $this->authorize('editRestream', $channel);
 
@@ -238,7 +257,35 @@ class ChannelController extends Controller {
                $channel->guessing_settings = $settings;
                $channel->save();
 
+               return $this->sendChannel($channel);
+       }
+
+       protected function sendChannel(Channel $channel) {
+               if (Gate::allows('editRestream', $channel)) {
+                       $this->unhideChannel($channel);
+               }
                return $channel->toJson();
        }
 
+       protected function sendChannels(Collection $channels) {
+               foreach ($channels as $channel) {
+                       if (Gate::allows('editRestream', $channel)) {
+                               $this->unhideChannel($channel);
+                       }
+               }
+               return $channels->toJson();
+       }
+
+       private function unhideChannel(Channel $channel) {
+               $channel->makeVisible([
+                       'access_key',
+                       'chat',
+                       'chat_commands',
+                       'chat_settings',
+                       'guessing_settings',
+                       'join',
+                       'twitch_chat',
+               ]);
+       }
+
 }
index da9f6a7360ad772002d0b1307cd751853c77314b..8debe4f9434ab554d70eb5e16086bf875ed1875e 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use Illuminate\Broadcasting\Channel as PublicChannel;
 use Illuminate\Broadcasting\PrivateChannel;
 use Illuminate\Database\Eloquent\BroadcastsEvents;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -17,6 +18,9 @@ class Channel extends Model {
                $channels = [
                        new PrivateChannel('Channel.'.$this->id),
                ];
+               if (!empty($this->access_key)) {
+                       $channels[] = new PublicChannel('ChannelKey.'.$this->access_key);
+               }
                return $channels;
        }
 
@@ -183,16 +187,23 @@ class Channel extends Model {
                'chat' => 'boolean',
                'chat_commands' => 'array',
                'chat_settings' => 'array',
+               'guessing_end' => 'datetime',
                'guessing_settings' => 'array',
                'guessing_start' => 'datetime',
-               'guessing_end' => 'datetime',
                'languages' => 'array',
                'join' => 'boolean',
        ];
 
        protected $hidden = [
+               'access_key',
+               'chat',
+               'chat_commands',
+               'chat_settings',
                'created_at',
                'ext_id',
+               'guessing_settings',
+               'join',
+               'twitch_chat',
                'updated_at',
        ];
 
index b8e413b299e68b824e27837370b139f702979ba3..4062452a7531e63e7188f7469882501611b27a90 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use Illuminate\Broadcasting\Channel as PublicChannel;
 use Illuminate\Broadcasting\PrivateChannel;
 use Illuminate\Database\Eloquent\BroadcastsEvents;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -20,6 +21,9 @@ class GuessingGuess extends Model {
                $channels = [
                        new PrivateChannel('Channel.'.$this->channel_id),
                ];
+               if (!empty($this->channel->access_key)) {
+                       $channels[] = new PublicChannel('ChannelKey.'.$this->channel->access_key);
+               }
                return $channels;
        }
 
index e6945bff37a6caa0980800b6f3330478bd13ecbc..0caada336256f012a29e2e01b4952e8a1daafce0 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use Illuminate\Broadcasting\Channel as PublicChannel;
 use Illuminate\Broadcasting\PrivateChannel;
 use Illuminate\Database\Eloquent\BroadcastsEvents;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -20,6 +21,9 @@ class GuessingWinner extends Model {
                $channels = [
                        new PrivateChannel('Channel.'.$this->channel_id),
                ];
+               if (!empty($this->channel->access_key)) {
+                       $channels[] = new PublicChannel('ChannelKey.'.$this->channel->access_key);
+               }
                return $channels;
        }
 
diff --git a/database/migrations/2024_03_01_090259_channel_access_key.php b/database/migrations/2024_03_01_090259_channel_access_key.php
new file mode 100644 (file)
index 0000000..1b37d09
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->string('access_key')->default('');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->dropColumn('access_key');
+               });
+       }
+};
index 3f73834311d5014845eb3de1e642c1d7047dbd21..dbdbcaed8b2f5ac40ab528e1fc1ca3192028aa3e 100644 (file)
@@ -135,6 +135,13 @@ const router = createBrowserRouter(
                                                '../pages/GuessingGameControls'
                                        )}
                                />
+                               <Route
+                                       path="monitor/:key"
+                                       lazy={() => import(
+                                               /* webpackChunkName: "guessing" */
+                                               '../pages/GuessingGameMonitor'
+                                       )}
+                               />
                        </Route>
                </Route>
        )
index 8f03baed0ef06cb811fab5f8056ac5fef2286254..4d9559c867cb119af5e3ae5af6996858753ceabd 100644 (file)
@@ -62,6 +62,7 @@ Icon.ADD = makePreset('AddIcon', 'circle-plus');
 Icon.ALLOWED = makePreset('AllowedIcon', 'square-check');
 Icon.APPLY = makePreset('ApplyIcon', 'right-to-bracket');
 Icon.APPLICATIONS = makePreset('ApplicationsIcon', 'person-running');
+Icon.BROWSER_SOURCE = makePreset('BrowserSourceIcon', 'tv');
 Icon.CHART = makePreset('ChartIcon', 'chart-line');
 Icon.CROSSHAIRS = makePreset('CrosshairsIcon', 'crosshairs');
 Icon.DELETE = makePreset('DeleteIcon', 'user-xmark');
@@ -103,6 +104,7 @@ Icon.TWITCH = makePreset('TwitchIcon', ['fab', 'twitch']);
 Icon.UNKNOWN = makePreset('UnknownIcon', 'square-question');
 Icon.UNLOCKED = makePreset('UnlockedIcon', 'lock-open');
 Icon.VIDEO = makePreset('VideoIcon', 'video');
+Icon.WARNING = makePreset('WarningIcon', 'triangle-exclamation');
 Icon.VOLUME = makePreset('VolumeIcon', 'volume-high');
 Icon.YOUTUBE = makePreset('YoutubeIcon', ['fab', 'youtube']);
 
diff --git a/resources/js/components/common/Slider.js b/resources/js/components/common/Slider.js
new file mode 100644 (file)
index 0000000..634c15c
--- /dev/null
@@ -0,0 +1,47 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const Slider = ({ children, duration, vertical }) => {
+       const [index, setIndex] = React.useState(0);
+
+       React.useEffect(() => {
+               const interval = setInterval(() => {
+                       setIndex(i => (i + 1) % React.Children.count(children));
+               }, duration);
+               return () => {
+                       clearInterval(interval);
+               };
+       }, [React.Children.count(children), duration]);
+
+       return <div className={`slider-container ${vertical ? 'vertical' : 'horizontal'}`}>
+               <div className="slider-slides" style={{
+                       transform: vertical ? `translateY(${-index * 100}%)` : `translateX(${-index * 100}%)`
+               }}>
+                       {children}
+               </div>
+       </div>;
+};
+
+Slider.propTypes = {
+       children: PropTypes.node,
+       duration: PropTypes.number,
+       vertical: PropTypes.bool,
+};
+
+Slider.defaultProps = {
+       duration: 2500,
+};
+
+const Slide = ({ children }) => {
+       return <div className="slider-slide">
+               {children}
+       </div>;
+};
+
+Slide.propTypes = {
+       children: PropTypes.node,
+};
+
+Slider.Slide = Slide;
+
+export default Slider;
index 84742d8aed9bf6fedeaf5b4860d66fcd661392cc..54f4decd1722f0bcb6488e7f3bb68e05cf1da8e1 100644 (file)
@@ -232,19 +232,34 @@ const Controls = () => {
                                <Col className="mt-5" md={12}>
                                        <div className="d-flex align-items-end justify-content-between">
                                                <h3>{t('twitchBot.guessingGame.settings')}</h3>
-                                               <Button
-                                                       onClick={() => {
-                                                               window.open(
-                                                                       '/guessing-game/controls',
-                                                                       '',
-                                                                       'width=640,height=800,titlebar=0,menubar=0,toolbar=0',
-                                                               );
-                                                       }}
-                                                       title={t('twitchBot.guessingGame.popoutControls')}
-                                                       variant="outline-secondary"
-                                               >
-                                                       <Icon.OPEN title="" />
-                                               </Button>
+                                               <div className="button-bar">
+                                                       {channel.access_key ?
+                                                               <Button
+                                                                       onClick={() => {
+                                                                               window.open(
+                                                                                       `/guessing-game/monitor/${channel.access_key}`,
+                                                                               );
+                                                                       }}
+                                                                       title={t('button.browserSource')}
+                                                                       variant="outline-secondary"
+                                                               >
+                                                                       <Icon.BROWSER_SOURCE title="" />
+                                                               </Button>
+                                                       : null}
+                                                       <Button
+                                                               onClick={() => {
+                                                                       window.open(
+                                                                               '/guessing-game/controls',
+                                                                               '',
+                                                                               'width=640,height=800,titlebar=0,menubar=0,toolbar=0',
+                                                                       );
+                                                               }}
+                                                               title={t('twitchBot.guessingGame.popoutControls')}
+                                                               variant="outline-secondary"
+                                                       >
+                                                               <Icon.OPEN title="" />
+                                                       </Button>
+                                               </div>
                                        </div>
                                        <GuessingSettingsForm
                                                name="gtbk"
index 262bc0d470e148a37756d4f7cfc36c43241831e0..8a9e8c6335668fc87226fbd92de336ec786493cc 100644 (file)
@@ -55,6 +55,7 @@ export default {
                button: {
                        add: 'Hinzufügen',
                        back: 'Zurück',
+                       browserSource: 'Browser Source',
                        cancel: 'Abbrechen',
                        chart: 'Diagramm',
                        close: 'Schließen',
index 2a4f2cb8a82a4178137beb784d23d5b0451f1a60..e12e3a35a689e1ec5d2253dd02ff20682cfc9bbb 100644 (file)
@@ -55,6 +55,7 @@ export default {
                button: {
                        add: 'Add',
                        back: 'Back',
+                       browserSource: 'Browser source',
                        cancel: 'Cancel',
                        chart: 'Chart',
                        close: 'Close',
diff --git a/resources/js/pages/GuessingGameMonitor.js b/resources/js/pages/GuessingGameMonitor.js
new file mode 100644 (file)
index 0000000..bf2922b
--- /dev/null
@@ -0,0 +1,194 @@
+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>;
+};
index cffa5785ad0500fa4c1a74caadb409811196117e..3e0f18528ec985bb236302f63428d0f154bb5dfa 100644 (file)
@@ -5,3 +5,58 @@
        opacity: .6;
        background: repeating-linear-gradient(150deg, #444, #444 1ex, transparent 1ex, transparent 1em);
 }
+
+.guessing-game-monitor {
+       font-size: 5vw;
+       text-align: center;
+
+       .message-box {
+               display: inline-block;
+               padding: 0.5ex 1ex;
+               border-radius: 0.5ex;
+               background: rgba(0, 0, 0, 0.5);
+       }
+       .message-title {
+               display: flex;
+               align-items: center;
+               justify-content: space-between;
+       }
+       .accepting-guesses .message-icon {
+               color: $warning;
+       }
+       .guessing-closed .message-icon {
+               color: $danger;
+       }
+       .message-text {
+               flex-grow: 1;
+               margin-left: 1ex;
+               margin-right: 1ex;
+               width: 70vw;
+               height: 1.2em;
+               line-height: 1;
+       }
+
+       .guessing-stats {
+               display: flex;
+               font-size: 3vw;
+               justify-content: center;
+               margin-top: 1ex;
+       }
+       .guessing-stat {
+               width: 1.3em;
+       }
+       .guessing-box {
+               width: 100%;
+               height: 2em;
+               position: relative;
+       }
+       .guessing-box-bar {
+               background-color: $primary;
+               position: absolute;
+               bottom: 0;
+               width: 100%;
+       }
+       .has-won .guessing-box-bar {
+               background-color: $danger;
+       }
+}
index 4d603f06a3b4926d0a8706e4e1ffdd0cf35477fa..0cb6655c981dd8046b2f040c7dc5f1186acde644 100644 (file)
@@ -78,6 +78,31 @@ h1 {
        }
 }
 
+.slider-container {
+       overflow: hidden;
+       height: 100%;
+       width: 100%;
+
+       > .slider-slides {
+               transition: transform 600ms;
+       }
+       &.horizontal {
+               > .slider-slides {
+                       height: 100%;
+                       white-space: nowrap;
+                       > .slider-slide {
+                               display: inline-block;
+                               width: 100%;
+                       }
+               }
+       }
+       &.vertical {
+               > .slider-slides {
+                       width: 100%;
+               }
+       }
+}
+
 .snes-button-a,
 .snes-button-b,
 .snes-button-x,
index 2135a2fb1d7218070954d6d090adfafc3b48ebf1..0c897af2f23f463e0cfdaac57bb8b698ee2cba03 100644 (file)
@@ -36,6 +36,8 @@ Route::get('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\Chan
 Route::post('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\ChannelController@controlGuessingGame');
 Route::put('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\ChannelController@saveGuessingGame');
 
+Route::get('guessing-game-monitor/{key}', 'App\Http\Controllers\ChannelController@getGuessingGameMonitor');
+
 Route::get('content', 'App\Http\Controllers\TechniqueController@search');
 Route::get('content/{tech:name}', 'App\Http\Controllers\TechniqueController@single');
 Route::put('content/{content}', 'App\Http\Controllers\TechniqueController@update');