~vpzom/bracketmonster

a2d246b92a6b52b5a2e98c770b833fab4d208f39 — Colin Reeder 5 months ago 9b8e121
Non-JS support for bracket info editing
M rosebush/src/client/modals.tsx => rosebush/src/client/modals.tsx +21 -1
@@ 5,6 5,7 @@ interface ModalContextValue {
	closing: boolean;
	dismiss(): void;
	remove(): void;
	dismissHref?: string;
}

export interface ModalInfo {


@@ 83,6 84,25 @@ Modal.contextType = ModalContext;

export function CancelButton(_props: {}): JSX.Element {
	return <ModalContext.Consumer>
		{ctx => <button type="button" onClick={ctx?.dismiss}><Text id="cancel" /></button>}
		{ctx => {
			const content = <Text id="cancel" />;
			if(ctx && ctx.dismissHref) {
				return <a class="button" href={ctx.dismissHref} onClick={ctx.dismiss}>{content}</a>;
			}
			return <button type="button" onClick={ctx?.dismiss}>{content}</button>;
		}}
	</ModalContext.Consumer>;
}

export function FakeModalContainer(props: {backLink: string; children?: ComponentChildren}) {
	return <ModalContext.Provider
		value={{
			closing: false,
			dismiss: () => undefined,
			remove: () => undefined,
			dismissHref: props.backLink,
		}}
	>
		{props.children}
	</ModalContext.Provider>;
}

M rosebush/src/client/pages/bracket.tsx => rosebush/src/client/pages/bracket.tsx +12 -8
@@ 39,7 39,7 @@ interface MatchInfo {
	skipped: boolean;
}

interface BracketInfo {
export interface BracketInfo {
	id: BracketID;
	name: string;
	description_md: string;


@@ 230,7 230,7 @@ class BracketView extends Component<BracketViewProps, BracketViewState> {
						<h1>
							{bracket.name}
							&nbsp;
							{props.yourInfo && props.yourInfo.isAdmin && <button onClick={this.editBracketInfo.bind(this)}><Edit size={16} /></button>}
							{props.yourInfo && props.yourInfo.isAdmin && <a class="button" href={"/dialog/bracketInfoEdit/" + bracket.id} onClick={this.editBracketInfoClick}><Edit size={16} /></a>}
						</h1>
						<p dangerouslySetInnerHTML={{__html: bracket.description_html}}></p>
						<LeaderboardView bracket={bracket} />


@@ 281,7 281,9 @@ class BracketView extends Component<BracketViewProps, BracketViewState> {
		</div>;
	}

	editBracketInfo(): void {
	@bind
	editBracketInfoClick(evt: Event): void {
		evt.preventDefault();
		this.props.addModal(<BracketInfoEditDialog bracket={this.props.bracket} />);
	}



@@ 429,7 431,7 @@ class MatchEditDialog extends Component<MatchEditProps, MatchEditState> {
}

interface BracketInfoEditProps {
	bracket: BracketInfo;
	bracket: Pick<BracketInfo, "name" | "description_md" | "id">;
}

interface BracketInfoEditState {


@@ 438,23 440,24 @@ interface BracketInfoEditState {
	submitting: boolean;
}

class BracketInfoEditDialog extends Component<BracketInfoEditProps, BracketInfoEditState> {
export class BracketInfoEditDialog extends Component<BracketInfoEditProps, BracketInfoEditState> {
	private modal = createRef();

	public render(props: BracketInfoEditProps, state: BracketInfoEditState) {
		const { bracket } = props;

		return <Modal title={<Text id="editBracketInfo" />} ref={this.modal}>
			<form onSubmit={this.submit.bind(this)}>
			<form method="POST" action="/formSubmit/bracketInfoEdit" onSubmit={this.submit}>
				<div class="modal-body">
					<input type="hidden" name="bracket" value={bracket.id} />
					<label class="form-control-label">
						<Text id="name" />
						<input type="text" value={typeof state.name === "undefined" ? bracket.name : state.name} onChange={linkState(this, "name")} />
						<input type="text" value={typeof state.name === "undefined" ? bracket.name : state.name} onChange={linkState(this, "name")} name="bracketName" />
					</label>

					<label class="form-control-label">
						<Text id="description" />
						<textarea value={typeof state.description === "undefined" ? bracket.description_md : state.description} onChange={linkState(this, "description")} />
						<textarea value={typeof state.description === "undefined" ? bracket.description_md : state.description} onChange={linkState(this, "description")} name="description_md" />
					</label>
				</div>
				<div class="modal-footer modal-footer-buttons">


@@ 467,6 470,7 @@ class BracketInfoEditDialog extends Component<BracketInfoEditProps, BracketInfoE
		</Modal>;
	}

	@bind
	private submit(evt: Event): void {
		evt.preventDefault();


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

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

import { BracketInfo, BracketInfoEditDialog } from "../bracket";

interface PageProps {
	bracketID: 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(info: Omit<BracketInfo, "description_html">) {
		return <FakeModalContainer backLink={"/bracket/" + info.id}>
			<BracketInfoEditDialog bracket={info} />
		</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
@@ 5,6 5,7 @@ const routes: Array<{path: string; component: string}> = [
	{path: "/myBrackets", component: "myBrackets"},
	{path: "/register", component: "register"},
	{path: "/login", component: "login"},
	{path: "/dialog/bracketInfoEdit/:bracketID", component: "dialog/bracketInfoEdit"},
];

export default routes;

M rosebush/src/server/index.tsx => rosebush/src/server/index.tsx +37 -21
@@ 156,10 156,9 @@ http.createServer(async (req_, res) => {
			return;
		}
		const rest = req.url.substring(12);

		if(rest === "login") {
			try {
				const fields = querystring.decode(await getStream(req));
		try {
			const fields = querystring.decode(await getStream(req));
			if(rest === "login") {
				const resp = await apiFetchRaw(req, "unstable/loginSessions", {method: "POST", body: JSON.stringify({
					username: fields.username,
					password: fields.password,


@@ 183,16 182,8 @@ http.createServer(async (req_, res) => {
				);
				res.write("Successfully logged in.");
				res.end();
			} catch(e) {
				console.error(e);
				res.writeHead(500);
				res.write("Internal Server Error");
				res.end();
			}
		}
		else if(rest === "newBracket") {
			try {
				const fields = querystring.decode(await getStream(req));
			else if(rest === "newBracket") {
				let newUserToken = null;
				const newHeaders: {[key: string]: string} = {};
				if(fields.newUser) {


@@ 245,16 236,41 @@ http.createServer(async (req_, res) => {
				);
				res.write("Created bracket " + newID);
				res.end();
			} catch(e) {
				console.error(e);
				res.writeHead(500);
				res.write("Internal Server Error");
			}
			else if(rest === "bracketInfoEdit") {
				const resp = await apiFetchRaw(
					req,
					"v1/brackets/" + fields.bracket,
					{
						method: "PATCH",
						body: JSON.stringify({
							name: fields.bracketName,
							description_md: fields.description_md,
						}),
					},
				);

				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");
			else {
				res.writeHead(404);
				res.write("Not Found");
				res.end();
			}
		} catch(e) {
			console.error(e);
			res.writeHead(500);
			res.write("Internal Server Error");
			res.end();
		}
	}