add status
This commit is contained in:
parent
d06f43682a
commit
a45002f693
18 changed files with 233 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,4 +3,5 @@ dist
|
||||||
certs
|
certs
|
||||||
.env.local
|
.env.local
|
||||||
.env.production
|
.env.production
|
||||||
|
.env.staging
|
||||||
stepData.ndjson
|
stepData.ndjson
|
6
Makefile
6
Makefile
|
@ -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
|
|
@ -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
5
lib/utils.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
23
src/App.tsx
23
src/App.tsx
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
7
src/components/Chip.module.scss
Normal file
7
src/components/Chip.module.scss
Normal 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
14
src/components/Chip.tsx
Normal 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>;
|
||||||
|
}
|
16
src/components/NavBar.module.scss
Normal file
16
src/components/NavBar.module.scss
Normal 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
20
src/components/NavBar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
25
src/components/WarningBars.module.scss
Normal file
25
src/components/WarningBars.module.scss
Normal 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;
|
||||||
|
}
|
61
src/components/WarningBars.tsx
Normal file
61
src/components/WarningBars.tsx
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,4 +14,9 @@ body,
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
|
@ -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",
|
||||||
|
});
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
}));
|
||||||
|
|
Loading…
Reference in a new issue