~vpzom/bracketmonster

92ed707c63cbd722b6b194b2dedc42747a20a2c5 — Colin Reeder 6 months ago 9748e12 ssr
Preload login state
M rosebush/package-lock.json => rosebush/package-lock.json +22 -7
@@ 831,6 831,14 @@
      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.17.tgz",
      "integrity": "sha512-Is+l3mcHvs47sKy+afn2O1rV4ldZFU7W8101cNlOd+MRbjM4Onida8jSZnJdTe/0Pcf25g9BNIUsuugmE6puHA=="
    },
    "@types/node-fetch": {
      "version": "2.5.4",
      "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz",
      "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==",
      "requires": {
        "@types/node": "*"
      }
    },
    "@types/qrcode": {
      "version": "1.3.4",
      "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.3.4.tgz",


@@ 3278,6 3286,17 @@
      "requires": {
        "node-fetch": "^1.0.1",
        "whatwg-fetch": ">=0.10.0"
      },
      "dependencies": {
        "node-fetch": {
          "version": "1.7.3",
          "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
          "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
          "requires": {
            "encoding": "^0.1.11",
            "is-stream": "^1.0.1"
          }
        }
      }
    },
    "js-levenshtein": {


@@ 3647,13 3666,9 @@
      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
    },
    "node-fetch": {
      "version": "1.7.3",
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
      "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
      "requires": {
        "encoding": "^0.1.11",
        "is-stream": "^1.0.1"
      }
      "version": "2.6.0",
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
      "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
    },
    "node-libs-browser": {
      "version": "2.2.1",

M rosebush/package.json => rosebush/package.json +2 -0
@@ 7,6 7,7 @@
    "@types/http-proxy": "^1.17.2",
    "@types/markdown-it": "0.0.9",
    "@types/node": "^12.12.17",
    "@types/node-fetch": "^2.5.4",
    "@types/qrcode": "^1.3.4",
    "@types/serve-static": "^1.13.3",
    "assets-webpack-plugin": "^3.9.10",


@@ 18,6 19,7 @@
    "linkstate": "^1.1.1",
    "markdown-it": "^10.0.0",
    "mini-css-extract-plugin": "^0.9.0",
    "node-fetch": "^2.6.0",
    "preact": "^10.1.0",
    "preact-feather": "^4.1.0",
    "preact-markup": "^2.0.0",

M rosebush/src/client/App.tsx => rosebush/src/client/App.tsx +6 -4
@@ 19,7 19,7 @@ export interface AppContextContent {
	setUserInfo(userInfo: Readonly<UserInfo>): void;
}

interface UserInfo {
export interface UserInfo {
	id: number;
	username: string | null;
}


@@ 29,6 29,7 @@ function NotFoundPage() {
}

interface Props {
	initUserInfo?: UserInfo | null;
	url?: string;
}



@@ 37,10 38,11 @@ interface State {
}

export default class App extends Component<Props, State> {
	public constructor() {
		super();
	public constructor(props: Props) {
		super(props);

		this.state = {
			userInfo: {state: "loading"},
			userInfo: typeof props.initUserInfo === "undefined" ? {state: "loading"} : {state: "done", value: props.initUserInfo},
		};
	}


M rosebush/src/client/Helmet.tsx => rosebush/src/client/Helmet.tsx +30 -17
@@ 9,7 9,7 @@
 *
 */

import { Component, ComponentChildren, JSX, h, toChildArray } from "preact";
import { Component, ComponentChildren, Fragment, JSX, h, toChildArray } from "preact";

let refs: Helmet[] = [];



@@ 18,20 18,27 @@ export const rewind = () => {
	return res
}

export function rewindClear(): HelmetProps {
	const result = rewind();
	refs = [];

	return result;
}

const Wrapper = (props: {children: ComponentChildren}) => {
	const children = toChildArray(props.children);
	if (typeof window !== 'undefined') {
		const titleChild = children.find(child => typeof child === "object" && child.type === 'title') as (JSX.Element | undefined);
		console.log("title", children);
		if (titleChild) {
			const title = titleChild.props.children
			if (title !== document.title) {
				document.title = title
			}
		}
		return null
	} else {
		return <div>{children}</div>
	}

	return null
}

interface HelmetProps {


@@ 46,23 53,19 @@ interface HelmetProps {
}

export default class Helmet extends Component<HelmetProps, {}> {
	constructor() {
		super();
		refs.push(this)
	}

	componentWillUnmount() {
		const refsWithout = refs.filter(c => c !== this)
		refs = refsWithout
		document.title = this._getTitle({})
	public static renderContent(props: HelmetProps) {
		return [
			<title data-helmet='true'>{Helmet._getTitle(props)}</title>,
			...Helmet._getMeta(props),
		];
	}

	_getTitle(props: HelmetProps) {
	private static _getTitle(props: HelmetProps) {
		const { title, titleTemplate = '%s', fallbackTitle } = { ...rewind(), ...props }
		return (!title && fallbackTitle) ? fallbackTitle : titleTemplate.replace('%s', title || '');
	}

	_getMeta({meta = []}: {meta?: HelmetProps["meta"]}) {
	private static _getMeta({meta = []}: {meta?: HelmetProps["meta"]}) {
		return meta.map(({ name, property, content }) =>
			<meta
				key={name}


@@ 74,11 77,21 @@ export default class Helmet extends Component<HelmetProps, {}> {
					)
	}

	constructor() {
		super();
		refs.push(this)
	}

	componentWillUnmount() {
		const refsWithout = refs.filter(c => c !== this)
		refs = refsWithout
		document.title = Helmet._getTitle({})
	}

	render() {
		return (
			<Wrapper>
				<title data-helmet='true'>{this._getTitle(this.props)}</title>
				{this._getMeta(this.props)}
				{Helmet.renderContent(this.props)}
			</Wrapper>
		)
	}

M rosebush/src/client/index.tsx => rosebush/src/client/index.tsx +4 -2
@@ 1,7 1,7 @@
import { h, render } from "preact";
import "preact/debug";

import App from "./App";
import App, { UserInfo } from "./App";

import "./main.scss";



@@ 9,7 9,9 @@ const root = document.getElementById("root")!;

console.log(root);

declare const INIT_USER_INFO: UserInfo | null;

render(
	<App />,
	<App initUserInfo={INIT_USER_INFO} />,
	root,
);

M rosebush/src/server/index.tsx => rosebush/src/server/index.tsx +33 -3
@@ 2,11 2,13 @@ import * as fs from "fs";
import * as http from "http";
import * as httpProxy from "http-proxy";
import isomorphicCookie = require("isomorphic-cookie");
import fetch from "node-fetch";
import { h } from "preact";
import { render } from "preact-render-to-string";
import * as serveStatic from "serve-static";

import App from "../client/App";
import Helmet, { rewindClear } from "../client/Helmet";

const API_HOST = process.env.API_HOST || "http://localhost:5000";
const SOCKET_HOST = process.env.SOCKET_HOST || "ws://localhost:5555";


@@ 24,7 26,26 @@ apiServer.on("proxyReq", function(proxyReq, req, res, options) {
	}
});

http.createServer((req_, res) => {
function apiFetch(srcReq: http.IncomingMessage, url: string) {
	const token = isomorphicCookie.load("rosebushToken", srcReq);

	const headers: {[key: string]: string} = {};

	if(token) {
		headers["Authorization"] = "Bearer " + token;
	}

	return fetch(API_HOST + "/" + url, {headers})
		.then(res => {
			if(res.status < 200 || res.status >= 300) {
				return res.text()
					.then(text => Promise.reject(new Error(text)));
			}
			return res;
		});
}

http.createServer(async (req_, res) => {
	const req = req_ as (typeof req_ & {url: string});
	if(req.url.startsWith("/api/")) {
		req.url = req.url.substring(4);


@@ 53,18 74,27 @@ http.createServer((req_, res) => {
		});
	}
	else {
		const app = <App url={req.url} />;
		const userInfo = isomorphicCookie.load("rosebushToken", req) ?
			await apiFetch(req, "unstable/users/~me")
				.then(res => res.json()) :
			null;

		const app = <App url={req.url} initUserInfo={userInfo} />;

		const content = render(app, {}, {pretty: true});
		console.log(content);
		const helmetInfo = rewindClear();

		const headContent = render(Helmet.renderContent(helmetInfo), {}, {pretty: true});

		res.writeHead(200, {"Content-type": "text/html"});
		res.write(`<!DOCTYPE html>
<html>
	<head>
		<meta name="viewport" content="width=device-width, initial-scale=1">
		${headContent}
		<script type="text/javascript">
			var SOCKET_HOST = ${JSON.stringify(SOCKET_HOST)};
			var INIT_USER_INFO = ${JSON.stringify(userInfo)};
		</script>
		<link rel="stylesheet" href="${assets.main.css}">
	</head>