mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
154 lines
3.7 KiB
TypeScript
154 lines
3.7 KiB
TypeScript
import {
|
|
createContext,
|
|
lazy,
|
|
Suspense,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { checksum } from "bknd/utils";
|
|
import { App, registries, sqlocal, type BkndConfig } from "bknd";
|
|
import { Route, Router, Switch } from "wouter";
|
|
import { ClientProvider } from "bknd/client";
|
|
import { SQLocalKysely } from "sqlocal/kysely";
|
|
import type { ClientConfig, DatabasePath } from "sqlocal";
|
|
import { OpfsStorageAdapter } from "bknd/adapter/browser";
|
|
import type { BkndAdminConfig } from "bknd/ui";
|
|
|
|
const Admin = lazy(() =>
|
|
Promise.all([
|
|
import("bknd/ui"),
|
|
// @ts-ignore
|
|
import("bknd/dist/styles.css"),
|
|
]).then(([mod]) => ({
|
|
default: mod.Admin,
|
|
})),
|
|
);
|
|
|
|
function safeViewTransition(fn: () => void) {
|
|
if (document.startViewTransition) {
|
|
document.startViewTransition(fn);
|
|
} else {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
export type BrowserBkndConfig<Args = ImportMetaEnv> = Omit<
|
|
BkndConfig<Args>,
|
|
"connection" | "app"
|
|
> & {
|
|
adminConfig?: BkndAdminConfig;
|
|
connection?: ClientConfig | DatabasePath;
|
|
};
|
|
|
|
export type BkndBrowserAppProps = {
|
|
children: ReactNode;
|
|
header?: ReactNode;
|
|
loading?: ReactNode;
|
|
notFound?: ReactNode;
|
|
} & BrowserBkndConfig;
|
|
|
|
const BkndBrowserAppContext = createContext<{
|
|
app: App;
|
|
hash: string;
|
|
}>(undefined!);
|
|
|
|
export function BkndBrowserApp({
|
|
children,
|
|
adminConfig,
|
|
header,
|
|
loading,
|
|
notFound,
|
|
...config
|
|
}: BkndBrowserAppProps) {
|
|
const [app, setApp] = useState<App | undefined>(undefined);
|
|
const [hash, setHash] = useState<string>("");
|
|
const adminRoutePath = (adminConfig?.basepath ?? "") + "/*?";
|
|
|
|
async function onBuilt(app: App) {
|
|
safeViewTransition(async () => {
|
|
setApp(app);
|
|
setHash(await checksum(app.toJSON()));
|
|
});
|
|
}
|
|
|
|
useEffect(() => {
|
|
setup({ ...config, adminConfig })
|
|
.then((app) => onBuilt(app as any))
|
|
.catch(console.error);
|
|
}, []);
|
|
|
|
if (!app) {
|
|
return (
|
|
loading ?? (
|
|
<Center>
|
|
<span style={{ opacity: 0.2 }}>Loading...</span>
|
|
</Center>
|
|
)
|
|
);
|
|
}
|
|
|
|
return (
|
|
<BkndBrowserAppContext.Provider value={{ app, hash }}>
|
|
<ClientProvider storage={window.localStorage} fetcher={app.server.request}>
|
|
{header}
|
|
<Router key={hash}>
|
|
<Switch>
|
|
{children}
|
|
|
|
<Route path={adminRoutePath}>
|
|
<Suspense>
|
|
<Admin config={adminConfig} />
|
|
</Suspense>
|
|
</Route>
|
|
<Route path="*">
|
|
{notFound ?? (
|
|
<Center style={{ fontSize: "48px", fontFamily: "monospace" }}>404</Center>
|
|
)}
|
|
</Route>
|
|
</Switch>
|
|
</Router>
|
|
</ClientProvider>
|
|
</BkndBrowserAppContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useApp() {
|
|
return useContext(BkndBrowserAppContext);
|
|
}
|
|
|
|
const Center = (props: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div
|
|
{...props}
|
|
style={{
|
|
width: "100%",
|
|
minHeight: "100vh",
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
...(props.style ?? {}),
|
|
}}
|
|
/>
|
|
);
|
|
|
|
let initialized = false;
|
|
async function setup(config: BrowserBkndConfig = {}) {
|
|
if (initialized) return;
|
|
initialized = true;
|
|
|
|
registries.media.register("opfs", OpfsStorageAdapter);
|
|
|
|
const app = App.create({
|
|
...config,
|
|
// @ts-ignore
|
|
connection: sqlocal(new SQLocalKysely(config.connection ?? ":localStorage:")),
|
|
});
|
|
|
|
await config.beforeBuild?.(app);
|
|
await app.build({ sync: true });
|
|
await config.onBuilt?.(app);
|
|
|
|
return app;
|
|
}
|