]> git.localhorst.tv Git - alttp.git/commitdiff
step ladder mode subscriptions
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 12 Jul 2025 16:17:01 +0000 (18:17 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 12 Jul 2025 16:17:01 +0000 (18:17 +0200)
14 files changed:
app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php
app/Http/Controllers/DiscordGuildController.php
app/Http/Controllers/EventController.php
app/Http/Controllers/StepLadderModeController.php [new file with mode: 0644]
app/Models/DiscordGuild.php
app/Models/DiscordGuildLadderSubscription.php [new file with mode: 0644]
app/Models/Episode.php
database/migrations/2025_07_12_130447_create_discord_guild_ladder_subscriptions_table.php [new file with mode: 0644]
resources/js/components/common/StepLadderModeSelect.jsx [new file with mode: 0644]
resources/js/components/discord-bot/GuildControls.jsx
resources/js/components/discord-bot/LadderSubscriptions.jsx [new file with mode: 0644]
resources/js/i18n/de.js
resources/js/i18n/en.js
routes/api.php

index 47fdf75da9d01a81ccbab86b731f54e17d13fa3e..94bb5229027738a779ab10a81c8da273835b3e4b 100644 (file)
@@ -43,20 +43,26 @@ class DiscordEpisodeSubscriptionsCommand extends Command
 
        private function handleGuild(DiscordGuild $guild): void {
                $from = now()->sub(1, 'hour');
+               $until = now()->add(14, 'days');
                $eventIDs = $guild->event_subscriptions->pluck('event_id')->toArray();
+               $modeIDs = $guild->ladder_subscriptions->pluck('step_ladder_mode_id')->toArray();
                $userIDs = $guild->user_subscriptions->pluck('user_id')->toArray();
-               if (empty($eventIDs) && empty($userIDs)) {
+               if (empty($eventIDs) && empty($modeIDs) && empty($userIDs)) {
                        return;
                }
 
                $query = Episode::with(['channels', 'event', 'players'])
                        ->where('episodes.start', '>', $from)
+                       ->where('episodes.start', '<', $until)
                        ->orderBy('episodes.start', 'ASC')
                        ->limit(20);
-               $query->where(function ($subquery) use ($eventIDs, $userIDs) {
+               $query->where(function ($subquery) use ($eventIDs, $modeIDs, $userIDs) {
                        if (!empty($eventIDs)) {
                                $subquery->whereIn('episodes.event_id', $eventIDs);
                        }
+                       if (!empty($modeIDs)) {
+                               $subquery->orWhereIn('episodes.step_ladder_mode_id', $modeIDs);
+                       }
                        if (!empty($userIDs)) {
                                $subquery->orWhereHas('players', function ($builder) use ($userIDs) {
                                        $builder->whereIn('episode_players.user_id', $userIDs);
@@ -70,28 +76,7 @@ class DiscordEpisodeSubscriptionsCommand extends Command
        }
 
        private function handleEpisode(DiscordGuild $guild, Episode $episode): void {
-               $mtime = $episode->updated_at;
-               foreach ($episode->channels as $channel) {
-                       if ($mtime < $channel->updated_at) {
-                               $mtime = $channel->updated_at;
-                       }
-               }
-               foreach ($episode->confirmedCrew() as $crew) {
-                       if ($mtime < $crew->updated_at) {
-                               $mtime = $crew->updated_at;
-                       }
-                       if ($crew->user && $mtime < $crew->user->updated_at) {
-                               $mtime = $crew->user->updated_at;
-                       }
-               }
-               foreach ($episode->players as $player) {
-                       if ($mtime < $player->updated_at) {
-                               $mtime = $player->updated_at;
-                               if ($player->user && $mtime < $player->user->updated_at) {
-                                       $mtime = $crew->user->updated_at;
-                               }
-                       }
-               }
+               $mtime = $episode->getMTime();
                $memo = $this->getMemo($guild, $episode);
                if ((is_null($memo->synced_at) && $episode->start > now()) || (!is_null($memo->synced_at) && $memo->synced_at < $mtime)) {
                        $this->line('pushing '.$episode->id.' '.$episode->getScheduledEventName());
index c2a5691d1f37fb487103a52f994977561c58c29b..979c675a8659a888418357a1f6a954afcca21276 100644 (file)
@@ -69,6 +69,8 @@ class DiscordGuildController extends Controller
                $guild = DiscordGuild::with([
                        'event_subscriptions',
                        'event_subscriptions.event',
+                       'ladder_subscriptions',
+                       'ladder_subscriptions.step_ladder_mode',
                        'user_subscriptions',
                        'user_subscriptions.user',
                ])->where('guild_id', '=', $guild_id)->firstOrFail();
@@ -82,14 +84,19 @@ class DiscordGuildController extends Controller
 
                $validatedData = $request->validate([
                        'add_event' => 'numeric|exists:App\Models\Event,id',
+                       'add_ladder' => 'numeric|exists:App\Models\StepLadderMode,id',
                        'add_user' => 'numeric|exists:App\Models\User,id',
                        'remove_event' => 'numeric|exists:App\Models\Event,id',
+                       'remove_ladder' => 'numeric|exists:App\Models\StepLadderMode,id',
                        'remove_user' => 'numeric|exists:App\Models\User,id',
                ]);
 
                if (isset($validatedData['add_event'])) {
                        $guild->event_subscriptions()->create(['event_id' => $validatedData['add_event']]);
                }
+               if (isset($validatedData['add_ladder'])) {
+                       $guild->ladder_subscriptions()->create(['step_ladder_mode_id' => $validatedData['add_ladder']]);
+               }
                if (isset($validatedData['add_user'])) {
                        $guild->user_subscriptions()->create(['user_id' => $validatedData['add_user']]);
                }
@@ -97,6 +104,10 @@ class DiscordGuildController extends Controller
                        $sub = $guild->event_subscriptions()->where('event_id', '=', $validatedData['remove_event'])->firstOrFail();
                        $sub->delete();
                }
+               if (isset($validatedData['remove_ladder'])) {
+                       $sub = $guild->ladder_subscriptions()->where('step_ladder_mode_id', '=', $validatedData['remove_ladder'])->firstOrFail();
+                       $sub->delete();
+               }
                if (isset($validatedData['remove_user'])) {
                        $sub = $guild->user_subscriptions()->where('user_id', '=', $validatedData['remove_user'])->firstOrFail();
                        $sub->delete();
@@ -104,6 +115,8 @@ class DiscordGuildController extends Controller
                $guild->load([
                        'event_subscriptions',
                        'event_subscriptions.event',
+                       'ladder_subscriptions',
+                       'ladder_subscriptions.step_ladder_mode',
                        'user_subscriptions',
                        'user_subscriptions.user',
                ]);
index 9d42b320e39a099c80a1f0d452d93cfe08cc9865..f7fd1cef222d5a9aed0f7ac430b36b48a3305415 100644 (file)
@@ -3,7 +3,6 @@
 namespace App\Http\Controllers;
 
 use App\Models\Event;
-use Carbon\Carbon;
 use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Illuminate\Http\Request;
 
diff --git a/app/Http/Controllers/StepLadderModeController.php b/app/Http/Controllers/StepLadderModeController.php
new file mode 100644 (file)
index 0000000..3f9121d
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\StepLadderMode;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
+use Illuminate\Http\Request;
+
+class StepLadderModeController extends Controller
+{
+
+       public function search(Request $request) {
+               $validatedData = $request->validate([
+                       'exclude_ids' => 'array|nullable',
+                       'exclude_ids.*' => 'int',
+                       'limit' => 'nullable|int',
+                       'phrase' => 'nullable|string',
+               ]);
+               $modes = StepLadderMode::query();
+               if (!empty($validatedData['exclude_ids'])) {
+                       $modes->whereNotIn('id', $validatedData['exclude_ids']);
+               }
+               if (isset($validatedData['limit'])) {
+                       $modes->limit($validatedData['limit']);
+               }
+               if (isset($validatedData['phrase'])) {
+                       $modes->where('name', 'LIKE', '%'.$validatedData['phrase'].'%');
+               }
+               $modes->orderBy('name');
+               return $modes->get()->toJson();
+       }
+
+       public function single(Request $request, StepLadderMode $mode) {
+               $mode->load('description');
+               return $mode->toJson();
+       }
+
+}
index f1821db6bee5b7dfe12528e0b6b4e056923a85a8..c210babefb917b7eab220e7677c7a8929e5bae33 100644 (file)
@@ -60,6 +60,10 @@ class DiscordGuild extends Model
                return $this->hasMany(DiscordGuildEventSubscription::class);
        }
 
+       public function ladder_subscriptions() {
+               return $this->hasMany(DiscordGuildLadderSubscription::class);
+       }
+
        public function roles() {
                return $this->hasMany(DiscordRole::class)->orderBy('position');
        }
diff --git a/app/Models/DiscordGuildLadderSubscription.php b/app/Models/DiscordGuildLadderSubscription.php
new file mode 100644 (file)
index 0000000..7ef16ad
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
+use Illuminate\Database\Eloquent\Model;
+
+class DiscordGuildLadderSubscription extends Model {
+
+       use BroadcastsEvents;
+
+       public function broadcastOn(string $event): array {
+               $channels = [];
+               if ($this->discord_guild_id) {
+                       $channels[] = new PrivateChannel('DiscordGuild.'.$this->discord_guild_id);
+               }
+               return $channels;
+       }
+
+       public function guild() {
+               return $this->belongsTo(DiscordGuild::class);
+       }
+
+       public function step_ladder_mode() {
+               return $this->belongsTo(StepLadderMode::class);
+       }
+
+       protected $fillable = [
+               'discord_guild_id',
+               'step_ladder_mode_id',
+       ];
+
+       protected $with = [
+               'step_ladder_mode',
+       ];
+
+}
index df7c802d1f6bed5828779e54388393b233c5dcbd..6ca4e0b4ac54afcaea291b9c7e672b3237a9a4a7 100644 (file)
@@ -4,6 +4,7 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
 
 class Episode extends Model
 {
@@ -44,6 +45,32 @@ class Episode extends Model
                return $this->belongsTo(StepLadderMode::class);
        }
 
+       public function getMTime() {
+               $mtime = $this->updated_at;
+               foreach ($this->channels as $channel) {
+                       if ($mtime < $channel->updated_at) {
+                               $mtime = $channel->updated_at;
+                       }
+               }
+               foreach ($this->confirmedCrew as $crew) {
+                       if ($mtime < $crew->updated_at) {
+                               $mtime = $crew->updated_at;
+                       }
+                       if ($crew->user && $mtime < $crew->user->updated_at) {
+                               $mtime = $crew->user->updated_at;
+                       }
+               }
+               foreach ($this->players as $player) {
+                       if ($mtime < $player->updated_at) {
+                               $mtime = $player->updated_at;
+                               if ($player->user && $mtime < $player->user->updated_at) {
+                                       $mtime = $crew->user->updated_at;
+                               }
+                       }
+               }
+               return $mtime;
+       }
+
        public function getScheduledEventName(): string {
                $parts = [];
                if (count($this->players) == 4) {
@@ -105,6 +132,9 @@ class Episode extends Model
                                }
                        }
                }
+               if ($this->raceroom) {
+                       $description .= "\nRaceroom: [".Str::afterLast($this->raceroom, '/').']('.$this->raceroom.")\n";
+               }
                return $description;
        }
 
diff --git a/database/migrations/2025_07_12_130447_create_discord_guild_ladder_subscriptions_table.php b/database/migrations/2025_07_12_130447_create_discord_guild_ladder_subscriptions_table.php
new file mode 100644 (file)
index 0000000..629da7b
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        */
+       public function up(): void {
+               Schema::create('discord_guild_ladder_subscriptions', function (Blueprint $table) {
+                       $table->id();
+                       $table->foreignId('discord_guild_id')->constrained();
+                       $table->foreignId('step_ladder_mode_id')->constrained();
+                       $table->timestamps();
+                       $table->unique(['discord_guild_id', 'step_ladder_mode_id'], 'guild_mode_unique');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void {
+               Schema::dropIfExists('discord_guild_ladder_subscriptions');
+       }
+};
diff --git a/resources/js/components/common/StepLadderModeSelect.jsx b/resources/js/components/common/StepLadderModeSelect.jsx
new file mode 100644 (file)
index 0000000..4e2aa1b
--- /dev/null
@@ -0,0 +1,123 @@
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { Button, Form, ListGroup } from 'react-bootstrap';
+
+import Icon from './Icon';
+import debounce from '../../helpers/debounce';
+
+const StepLadderModeSelect = ({ excludeIds = [], name, onChange, value }) => {
+       const [resolved, setResolved] = useState(null);
+       const [results, setResults] = useState([]);
+       const [search, setSearch] = useState('');
+       const [showResults, setShowResults] = useState(false);
+
+       const ref = useRef(null);
+
+       useEffect(() => {
+               const handleEventOutside = e => {
+                       if (ref.current && !ref.current.contains(e.target)) {
+                               setShowResults(false);
+                       }
+               };
+               document.addEventListener('mousedown', handleEventOutside, true);
+               document.addEventListener('focus', handleEventOutside, true);
+               return () => {
+                       document.removeEventListener('mousedown', handleEventOutside, true);
+                       document.removeEventListener('focus', handleEventOutside, true);
+               };
+       }, []);
+
+       let ctrl = null;
+       const fetch = useCallback(debounce(async phrase => {
+               if (ctrl) {
+                       ctrl.abort();
+               }
+               ctrl = new AbortController();
+               try {
+                       const response = await axios.get(`/api/step-ladder-modes`, {
+                               params: {
+                                       exclude_ids: excludeIds,
+                                       limit: 5,
+                                       phrase,
+                               },
+                               signal: ctrl.signal,
+                       });
+                       ctrl = null;
+                       setResults(response.data);
+                       if (phrase) {
+                               setShowResults(true);
+                       }
+               } catch (e) {
+                       ctrl = null;
+                       console.error(e);
+               }
+       }, 300), [excludeIds]);
+
+       useEffect(() => {
+               fetch(search);
+       }, [search]);
+
+       useEffect(() => {
+               if (value) {
+                       axios
+                               .get(`/api/step-ladder-modes/${value}`)
+                       .then(response => {
+                               setResolved(response.data);
+                       });
+               } else {
+                       setResolved(null);
+               }
+       }, [value]);
+
+       if (value) {
+               return <div className="d-flex justify-content-between">
+                       {resolved ? <span>{resolved.name} ({resolved.ext_id.substr(11)})</span> : <span>value</span>}
+                       <Button
+                               onClick={() => onChange({ target: { name, value: null }})}
+                               size="sm"
+                               variant="outline-danger"
+                       >
+                               <Icon.REMOVE />
+                       </Button>
+               </div>;
+       }
+       return <div className={`model-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
+               <Form.Control
+                       className="search-input"
+                       name={Math.random().toString(20).substr(2, 10)}
+                       onChange={e => setSearch(e.target.value)}
+                       onFocus={() => setShowResults(true)}
+                       type="search"
+                       value={search}
+               />
+               <div className="search-results-holder">
+                       <ListGroup className="search-results">
+                               {results.map(result =>
+                                       <ListGroup.Item
+                                               action
+                                               key={result.id}
+                                               onClick={() => {
+                                                       onChange({
+                                                               target: { name, value: result.id },
+                                                       });
+                                                       setSearch('');
+                                                       setShowResults(false);
+                                               }}
+                                       >
+                                               <span>{result.name} ({result.ext_id.substr(11)})</span>
+                                       </ListGroup.Item>
+                               )}
+                       </ListGroup>
+               </div>
+       </div>;
+};
+
+StepLadderModeSelect.propTypes = {
+       excludeIds: PropTypes.arrayOf(PropTypes.number),
+       name: PropTypes.string,
+       onChange: PropTypes.func,
+       value: PropTypes.string,
+};
+
+export default StepLadderModeSelect;
index e4a35c65fb518793799b022bb38dc545f4e213a9..499d77d510691abf6b7e5eada3ab7302d493b4fc 100644 (file)
@@ -8,6 +8,7 @@ import toastr from 'toastr';
 import EventSubscriptions from './EventSubscriptions';
 import GuildCrew from './GuildCrew';
 import GuildProtocol from './GuildProtocol';
+import LadderSubscriptions from './LadderSubscriptions';
 import UserSubscriptions from './UserSubscriptions';
 import ErrorBoundary from '../common/ErrorBoundary';
 import { compareTitle } from '../../helpers/Event';
@@ -74,6 +75,27 @@ const GuildControls = ({ guild, patchGuild }) => {
                }
        }, [guild.guild_id, patchGuild, t]);
 
+       const addLadderSub = React.useCallback(async (mode_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               add_ladder: mode_id,
+                       });
+                       patchGuild(response.data);
+               } catch (error) {
+                       toastr.error(t('discordBot.ladderSubError', { error }));
+               }
+       }, [guild.guild_id, patchGuild, t]);
+
+       const removeLadderSub = React.useCallback(async (mode_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               remove_ladder: mode_id,
+                       });
+                       patchGuild(response.data);
+               } catch (error) {
+                       toastr.error(t('discordBot.ladderUnsubError', { error }));
+               }
+       }, [guild.guild_id, patchGuild, t]);
 
        const addUserSub = React.useCallback(async (user_id) => {
                try {
@@ -156,6 +178,21 @@ const GuildControls = ({ guild, patchGuild }) => {
                                        patchGuild(g => ({ ...g, event_subscriptions: (g.event_subscriptions || []).filter(c => c.id !== e.model.id) }));
                                }
                        })
+                       .listen('.DiscordGuildLadderSubscriptionCreated', e => {
+                               if (e.model) {
+                                       patchGuild(g => ({ ...g, ladder_subscriptions: [...g.ladder_subscriptions || [], e.model] }));
+                               }
+                       })
+                       .listen('.DiscordGuildLadderSubscriptionUpdated', e => {
+                               if (e.model) {
+                                       patchGuild(g => ({ ...g, ladder_subscriptions: (g.ladder_subscriptions || []).map(c => c.id === e.model.id ? { ...c, ...e.model } : c) }));
+                               }
+                       })
+                       .listen('.DiscordGuildLadderSubscriptionDeleted', e => {
+                               if (e.model) {
+                                       patchGuild(g => ({ ...g, ladder_subscriptions: (g.ladder_subscriptions || []).filter(c => c.id !== e.model.id) }));
+                               }
+                       })
                        .listen('.DiscordGuildUserSubscriptionCreated', e => {
                                if (e.model) {
                                        patchGuild(g => ({ ...g, user_subscriptions: [...g.user_subscriptions || [], e.model] }));
@@ -203,7 +240,7 @@ const GuildControls = ({ guild, patchGuild }) => {
                                </Col>
                        </Row>
                        <Row>
-                               <Col md={6}>
+                               <Col className="my-5" md={6}>
                                        <h3>{t('discordBot.eventSubscriptions')}</h3>
                                        <p style={{ minHeight: '3.5em' }}>{t('discordBot.eventSubscriptionDescription')}</p>
                                        <ErrorBoundary>
@@ -215,7 +252,7 @@ const GuildControls = ({ guild, patchGuild }) => {
                                                />
                                        </ErrorBoundary>
                                </Col>
-                               <Col md={6}>
+                               <Col className="my-5" md={6}>
                                        <h3>{t('discordBot.userSubscriptions')}</h3>
                                        <p style={{ minHeight: '3.5em' }}>{t('discordBot.userSubscriptionDescription')}</p>
                                        <ErrorBoundary>
@@ -227,6 +264,18 @@ const GuildControls = ({ guild, patchGuild }) => {
                                                />
                                        </ErrorBoundary>
                                </Col>
+                               <Col className="my-5" md={6}>
+                                       <h3>{t('discordBot.ladderSubscriptions')}</h3>
+                                       <p style={{ minHeight: '3.5em' }}>{t('discordBot.ladderSubscriptionDescription')}</p>
+                                       <ErrorBoundary>
+                                               <LadderSubscriptions
+                                                       addMode={addLadderSub}
+                                                       guild={guild}
+                                                       removeMode={removeLadderSub}
+                                                       subs={guild.ladder_subscriptions || []}
+                                               />
+                                       </ErrorBoundary>
+                               </Col>
                        </Row>
                </section>
                <section className="mt-5">
@@ -244,6 +293,8 @@ GuildControls.propTypes = {
                })),
                id: PropTypes.number,
                guild_id: PropTypes.string,
+               ladder_subscriptions: PropTypes.arrayOf(PropTypes.shape({
+               })),
                user_subscriptions: PropTypes.arrayOf(PropTypes.shape({
                })),
        }),
diff --git a/resources/js/components/discord-bot/LadderSubscriptions.jsx b/resources/js/components/discord-bot/LadderSubscriptions.jsx
new file mode 100644 (file)
index 0000000..65bea33
--- /dev/null
@@ -0,0 +1,58 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import StepLadderModeSelect from '../common/StepLadderModeSelect';
+import Icon from '../common/Icon';
+import { mayManageGuild } from '../../helpers/permissions';
+import { useUser } from '../../hooks/user';
+
+const LadderSubscriptions = ({ addMode, guild, removeMode, subs }) => {
+       const { t } = useTranslation();
+       const { user } = useUser();
+
+       const mayManage = React.useMemo(() => mayManageGuild(user, guild), [guild, user]);
+
+       return <div>
+               {mayManage ?
+                       <Form.Group controlId="lsubs.addMode">
+                               <Form.Label>{t('discordBot.addLadderMode')}</Form.Label>
+                               <Form.Control
+                                       as={StepLadderModeSelect}
+                                       excludeIds={subs.map(s => s.step_ladder_mode_id)}
+                                       onChange={e => addMode(e.target.value)}
+                                       value=""
+                               />
+                       </Form.Group>
+               : null}
+               {subs.map((lsub) => (
+                       <div className="d-flex align-items-center justify-content-between my-2" key={lsub.id}>
+                               <div>{lsub.step_ladder_mode.name} ({lsub.step_ladder_mode.ext_id.substr(11)})</div>
+                               {mayManage ?
+                                       <div className="button-bar">
+                                               <Button
+                                                       onClick={() => removeMode(lsub.step_ladder_mode_id)}
+                                                       size="sm"
+                                                       title={t('button.remove')}
+                                                       variant="outline-danger"
+                                               >
+                                                       <Icon.DELETE title="" />
+                                               </Button>
+                                       </div>
+                               : null}
+                       </div>
+               ))}
+       </div>;
+};
+
+LadderSubscriptions.propTypes = {
+       addMode: PropTypes.func,
+       guild: PropTypes.shape({
+       }),
+       removeMode: PropTypes.func,
+       subs: PropTypes.arrayOf(PropTypes.shape({
+       })),
+};
+
+export default LadderSubscriptions;
index 89d4d669b93b49ae1768e5c0af60d36d74119aaa..21296fef37a6ebfd0c70cf133649619106ef7ac0 100644 (file)
@@ -140,6 +140,7 @@ export default {
                discordBot: {
                        addCrew: 'User hinzufügen',
                        addEvent: 'Event abonnieren',
+                       addLadderMode: 'Ladder Mode abonnieren',
                        addUser: 'User abonnieren',
                        channel: 'Kanal',
                        channelControls: 'Kanal-Steuerung',
@@ -179,6 +180,8 @@ export default {
                        guildProtocol: 'Command Protokoll',
                        heading: 'Discord Bot',
                        invite: 'Bot einladen',
+                       ladderSubscriptionDescription: 'Races abonnierter Ladder Modi werden als Discord Event angelegt. Einige der Einträge tauchen von Ladderseite aus mehrfach auf. In dem Fall bitte alle Doubletten hinzufügen, sonst wird möglicherweise nur jedes 2. oder 3. Race erkannt.',
+                       ladderSubscriptions: 'Abonnierte Ladder Modi',
                        message: 'Nachricht',
                        messageError: 'Fehler beim Senden',
                        messageSuccess: 'Nachricht in Warteschlange',
index 9f28396748819e5f75c56a08c9d787e08a0db1c0..1b504690e10109a32736f62673123d50ed992531 100644 (file)
@@ -140,6 +140,7 @@ export default {
                discordBot: {
                        addCrew: 'Add user',
                        addEvent: 'Subscribe to event',
+                       addLadderMode: 'Subscribe to ladder mode',
                        addUser: 'Subscribe to user',
                        channel: 'Channel',
                        channelControls: 'Channel controls',
@@ -179,6 +180,8 @@ export default {
                        guildProtocol: 'Command protocol',
                        heading: 'Discord Bot',
                        invite: 'Invite bot',
+                       ladderSubscriptionDescription: 'Races of subscribed modes will be added as Discord events. Some of the modes have multiple entries on ladder. Please select all dupes, otherwise you may be missing every other or third race in some cases.',
+                       ladderSubscriptions: 'Ladder mode subscriptions',
                        message: 'Message',
                        messageError: 'Error sending message',
                        messageSuccess: 'Message queued',
index 7c4d0ea19006b674f1d56ab51aa9971e7e0ab2ea..90fefbf950c5c8927846f0825908bf34efef317e 100644 (file)
@@ -85,6 +85,9 @@ Route::post('rounds/{round}/lock', 'App\Http\Controllers\RoundController@lock');
 Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setSeed');
 Route::post('rounds/{round}/unlock', 'App\Http\Controllers\RoundController@unlock');
 
+Route::get('step-ladder-modes', 'App\Http\Controllers\StepLadderModeController@search');
+Route::get('step-ladder-modes/{mode}', 'App\Http\Controllers\StepLadderModeController@single');
+
 Route::get('tech', 'App\Http\Controllers\TechniqueController@search');
 Route::get('tech/{tech:name}', 'App\Http\Controllers\TechniqueController@single');