->protocols()
->with('user')
->orderBy('created_at', 'desc')
+ ->limit(150)
->get();
return $protocol->values()->toJson();
}
$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) {
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;
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
/>
)}
</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,
})),
--- /dev/null
+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;
const Detail = ({
addRound,
+ moreRounds,
tournament,
}) => {
const { t } = useTranslation();
: null}
</div>
{tournament.rounds ?
- <Rounds rounds={tournament.rounds} tournament={tournament} />
+ <Rounds
+ loadMore={moreRounds}
+ rounds={tournament.rounds}
+ tournament={tournament}
+ />
: null}
</Col>
</Row>
Detail.propTypes = {
addRound: PropTypes.func,
+ moreRounds: PropTypes.func,
tournament: PropTypes.shape({
id: PropTypes.number,
participants: PropTypes.arrayOf(PropTypes.shape({
.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');
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.',
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.',
import NotFound from '../pages/NotFound';
import Detail from '../components/tournament/Detail';
import {
+ canLoadMoreRounds,
+ getLastRound,
patchApplication,
patchParticipant,
patchResult,
};
}, [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) {
<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>;
};
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');