~vpzom/bracketmonster

3f93e6a525a10bee455767ccfdf8e16d002b1ff2 — Colin Reeder 5 months ago a2d246b
no-js match edit
M rosebush/src/client/lang/en.json => rosebush/src/client/lang/en.json +1 -0
@@ 12,6 12,7 @@
	"loading": "Loading…",
	"loggingIn": "Logging in…",
	"login": "Login",
	"matchNotFound": "Failed to find match",
	"myBrackets": "My Brackets",
	"name": "Name",
	"newBracketPage": "Create Bracket",

M rosebush/src/client/pages/bracket.tsx => rosebush/src/client/pages/bracket.tsx +22 -14
@@ 26,7 26,7 @@ enum PlayerOutcome {
	Loss = "loss",
}

interface MatchInfo {
export interface MatchInfo {
	id: number;
	players: Array<{
		player: null | {id: number};


@@ 198,13 198,7 @@ class BracketView extends Component<BracketViewProps, BracketViewState> {
	public constructor(props: BracketViewProps) {
		super(props);

		const playerMap: PlayerMap = {};

		props.bracket.players.forEach(player => {
			playerMap[player.id] = player;
		});

		this.state = {playerMap};
		this.state = {playerMap: makePlayerMap(props.bracket.players)};
	}

	render(props: BracketViewProps, state: BracketViewState) {


@@ 260,7 254,7 @@ class BracketView extends Component<BracketViewProps, BracketViewState> {
													})}
												</ul>
												{showButtons && <div class="buttons">
													<button onClick={this.edit.bind(this, match)}><Edit size={16} /></button>
													<a href={"/dialog/matchEdit/" + bracket.id + "/" + match.id} class="button" onClick={this.editClick.bind(this, match)}><Edit size={16} /></a>
												</div>}
											</div>
										</div>;


@@ 287,7 281,9 @@ class BracketView extends Component<BracketViewProps, BracketViewState> {
		this.props.addModal(<BracketInfoEditDialog bracket={this.props.bracket} />);
	}

	edit(match: MatchInfo): void {
	@bind
	editClick(match: MatchInfo, evt: Event): void {
		evt.preventDefault();
		this.props.addModal(<MatchEditDialog match={match} playerMap={this.state.playerMap} bracket={this.props.bracket} />);
	}
}


@@ 322,7 318,7 @@ function LeaderboardView(props: {bracket: BracketInfo}): JSX.Element {
}

interface MatchEditProps {
	bracket: BracketInfo;
	bracket: Pick<BracketInfo, "id" | "use_scores">;
	match: MatchInfo;
	playerMap: PlayerMap;
}


@@ 334,7 330,7 @@ interface MatchEditState {
	submitting: boolean;
}

class MatchEditDialog extends Component<MatchEditProps, MatchEditState> {
export class MatchEditDialog extends Component<MatchEditProps, MatchEditState> {
	private modal = createRef();

	public constructor(props: MatchEditProps) {


@@ 349,8 345,10 @@ class MatchEditDialog extends Component<MatchEditProps, MatchEditState> {

	public render(props: MatchEditProps, state: MatchEditState) {
		return <Modal title={<Text id="editMatch" />} ref={this.modal}>
			<form onSubmit={this.submit.bind(this)}>
			<form onSubmit={this.submit.bind(this)} method="POST" action="/formSubmit/matchEdit">
				<div class="modal-body">
					<input type="hidden" name="bracket" value={props.bracket.id} />
					<input type="hidden" name="match" value={props.match.id} />
					<RadioGroup name="winner" selectedValue={String(state.winner)} onChange={this.onChangeWinner.bind(this)}>
						<table>
							<tr>


@@ 365,7 363,7 @@ class MatchEditDialog extends Component<MatchEditProps, MatchEditState> {
										{player.player === null ? ["[", <Text id="unknown" />, "]"] : props.playerMap[player.player.id].name}
									</td>
									{props.bracket.use_scores && <td>
										<input type="number" value={state.scores[idx]} onChange={this.onChangeScore.bind(this, idx)} />
										<input type="number" value={state.scores[idx]} onChange={this.onChangeScore.bind(this, idx)} name="score[]" />
									</td>}
									<td>
										<Radio value={String(idx)} />


@@ 485,3 483,13 @@ export class BracketInfoEditDialog extends Component<BracketInfoEditProps, Brack
		});
	}
}

export function makePlayerMap(players: PlayerInfo[]) {
	const playerMap: PlayerMap = {};

	players.forEach(player => {
		playerMap[player.id] = player;
	});

	return playerMap;
}

A rosebush/src/client/pages/dialog/matchEdit.tsx => rosebush/src/client/pages/dialog/matchEdit.tsx +56 -0
@@ 0,0 1,56 @@
import { bind } from "decko";
import { Component, h } from "preact";
import { Text } from "preact-jsx-i18n";

import Data, { LoadState } from "../../Data";
import fetchWithError from "../../fetchWithError";
import { FakeModalContainer } from "../../modals";
import { addData } from "../../preload";

import { BracketInfo, MatchEditDialog, MatchInfo, makePlayerMap } from "../bracket";

interface PageProps {
	bracketID: string;
	matchID: string;
}

interface PageState {
	bracket: LoadState<Omit<BracketInfo, "description_html">>;
}

export default class BracketInfoEditPage extends Component<PageProps, PageState> {
	public render(props: PageProps, state: PageState) {
		return <Data state={state.bracket}>{this.renderContent}</Data>;
	}

	@bind
	private renderContent(bracket: Omit<BracketInfo, "description_html">) {
		let match: MatchInfo | null = null;

		for(let i = 0; i < bracket.matches.length; i++) {
			const current = bracket.matches[i];
			if(String(current.id) === this.props.matchID) {
				match = current;
				break;
			}
		}

		if(match === null) {
			return <div class="errorBox">
				<Text id="errorPrefix" /> <Text id="matchNotFound" />
			</div>;
		}

		return <FakeModalContainer backLink={"/bracket/" + bracket.id}>
			<MatchEditDialog bracket={bracket} playerMap={makePlayerMap(bracket.players)} match={match} />
		</FakeModalContainer>;
	}
}

addData<PageProps, PageState, BracketInfoEditPage>(
	BracketInfoEditPage,
	(props, fetch) => ({
		bracket: fetchWithError(fetch, "/api/v1/brackets/" + props.bracketID)
			.then(res => res.json()),
	}),
);

M rosebush/src/client/routes.ts => rosebush/src/client/routes.ts +1 -0
@@ 6,6 6,7 @@ const routes: Array<{path: string; component: string}> = [
	{path: "/register", component: "register"},
	{path: "/login", component: "login"},
	{path: "/dialog/bracketInfoEdit/:bracketID", component: "dialog/bracketInfoEdit"},
	{path: "/dialog/matchEdit/:bracketID/:matchID", component: "dialog/matchEdit"},
];

export default routes;

M rosebush/src/server/index.tsx => rosebush/src/server/index.tsx +30 -0
@@ 262,6 262,36 @@ http.createServer(async (req_, res) => {
				res.write("Successfully edited.");
				res.end();
			}
			else if(rest === "matchEdit") {
				let winner;

				if(fields.winner === "null") winner = null;
				else winner = parseInt(String(fields.winner), 10);

				const resp = await apiFetchRaw(
					req,
					"v1/brackets/" + fields.bracket + "/matches/" + fields.match,
					{
						method: "PATCH",
						body: JSON.stringify({
							scores: (fields["score[]"] as string[]).map(str => parseInt(str, 10)),
							winner,
						}),
					},
				);

				if(resp.status < 200 || resp.status >= 300) {
					const text = await resp.text();
					res.writeHead(resp.status);
					res.write(text);
					res.end();
					return;
				}

				res.writeHead(303, {Location: "/bracket/" + fields.bracket});
				res.write("Successfully edited.");
				res.end();
			}
			else {
				res.writeHead(404);
				res.write("Not Found");