From 6e745642283bbb7dd4c44aedfd9a8c47b222adc3 Mon Sep 17 00:00:00 2001 From: Shuji Koike Date: Sun, 23 Jan 2022 06:29:51 +0900 Subject: [PATCH] fixup! [ci skip] WIP WIP WIP WIP WIP --- packages/app/app.tsx | 4 +- .../components/{auth.tsx => AuthAvatar.tsx} | 2 +- packages/app/components/layout.tsx | 4 +- packages/app/demo/DemoFilePicker.tsx | 36 +++++++++++++++ packages/app/demo/FilePicker.tsx | 39 ---------------- packages/app/demo/io.ts | 46 ++++++++----------- packages/app/hooks/useLocationState.ts | 11 +++++ packages/app/pages/DemoList.tsx | 4 +- packages/app/pages/DemoPage.tsx | 13 ++++-- packages/app/pages/Home.tsx | 20 ++++++-- 10 files changed, 100 insertions(+), 79 deletions(-) rename packages/app/components/{auth.tsx => AuthAvatar.tsx} (97%) create mode 100644 packages/app/demo/DemoFilePicker.tsx delete mode 100644 packages/app/demo/FilePicker.tsx create mode 100644 packages/app/hooks/useLocationState.ts diff --git a/packages/app/app.tsx b/packages/app/app.tsx index 5d609096..10c5558c 100644 --- a/packages/app/app.tsx +++ b/packages/app/app.tsx @@ -42,9 +42,9 @@ const routes = ( ) -interface AppState { +export interface AppState extends Record { match?: Match | null - setMatch: (match: Match | null | undefined) => void + setMatch: React.Dispatch> } export const AppContext = React.createContext({ diff --git a/packages/app/components/auth.tsx b/packages/app/components/AuthAvatar.tsx similarity index 97% rename from packages/app/components/auth.tsx rename to packages/app/components/AuthAvatar.tsx index 3620e6df..6f4f3cff 100644 --- a/packages/app/components/auth.tsx +++ b/packages/app/components/AuthAvatar.tsx @@ -10,7 +10,7 @@ import React from "react" import { useAuth } from "../hooks" -export const AuthButton: React.VFC<{ +export const AuthAvatar: React.VFC<{ diameter?: number }> = ({ diameter = 32 }) => { const user = useAuth() diff --git a/packages/app/components/layout.tsx b/packages/app/components/layout.tsx index de5cb63e..aca29a74 100644 --- a/packages/app/components/layout.tsx +++ b/packages/app/components/layout.tsx @@ -13,7 +13,7 @@ import React from "react" import ReactDOM from "react-dom" import { useLocation, NavLink, Link } from "react-router-dom" -import { AuthButton } from "./auth" +import { AuthAvatar } from "./AuthAvatar" interface LayoutState { hideHeader: boolean @@ -72,7 +72,7 @@ export const Layout: React.VFC<{ alignItems="center" margin="0 32px" /> - + void + onLoad?: (match: Match, name: string) => void +}> = ({ setMatch, onLoad }) => { + const [output, setOutput] = React.useState([]) + const [files, setFiles] = React.useState() + React.useEffect(() => { + const file = files && files[0] + if (!file) return + if (!isValidFile(file)) return console.error("invalid file") + openDemo(file, setOutput, setMatch).then((match) => { + setMatch?.(match) + if (match) onLoad?.(match, file.name) + }) + }, [files]) + return ( + <> + setFiles([...(e.currentTarget.files || [])])} + /> + {output.length > 0 && ( +
+          

Converting DEM file `{files?.[0]?.name}`

+ {output} +
+ )} + + ) +} diff --git a/packages/app/demo/FilePicker.tsx b/packages/app/demo/FilePicker.tsx deleted file mode 100644 index 01978a2f..00000000 --- a/packages/app/demo/FilePicker.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react" - -import { AppContext } from "../app" -import { openDemo } from "./io" - -export const FilePicker: React.VFC<{ - onLoad?: (match: Match, name: string) => void -}> = ({ onLoad }) => { - const { setMatch } = React.useContext(AppContext) - const [output, setOutput] = React.useState([]) - const [files, setFiles] = React.useState() - const [file, setFile] = React.useState() - React.useEffect(() => { - if (files && files[0]) setFile(files[0]) - }, [files]) - React.useEffect(() => { - if (file) - openDemo(file, setOutput, setMatch).then((match) => { - setMatch(match) - if (match) onLoad?.(match, file.name) - }) - }, [file]) - return ( - <> - 0} - onChange={(e) => setFiles([...(e.currentTarget.files || [])])} - /> - {output.length > 0 && ( -
-          

Converting DEM file `{files?.[0]?.name}`

- {output.join("\n")} -
- )} - - ) -} diff --git a/packages/app/demo/io.ts b/packages/app/demo/io.ts index 9f94e696..f70a4b40 100644 --- a/packages/app/demo/io.ts +++ b/packages/app/demo/io.ts @@ -18,11 +18,11 @@ export async function openDemo( onOutput?: (arr: string[]) => void, onRoundEnd?: (match: Match) => void ): Promise { + if (!file) return null if (typeof file === "string") { - if (/^(public|private|sandbox)/.test(file)) { - return await storageFetch(file) - } - return fetch(file).then(parseJson) + if (/^(public|private|sandbox)/.test(file)) + return getDownloadURL(ref(getStorage(), file)).then(fetch).then(parseJson) + return parseJson(await fetch(file)) } if (file instanceof Response) return parseJson(file) if (file instanceof File && file.name.endsWith(".json")) @@ -31,8 +31,9 @@ export async function openDemo( return parseJson(file) if (file instanceof File && file.name.endsWith(".dem")) return parseDemo(file, onOutput, onRoundEnd) - if (file) console.warn("openDemo", "unsupported file type!") - return null + if (file instanceof File) throw new Error("unsupported file type!") + const never: never = file + throw new Error(never) } async function parseJson( @@ -51,8 +52,7 @@ async function parseJson( if (data.name.endsWith(".gz")) return data.arrayBuffer().then(parseJson) else return data.text().then(parseJson) const never: never = data - console.error(never) - throw new Error() + throw new Error(never) } export function parseDemo( @@ -89,31 +89,29 @@ export async function storagePut( path: string, data: string | Match, compress = true -): Promise { +): Promise { const json = typeof data === "string" ? data : JSON.stringify(data) - if (compress || /\.gz$/.test(path)) { + if (compress || /\.gz$/i.test(path)) { await uploadBytes(ref(getStorage(), path), await gzip(json)) } else { await uploadString(ref(getStorage(), path), json) } + return path } -export async function storagePutPublicMatch(match: Match, file: File | string) { - await storagePut( - `public/${new Date().getTime()}-${toName(file)}.json.gz`, - match - ) +export async function storagePutPublicMatch( + match: Match, + file: File | string +): Promise { + const path = `public/${new Date().getTime()}-${toName(file)}.json.gz` + return await storagePut(path, match) } function toName(file: File | string) { return encodeURIComponent(typeof file === "string" ? file : file.name) } -export function storageFetch(path: string): Promise { - return getDownloadURL(ref(getStorage(), path)).then(fetch).then(parseJson) -} - -export function fileTypeFilter(file: unknown): boolean { +export function isValidFile(file: unknown): file is File { if (file instanceof File) return /\.(dem|json)(\.gz)?$/i.test(file.name) return false } @@ -125,14 +123,10 @@ export function setStorage(match: Match | null): Match | null { export async function gzip(input: string): Promise { await initGzip(await fetch(gzipWasm)) - const output = compressStringGzip(input) - if (!output) throw new Error() - return output + return compressStringGzip(input) ?? Promise.reject() } export async function gunzip(input: Uint8Array): Promise { await initGzip(await fetch(gzipWasm)) - const output = decompressGzip(input) - if (!output) throw new Error() - return output + return decompressGzip(input) ?? Promise.reject() } diff --git a/packages/app/hooks/useLocationState.ts b/packages/app/hooks/useLocationState.ts new file mode 100644 index 00000000..8c2931d0 --- /dev/null +++ b/packages/app/hooks/useLocationState.ts @@ -0,0 +1,11 @@ +import { useLocation } from "react-router" + +export function useLocationState>( + validate?: (x: unknown) => x is T +): T { + const { state } = useLocation() + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + if (!validate) return (state ?? {}) as T + if (validate?.(state)) return state + throw new Error() +} diff --git a/packages/app/pages/DemoList.tsx b/packages/app/pages/DemoList.tsx index 792e7213..7476aed3 100644 --- a/packages/app/pages/DemoList.tsx +++ b/packages/app/pages/DemoList.tsx @@ -51,7 +51,9 @@ const DemoItem: React.VFC<{ nodeId={nodeId} icon={ } onClick={() => navigate(`/dem/${file}`)} diff --git a/packages/app/pages/DemoPage.tsx b/packages/app/pages/DemoPage.tsx index 187faba2..0b394573 100644 --- a/packages/app/pages/DemoPage.tsx +++ b/packages/app/pages/DemoPage.tsx @@ -2,15 +2,20 @@ import React from "react" import { useParams } from "react-router-dom" import sample from "../../../static/sample.dem.json?url" -import { AppContext } from "../app" +import { AppContext, AppState } from "../app" import { MatchView } from "../demo/MatchView" import { openDemo } from "../demo/io" +import { useLocationState } from "../hooks/useLocationState" export const DemoPage: React.VFC<{ path?: string }> = ({ path }) => { + const state = useLocationState() const { "*": paramPath } = useParams<"*">() const { match, setMatch } = React.useContext(AppContext) React.useEffect(() => { - openDemo(paramPath === "sample" ? sample : path || paramPath).then(setMatch) - }, [path]) - return + if (!state.match) + Promise.resolve(paramPath === "sample" ? sample : path || paramPath) + .then(openDemo) + .then(setMatch) + }, [path, paramPath]) + return } diff --git a/packages/app/pages/Home.tsx b/packages/app/pages/Home.tsx index ccee7c13..055c2dae 100644 --- a/packages/app/pages/Home.tsx +++ b/packages/app/pages/Home.tsx @@ -2,19 +2,23 @@ import { Alert } from "@mui/lab" import { getAnalytics, logEvent } from "firebase/analytics" import React from "react" import { isChrome } from "react-device-detect" -import { useNavigate } from "react-router" +import { useLocation, useNavigate } from "react-router" import { AppContext } from "../app" -import { FilePicker } from "../demo/FilePicker" +import { DemoFilePicker } from "../demo/DemoFilePicker" import { MatchView } from "../demo/MatchView" import { storagePutPublicMatch } from "../demo/io" export const Home: React.VFC = () => { + const location = useLocation() const navigate = useNavigate() - const { match } = React.useContext(AppContext) + const { match, setMatch } = React.useContext(AppContext) React.useEffect(() => { if (match) logEvent(getAnalytics(), "view_item") }, [match]) + React.useEffect(() => { + if (match) setMatch(undefined) + }, [location.pathname]) return match ? ( ) : ( @@ -23,7 +27,15 @@ export const Home: React.VFC = () => { Only Google Chrome is supported! )}

Click the button below and select a DEM file.

- + { + const path = /\.dem$/i.test(name) + ? await storagePutPublicMatch(match, name) + : name + navigate(`/dem/${path}`, { state: { match } }) + }} + /> {import.meta.env.DEV && (