From: Daniel Karbach <daniel.karbach@localhorst.tv>
Date: Sat, 24 Feb 2024 13:38:58 +0000 (+0100)
Subject: limits for huge tournaments
X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=d518ede5ffe8d4e44b0194279a9f32839bc1f903;p=alttp.git

limits for huge tournaments
---

diff --git a/app/Http/Controllers/ProtocolController.php b/app/Http/Controllers/ProtocolController.php
index e4f0280..6b5e48c 100644
--- a/app/Http/Controllers/ProtocolController.php
+++ b/app/Http/Controllers/ProtocolController.php
@@ -14,6 +14,7 @@ class ProtocolController extends Controller
 			->protocols()
 			->with('user')
 			->orderBy('created_at', 'desc')
+			->limit(150)
 			->get();
 		return $protocol->values()->toJson();
 	}
diff --git a/app/Http/Controllers/TournamentController.php b/app/Http/Controllers/TournamentController.php
index 9b21736..f53dad4 100644
--- a/app/Http/Controllers/TournamentController.php
+++ b/app/Http/Controllers/TournamentController.php
@@ -28,21 +28,21 @@ class TournamentController extends Controller
 		$tournament = Tournament::with(
 			'applications',
 			'applications.user',
-			'rounds',
-			'rounds.results',
-			'rounds.results.user',
 			'participants',
 			'participants.user',
 		)->findOrFail($id);
 		$this->authorize('view', $tournament);
-		foreach ($tournament->rounds as $round) {
+		$rounds = $tournament->rounds()->with(['results', 'results.user'])->limit(25)->get();
+		foreach ($rounds as $round) {
 			try {
 				$this->authorize('seeResults', $round);
 			} catch (AuthorizationException) {
 				$round->hideResults();
 			}
 		}
-		return $tournament->toJson();
+		$json = $tournament->toArray();
+		$json['rounds'] = $rounds->toArray();
+		return $json;
 	}
 
 	public function discord(Request $request, Tournament $tournament) {
@@ -81,6 +81,27 @@ class TournamentController extends Controller
 		return $tournament->toJson();
 	}
 
+	public function moreRounds(Request $request, Tournament $tournament) {
+		$this->authorize('view', $tournament);
+
+		$validatedData = $request->validate([
+			'last_known' => 'integer|required',
+		]);
+
+		$rounds = $tournament->rounds()
+			->where('number', '<', $validatedData['last_known'])
+			->with(['results', 'results.user'])
+			->limit(25)->get();
+		foreach ($rounds as $round) {
+			try {
+				$this->authorize('seeResults', $round);
+			} catch (AuthorizationException) {
+				$round->hideResults();
+			}
+		}
+		return $rounds->toArray();
+	}
+
 	public function open(Request $request, Tournament $tournament) {
 		$this->authorize('update', $tournament);
 		$tournament->accept_applications = true;
diff --git a/resources/js/components/rounds/List.js b/resources/js/components/rounds/List.js
index 2ac8f17..5aa2a4c 100644
--- a/resources/js/components/rounds/List.js
+++ b/resources/js/components/rounds/List.js
@@ -4,12 +4,14 @@ import { Alert } from 'react-bootstrap';
 import { withTranslation } from 'react-i18next';
 
 import Item from './Item';
+import LoadMore from './LoadMore';
 import i18n from '../../i18n';
 
 const List = ({
+	loadMore,
 	rounds,
 	tournament,
-}) => rounds && rounds.length ?
+}) => rounds && rounds.length ? <>
 	<ol className="rounds">
 		{rounds.map(round =>
 			<Item
@@ -19,13 +21,17 @@ const List = ({
 			/>
 		)}
 	</ol>
-:
+	{loadMore ?
+		<LoadMore loadMore={loadMore} />
+	: null}
+</> :
 	<Alert variant="info">
 		{i18n.t('rounds.empty')}
 	</Alert>
 ;
 
 List.propTypes = {
+	loadMore: PropTypes.func,
 	rounds: PropTypes.arrayOf(PropTypes.shape({
 		id: PropTypes.number,
 	})),
diff --git a/resources/js/components/rounds/LoadMore.js b/resources/js/components/rounds/LoadMore.js
new file mode 100644
index 0000000..d0ea365
--- /dev/null
+++ b/resources/js/components/rounds/LoadMore.js
@@ -0,0 +1,31 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+const LoadMore = ({ loadMore }) => {
+	const [loading, setLoading] = React.useState(false);
+
+	const { t } = useTranslation();
+
+	const handleLoadMore = React.useCallback(async () => {
+		setLoading(true);
+		try {
+			await loadMore();
+		} finally {
+			setLoading(false);
+		}
+	}, [loadMore]);
+
+	return <div className="d-grid mt-2">
+		<Button disabled={loading} onClick={handleLoadMore} variant="outline-secondary">
+		{t('rounds.loadMore')}
+		</Button>
+	</div>;
+};
+
+LoadMore.propTypes = {
+	loadMore: PropTypes.func,
+};
+
+export default LoadMore;
diff --git a/resources/js/components/tournament/Detail.js b/resources/js/components/tournament/Detail.js
index 84592f6..318e677 100644
--- a/resources/js/components/tournament/Detail.js
+++ b/resources/js/components/tournament/Detail.js
@@ -42,6 +42,7 @@ const getClassName = (tournament, user) => {
 
 const Detail = ({
 	addRound,
+	moreRounds,
 	tournament,
 }) => {
 	const { t } = useTranslation();
@@ -111,7 +112,11 @@ const Detail = ({
 					: null}
 				</div>
 				{tournament.rounds ?
-					<Rounds rounds={tournament.rounds} tournament={tournament} />
+					<Rounds
+						loadMore={moreRounds}
+						rounds={tournament.rounds}
+						tournament={tournament}
+					/>
 				: null}
 			</Col>
 		</Row>
@@ -120,6 +125,7 @@ const Detail = ({
 
 Detail.propTypes = {
 	addRound: PropTypes.func,
+	moreRounds: PropTypes.func,
 	tournament: PropTypes.shape({
 		id: PropTypes.number,
 		participants: PropTypes.arrayOf(PropTypes.shape({
diff --git a/resources/js/helpers/Tournament.js b/resources/js/helpers/Tournament.js
index 9d2962d..7270351 100644
--- a/resources/js/helpers/Tournament.js
+++ b/resources/js/helpers/Tournament.js
@@ -30,6 +30,16 @@ export const getRunners = tournament => {
 		.sort(Participant.compareUsername);
 };
 
+export const getLastRound = tournament => {
+	if (!tournament || !tournament.rounds || !tournament.rounds.length) return null;
+	return tournament.rounds.slice(-1)[0];
+};
+
+export const canLoadMoreRounds = tournament => {
+	const last_round = getLastRound(tournament);
+	return last_round && last_round.number > 1;
+};
+
 export const hasScoreboard = tournament => !!(tournament && tournament.type === 'signup-async');
 
 export const hasSignup = tournament => !!(tournament && tournament.type === 'signup-async');
diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js
index 308ca7f..0d17d73 100644
--- a/resources/js/i18n/de.js
+++ b/resources/js/i18n/de.js
@@ -399,6 +399,7 @@ export default {
 			noSeed: 'Noch kein Seed',
 			numberOfResults_one: '{{ count }} Ergebnis',
 			numberOfResults_other: '{{ count }} Ergebnisse',
+			loadMore: 'weitere Runden laden',
 			lock: 'Runde sperren',
 			lockDescription: 'Wenn die Runde gesperrt wird, können Runner keine Änderungen an ihrem Ergebnis mehr vornehmen.',
 			locked: 'Die Runde ist für weitere Änderungen am Ergebnis gesperrt.',
diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js
index a146ca2..3722991 100644
--- a/resources/js/i18n/en.js
+++ b/resources/js/i18n/en.js
@@ -399,6 +399,7 @@ export default {
 			noSeed: 'No seed set',
 			numberOfResults_one: '{{ count }} submission',
 			numberOfResults_other: '{{ count }} submissions',
+			loadMore: 'load more rounds',
 			lock: 'Lock round',
 			lockDescription: 'When a round is locked, runners cannot submit or change results.',
 			locked: 'Results for this round have been locked.',
diff --git a/resources/js/pages/Tournament.js b/resources/js/pages/Tournament.js
index 5de5dbe..ecaab6c 100644
--- a/resources/js/pages/Tournament.js
+++ b/resources/js/pages/Tournament.js
@@ -10,6 +10,8 @@ import Loading from '../components/common/Loading';
 import NotFound from '../pages/NotFound';
 import Detail from '../components/tournament/Detail';
 import {
+	canLoadMoreRounds,
+	getLastRound,
 	patchApplication,
 	patchParticipant,
 	patchResult,
@@ -97,6 +99,21 @@ export const Component = () => {
 		};
 	}, [id]);
 
+	const moreRounds = React.useCallback(async () => {
+		const last_round = getLastRound(tournament);
+		if (!last_round) return;
+		console.log(last_round);
+		const last_known = last_round.number;
+		const rsp = await axios.get(
+			`/api/tournaments/${id}/more-rounds`,
+			{ params: { last_known } },
+		);
+		setTournament(tournament => ({
+			...tournament,
+			rounds: [...tournament.rounds, ...rsp.data],
+		}));
+	}, [id, tournament]);
+
 	useEffect(() => {
 		const cb = (e) => {
 			if (e.user) {
@@ -132,6 +149,10 @@ export const Component = () => {
 			<title>{tournament.title}</title>
 		</Helmet>
 		<CanonicalLinks base={`/tournaments/${tournament.id}`} />
-		<Detail addRound={addRound} tournament={tournament} />
+		<Detail
+			addRound={addRound}
+			moreRounds={canLoadMoreRounds(tournament) ? moreRounds : null}
+			tournament={tournament}
+		/>
 	</ErrorBoundary>;
 };
diff --git a/routes/api.php b/routes/api.php
index 2b8b682..ca02e36 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -70,6 +70,7 @@ Route::get('tech', 'App\Http\Controllers\TechniqueController@search');
 Route::get('tech/{tech:name}', 'App\Http\Controllers\TechniqueController@single');
 
 Route::get('tournaments/{id}', 'App\Http\Controllers\TournamentController@single');
+Route::get('tournaments/{tournament}/more-rounds', 'App\Http\Controllers\TournamentController@moreRounds');
 Route::post('tournaments/{tournament}/apply', 'App\Http\Controllers\TournamentController@apply');
 Route::post('tournaments/{tournament}/close', 'App\Http\Controllers\TournamentController@close');
 Route::post('tournaments/{tournament}/discord', 'App\Http\Controllers\TournamentController@discord');