add status

This commit is contained in:
Michael Zhang 2024-05-15 17:29:17 -05:00
parent d06f43682a
commit a45002f693
18 changed files with 233 additions and 16 deletions

1
.gitignore vendored
View file

@ -3,4 +3,5 @@ dist
certs certs
.env.local .env.local
.env.production .env.production
.env.staging
stepData.ndjson stepData.ndjson

View file

@ -1,3 +1,3 @@
deploy: deploy-staging:
pnpm run build --base=/ddr pnpm run build --mode staging --base=/ddr-staging
rsync -azrP dist/ root@veil:/home/blogDeploy/public/ddr rsync -azrP dist/ root@veil:/home/blogDeploy/public/ddr-staging

View file

@ -3,7 +3,7 @@
<head> <head>
<title>DDR Companion</title> <title>DDR Companion</title>
<meta name="description" content="My Awesome App description"> <meta name="description" content="My Awesome App description">
<link rel="icon" href="/ddr/favicon.ico"> <link rel="icon" href="%BASE_URL%/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
</head> </head>

5
lib/utils.ts Normal file
View file

@ -0,0 +1,5 @@
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

View file

@ -7,6 +7,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview",
"check": "tsc", "check": "tsc",
"generate-pwa-assets": "pwa-assets-generator" "generate-pwa-assets": "pwa-assets-generator"
}, },
@ -35,6 +36,8 @@
"@mui/icons-material": "^5.15.17", "@mui/icons-material": "^5.15.17",
"@mui/material": "^5.15.17", "@mui/material": "^5.15.17",
"@tanstack/react-query": "^5.36.1", "@tanstack/react-query": "^5.36.1",
"classnames": "^2.5.1",
"jotai": "^2.8.0",
"ndjson": "^2.0.0", "ndjson": "^2.0.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View file

@ -23,6 +23,12 @@ importers:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.36.1 specifier: ^5.36.1
version: 5.36.1(react@18.3.1) version: 5.36.1(react@18.3.1)
classnames:
specifier: ^2.5.1
version: 2.5.1
jotai:
specifier: ^2.8.0
version: 2.8.0(@types/react@18.3.2)(react@18.3.1)
ndjson: ndjson:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -1607,6 +1613,9 @@ packages:
chownr@1.1.4: chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
clone-response@1.0.3: clone-response@1.0.3:
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
@ -2152,6 +2161,18 @@ packages:
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
hasBin: true hasBin: true
jotai@2.8.0:
resolution: {integrity: sha512-yZNMC36FdLOksOr8qga0yLf14miCJlEThlp5DeFJNnqzm2+ZG7wLcJzoOyij5K6U6Xlc5ljQqPDlJRgqW0Y18g==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=17.0.0'
react: '>=17.0.0'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
js-green-licenses@4.0.0: js-green-licenses@4.0.0:
resolution: {integrity: sha512-kcgTOaZmpDpINcRAOKKhjHtBN6zibMVTC8qfPUOpowQtI/6fUgdmwJLJ0ycCb0pUO3ZYKn++56sy8IlG60p5mg==} resolution: {integrity: sha512-kcgTOaZmpDpINcRAOKKhjHtBN6zibMVTC8qfPUOpowQtI/6fUgdmwJLJ0ycCb0pUO3ZYKn++56sy8IlG60p5mg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -4643,6 +4664,8 @@ snapshots:
chownr@1.1.4: {} chownr@1.1.4: {}
classnames@2.5.1: {}
clone-response@1.0.3: clone-response@1.0.3:
dependencies: dependencies:
mimic-response: 1.0.1 mimic-response: 1.0.1
@ -5233,6 +5256,11 @@ snapshots:
jiti@1.21.0: {} jiti@1.21.0: {}
jotai@2.8.0(@types/react@18.3.2)(react@18.3.1):
optionalDependencies:
'@types/react': 18.3.2
react: 18.3.1
js-green-licenses@4.0.0: js-green-licenses@4.0.0:
dependencies: dependencies:
gaxios: 5.1.3 gaxios: 5.1.3

View file

@ -1,6 +1,7 @@
.container { .container {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -8,4 +9,7 @@
.content { .content {
flex-grow: 1; flex-grow: 1;
min-height: 0;
overflow-y: scroll;
} }

View file

@ -1,7 +1,3 @@
import { BottomNavigation, BottomNavigationAction, Icon } from "@mui/material";
import LineWeightIcon from "@mui/icons-material/LineWeight";
import ManageSearchIcon from "@mui/icons-material/ManageSearch";
import SettingsIcon from "@mui/icons-material/Settings";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
createBrowserRouter, createBrowserRouter,
@ -20,7 +16,10 @@ import Scores from "./pages/Scores";
import Settings from "./pages/Settings"; import Settings from "./pages/Settings";
import ImportChartWorker from "./importCharts?worker"; import ImportChartWorker from "./importCharts?worker";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { CHART_STORE_CREATION_EVENT } from "./globals"; import { CHART_STORE_CREATION_EVENT, dbStatusAtom } from "./globals";
import NavBar from "./components/NavBar";
import WarningBars from "./components/WarningBars";
import { useAtom, useSetAtom } from "jotai";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -55,10 +54,12 @@ export function AppWrapper() {
return ( return (
<> <>
<div className={styles.container}> <div className={styles.container}>
<WarningBars />
<div className={styles.content}> <div className={styles.content}>
<Outlet /> <Outlet />
</div> </div>
<BottomNavigation <NavBar />
{/* <BottomNavigation
showLabels showLabels
value={navId} value={navId}
onChange={(event, newValue) => { onChange={(event, newValue) => {
@ -68,17 +69,25 @@ export function AppWrapper() {
<BottomNavigationAction label="Charts" icon={<ManageSearchIcon />} /> <BottomNavigationAction label="Charts" icon={<ManageSearchIcon />} />
<BottomNavigationAction label="Scores" icon={<LineWeightIcon />} /> <BottomNavigationAction label="Scores" icon={<LineWeightIcon />} />
<BottomNavigationAction label="Settings" icon={<SettingsIcon />} /> <BottomNavigationAction label="Settings" icon={<SettingsIcon />} />
</BottomNavigation> </BottomNavigation> */}
</div> </div>
</> </>
); );
} }
export default function App() { export default function App() {
const setDbStatus = useSetAtom(dbStatusAtom);
useEffect(() => { useEffect(() => {
const worker = new ImportChartWorker(); const worker = new ImportChartWorker();
worker.postMessage("start");
worker.onmessage = (evt) => { worker.onmessage = (evt) => {
console.log("got event from web worker", evt); console.log("got event from web worker", evt);
switch (evt.kind) {
case "dbIsBlocked": {
setDbStatus((db) => ({ ...db, status: "upgrading" }));
}
}
if (evt.kind === "chartStoreCreate") { if (evt.kind === "chartStoreCreate") {
document.dispatchEvent(CHART_STORE_CREATION_EVENT); document.dispatchEvent(CHART_STORE_CREATION_EVENT);
} }

View file

@ -0,0 +1,7 @@
.chip {
display: flex;
border-radius: 1000px;
border: 1px solid lightgray;
padding: 2px 6px;
font-size: 10pt;
}

14
src/components/Chip.tsx Normal file
View file

@ -0,0 +1,14 @@
import classNames from "classnames";
import styles from "./Chip.module.scss";
import type { PropsWithChildren } from "react";
export interface ChipProps {
className?: string | undefined;
}
export default function Chip({
className,
children,
}: PropsWithChildren<ChipProps>) {
return <div className={classNames(className, styles.chip)}>{children}</div>;
}

View file

@ -0,0 +1,16 @@
.bar {
display: flex;
border-top: 1px solid lightgray;
}
.searchBar {
flex-grow: 1;
display: flex;
>input {
flex-grow: 1;
border: none;
outline: none;
}
}

20
src/components/NavBar.tsx Normal file
View file

@ -0,0 +1,20 @@
import styles from "./NavBar.module.scss";
import SettingsIcon from "@mui/icons-material/Settings";
import HelpIcon from "@mui/icons-material/Help";
export default function NavBar() {
return (
<nav className={styles.bar}>
<div className={styles.searchBar}>
<input
placeholder="Search charts..."
// biome-ignore lint/a11y/noAutofocus: this is the only one in the page
autoFocus
/>
</div>
<SettingsIcon />
<HelpIcon />
</nav>
);
}

View file

@ -0,0 +1,25 @@
.container {
display: flex;
flex-direction: column;
}
.bar {
display: flex;
align-items: center;
border-bottom: 1px solid lightgray;
font-size: 10pt;
height: 24px;
padding: 2px 8px;
}
.modeBar {
--bar1: rgb(237, 224, 40);
--bar2: rgb(240, 232, 118);
background: repeating-linear-gradient(45deg,
var(--bar1),
var(--bar1) 10px,
var(--bar2) 10px,
var(--bar2) 20px);
backdrop-filter: blur(10px);
color: black;
}

View file

@ -0,0 +1,61 @@
import classNames from "classnames";
import styles from "./WarningBars.module.scss";
import WarningIcon from "@mui/icons-material/Warning";
import Chip from "./Chip";
import { dbStatusAtom } from "../globals";
import { useAtom, useAtomValue } from "jotai";
import { ReactNode } from "react";
export default function WarningBars() {
return (
<div className={styles.container}>
<ModeBar />
<StatusBar />
</div>
);
}
/** Warn if this is not the production mode */
function ModeBar() {
const mode = import.meta.env.MODE;
if (mode === "production") return null;
return (
<div className={classNames(styles.bar, styles.modeBar)}>
<span>
<WarningIcon fontSize="small" />
This is the <b>{mode}</b> site. Click{" "}
<a
href="https://mzhang.io/ddr"
target="_blank"
rel="noreferrer noopener"
>
here
</a>{" "}
to go to the production site.
</span>
</div>
);
}
function StatusBar() {
const statuses = [];
statuses.push(
<Chip>
db: <DbStatusChip />
</Chip>,
);
return <div className={classNames(styles.bar)}>{statuses}</div>;
}
function DbStatusChip(): ReactNode {
const dbStatus = useAtomValue(dbStatusAtom);
switch (dbStatus.status) {
case "uninit":
return "uninit";
case "upgrading":
return "upgrading";
}
}

View file

@ -14,4 +14,9 @@ body,
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
min-height: 0;
}
.spacer {
flex-grow: 1;
} }

View file

@ -1,4 +1,17 @@
// export const APP_DATA_VERSION = 1; import { atom } from "jotai";
const PROD_DATA_VERSION = 1;
const DEV_DATA_VERSION = Math.floor(new Date().getTime() / 1000) % 1000000;
export const APP_DATA_VERSION = export const APP_DATA_VERSION =
Math.floor(new Date().getTime() / 1000) % 1000000; import.meta.env.MODE === "production" ? PROD_DATA_VERSION : DEV_DATA_VERSION;
export const CHART_STORE_CREATION_EVENT = new Event("chartStoreCreate"); export const CHART_STORE_CREATION_EVENT = new Event("chartStoreCreate");
export interface DbStatus {
persistence: "unknown" | "persisted" | "unpersisted";
status: "uninit" | "upgrading";
}
export const dbStatusAtom = atom<DbStatus>({
persistence: "unknown",
status: "uninit",
});

View file

@ -91,6 +91,7 @@ function openDb() {
db.addEventListener("blocked", (evt) => { db.addEventListener("blocked", (evt) => {
console.log("BLOCKED", evt); console.log("BLOCKED", evt);
self.postMessage({ kind: "dbIsBlocked" });
}); });
db.addEventListener("upgradeneeded", (evt) => { db.addEventListener("upgradeneeded", (evt) => {

View file

@ -3,8 +3,13 @@ import react from "@vitejs/plugin-react-swc";
import { VitePWA } from "vite-plugin-pwa"; import { VitePWA } from "vite-plugin-pwa";
import basicSsl from "@vitejs/plugin-basic-ssl"; import basicSsl from "@vitejs/plugin-basic-ssl";
export default defineConfig({ const baseUrls = {
base: process.env.BASE_URL ?? undefined, production: "/ddr",
staging: "/ddr-staging",
};
export default defineConfig(({ mode }) => ({
base: baseUrls[mode],
server: { server: {
https: {}, https: {},
}, },
@ -26,4 +31,4 @@ export default defineConfig({
certDir: "/Users/.../.devServer/cert", certDir: "/Users/.../.devServer/cert",
}), }),
], ],
}); }));