Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e595493

Browse files
committed
feat(play.loradb.com): scaffold Next.js playground app
1 parent 9cd852f commit e595493

109 files changed

Lines changed: 14045 additions & 61 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/play.loradb.com/.eslintrc.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = { extends: ["next/core-web-vitals", "next/typescript"] };

apps/play.loradb.com/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Next.js build output
2+
.next/
3+
out/
4+
5+
# TypeScript
6+
*.tsbuildinfo
7+
8+
# Dependencies (usually hoisted in workspaces)
9+
node_modules/
10+
11+
# Local env files
12+
.env*.local

apps/play.loradb.com/README.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# LoraDB Playground
2+
3+
In-browser IDE for the LoraDB graph database. Runs entirely client-side —
4+
the WASM engine, the Cypher editor, the graph canvas, and the result grid
5+
all live in your browser tab. No backend, no server actions, no `/api`
6+
routes. Data persists locally via IndexedDB and `localStorage`.
7+
8+
## Development
9+
10+
```bash
11+
yarn workspace @loradb/play dev
12+
```
13+
14+
Open <http://localhost:3000>.
15+
16+
Other useful scripts:
17+
18+
```bash
19+
yarn workspace @loradb/play typecheck # strict tsc, --noEmit
20+
yarn workspace @loradb/play lint # next lint
21+
yarn workspace @loradb/play build # static export → apps/play.loradb.com/out
22+
```
23+
24+
If you change any of the workspace dependencies
25+
(`@loradb/lora-wasm`, `@loradb/lora-query`, `@loradb/lora-graph-canvas`),
26+
rebuild them first so the Next bundler picks up fresh artefacts:
27+
28+
```bash
29+
yarn workspace @loradb/lora-wasm build
30+
yarn workspace @loradb/lora-query build
31+
yarn workspace @loradb/lora-graph-canvas build
32+
```
33+
34+
## Architecture summary
35+
36+
- `app/` — Next 15 App Router shell, Mantine providers, root layout, and
37+
the playground page.
38+
- `app/_components/` — Dockview panel layout, query editor wrapper,
39+
graph canvas wrapper, result grid, etc.
40+
- `lib/` — client-only utilities (history, saved-queries, settings,
41+
snapshot import/export, worker plumbing for `@loradb/lora-wasm`).
42+
- `shims/empty.mjs` — webpack alias target used by `next.config.mjs` to
43+
drop `loader-node` from the client bundle (see config for rationale).
44+
- `public/` — static assets served verbatim, plus `_headers` and
45+
`_redirects` consumed by Cloudflare Pages at deploy time.
46+
47+
The build is a fully static export: `yarn build` writes
48+
`apps/play.loradb.com/out/` with self-contained HTML, JS, CSS, and WASM
49+
assets that any object store / CDN can serve as flat files.
50+
51+
## Production deploy
52+
53+
### Recommended host: Cloudflare Pages
54+
55+
The Docusaurus site at `apps/loradb.com` already occupies the single
56+
GitHub Pages site allowed per repo, so the playground deploys to
57+
Cloudflare Pages instead. The `.github/workflows/play-loradb.yml`
58+
workflow builds the static export on every push to `main` that touches
59+
the app or its workspace deps and uploads the result to Cloudflare via
60+
`cloudflare/wrangler-action@v3`.
61+
62+
One-time setup:
63+
64+
1. **Create the Pages project.** In the Cloudflare dashboard go to
65+
_Workers & Pages → Create → Pages → Create using Direct Upload_ and
66+
name it `play-loradb`. Do not connect a Git source — this repo
67+
deploys via GitHub Actions, not Cloudflare's built-in builder.
68+
(If you _do_ connect to Git, disable auto-builds; otherwise CF will
69+
try to run `next build` itself and fight the workflow.)
70+
2. **Attach the custom domain.** In the project page open
71+
_Custom domains → Set up a custom domain_ and enter
72+
`play.loradb.com`. Cloudflare prints a CNAME target like
73+
`play-loradb.pages.dev`. Add a `CNAME` record at your DNS provider
74+
for `play.loradb.com` pointing at that target, then wait for the
75+
custom-domain status in the dashboard to flip to "Active". TLS is
76+
issued automatically.
77+
3. **Provision repo secrets.** In GitHub go to _Settings → Secrets and
78+
variables → Actions → New repository secret_ and add:
79+
- `CLOUDFLARE_API_TOKEN` — create at
80+
<https://dash.cloudflare.com/profile/api-tokens> with the
81+
_Pages — Edit_ permission scope (Account → Cloudflare Pages →
82+
Edit). No other permissions are needed.
83+
- `CLOUDFLARE_ACCOUNT_ID` — visible in the right sidebar of any
84+
Cloudflare dashboard page.
85+
The deploy job runs a pre-flight check that fails with a clear
86+
`::error::` message if either secret is missing.
87+
4. **Trigger the first deploy.** Either push a change under
88+
`apps/play.loradb.com/` to `main`, or run the workflow manually:
89+
```bash
90+
gh workflow run play-loradb
91+
```
92+
Once it completes the site is live at <https://play.loradb.com>.
93+
94+
`wrangler.toml` at the app root pins `pages_build_output_dir = "out"`
95+
and the project name `play-loradb`, so you can also run an ad-hoc
96+
deploy from a workstation:
97+
98+
```bash
99+
yarn workspace @loradb/play build
100+
npx wrangler pages deploy apps/play.loradb.com/out \
101+
--project-name=play-loradb --branch=main
102+
```
103+
104+
### Alternative: Vercel
105+
106+
Vercel is fully supported as a static host for the same `out/`
107+
directory. Workflow:
108+
109+
1. Install the Vercel CLI globally (`npm i -g vercel`).
110+
2. From `apps/play.loradb.com`, run `vercel link --project play-loradb`
111+
to bind the directory to a new Vercel project.
112+
3. In the project settings on vercel.com configure:
113+
- Framework preset: **Next.js**.
114+
- Build command: `yarn workspace @loradb/play build`.
115+
- Install command: `yarn install --immutable` (run from the repo
116+
root — the project root must therefore be the repo root, not
117+
`apps/play.loradb.com`).
118+
- Output directory: `apps/play.loradb.com/out`.
119+
4. Add the custom domain `play.loradb.com` under _Domains_ and update
120+
the DNS `CNAME` to the Vercel target that the dashboard prints.
121+
5. If you want CI-driven deploys (rather than Vercel's own Git
122+
integration), add a workflow that runs `vercel deploy --prebuilt`
123+
after building. Not included by default — the Cloudflare workflow
124+
in this repo is the canonical path.
125+
126+
There is **no** committed workflow for Vercel; this section is provided
127+
purely as a fallback.
128+
129+
### Caveats
130+
131+
- The app is fully static. There are no API routes, no server actions,
132+
no edge functions, no ISR — it _must_ be hosted as flat files behind
133+
a CDN.
134+
- The WASM payloads must be served with
135+
`Content-Type: application/wasm`. The bundled `public/_headers` file
136+
handles that on Cloudflare Pages; replicate the rule if you host
137+
elsewhere.
138+
- IndexedDB and `localStorage` are origin-scoped. Saved queries,
139+
snapshots, settings, and history are local to the user's browser
140+
and the active origin — switching origins (e.g.
141+
`play.loradb.com` ↔ a staging URL) does not migrate user data.
142+
- `@loradb/lora-wasm` ships its own Web Worker. Cross-origin isolation
143+
(`COOP`/`COEP`) is **not** required, but caching the worker and its
144+
WASM payload as `immutable` (handled by `public/_headers`) keeps
145+
reloads fast.
146+
147+
## Build verification locally
148+
149+
```bash
150+
yarn workspace @loradb/play build
151+
# Static export lands in apps/play.loradb.com/out
152+
npx serve apps/play.loradb.com/out -l 5000
153+
# Visit http://localhost:5000
154+
```
155+
156+
The static export is genuinely static — there is no `next start`. If
157+
something only works under `next dev` and breaks under `next build`, it
158+
is almost certainly an SSR-vs-static-export issue (e.g. a top-level
159+
`window` reference inside a module imported by a Server Component) and
160+
needs a `"use client"` boundary.
161+
162+
## Known issues
163+
164+
- **Hash-route reload via direct URL.** Deep links that encode state in
165+
the URL hash (`#q=...`) refresh cleanly because `_redirects` falls
166+
back to `index.html`; deep links that rely on a path that Next did
167+
not statically render at build time will 404. Keep new state in the
168+
hash, not the pathname, until we revisit routing.
169+
- **WASM mime on non-Cloudflare hosts.** Vercel and most CDNs serve
170+
`.wasm` correctly out of the box; bespoke object-store setups (e.g.
171+
raw S3 + CloudFront) need a manual MIME override or the browser
172+
refuses to instantiate via streaming.
173+
- **Workspace dep rebuilds.** The CI workflow rebuilds `lora-wasm`,
174+
`lora-query`, and `lora-graph-canvas` before building `@loradb/play`.
175+
Locally, after pulling, run the workspace builds shown in the
176+
Development section or `next build` may load stale `dist/` output.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"use client";
2+
3+
/**
4+
* VS-Code-style activity bar — a 48px vertical rail with one icon
5+
* button per section. Clicking a button selects the section and
6+
* forces the sidebar open if it was collapsed.
7+
*/
8+
9+
import type { ComponentType } from "react";
10+
import { Stack, Tooltip, UnstyledButton } from "@mantine/core";
11+
import {
12+
IconCamera,
13+
IconFileText,
14+
IconHistory,
15+
IconSchema,
16+
IconSettings,
17+
type IconProps,
18+
} from "@tabler/icons-react";
19+
20+
import { useStore } from "@/lib/state/store";
21+
import type { ActivitySection } from "@/lib/state/slices/layout";
22+
import { hexA } from "@/lib/theme/util";
23+
import { usePlaygroundTheme } from "@/lib/theme/usePlaygroundTheme";
24+
25+
interface ActivityItem {
26+
section: ActivitySection;
27+
label: string;
28+
Icon: ComponentType<IconProps>;
29+
}
30+
31+
const ITEMS: ReadonlyArray<ActivityItem> = [
32+
{
33+
section: "queries",
34+
label: "Saved queries",
35+
Icon: IconFileText,
36+
},
37+
{
38+
section: "schema",
39+
label: "Schema browser",
40+
Icon: IconSchema,
41+
},
42+
{
43+
section: "snapshots",
44+
label: "Snapshots",
45+
Icon: IconCamera,
46+
},
47+
{
48+
section: "history",
49+
label: "History",
50+
Icon: IconHistory,
51+
},
52+
{
53+
section: "settings",
54+
label: "Settings",
55+
Icon: IconSettings,
56+
},
57+
];
58+
59+
export const ACTIVITY_BAR_WIDTH = 48;
60+
61+
export function ActivityBar() {
62+
const { tokens } = usePlaygroundTheme();
63+
const activeSection = useStore((s) => s.activitySection);
64+
const sidebarOpen = useStore((s) => s.sidebarOpen);
65+
const setActivitySection = useStore((s) => s.setActivitySection);
66+
const toggleSidebar = useStore((s) => s.toggleSidebar);
67+
68+
return (
69+
<Stack
70+
gap={4}
71+
align="center"
72+
py={8}
73+
style={{
74+
width: ACTIVITY_BAR_WIDTH,
75+
flexShrink: 0,
76+
background: tokens.bg.sidebar,
77+
borderRight: `1px solid ${tokens.border.subtle}`,
78+
}}
79+
role="tablist"
80+
aria-label="Activity sections"
81+
>
82+
{ITEMS.map(({ section, label, Icon }) => {
83+
const active = section === activeSection && sidebarOpen;
84+
return (
85+
<Tooltip
86+
key={section}
87+
label={label}
88+
position="right"
89+
withArrow
90+
>
91+
<UnstyledButton
92+
role="tab"
93+
aria-selected={active}
94+
aria-label={label}
95+
onClick={() => {
96+
if (section === activeSection && sidebarOpen) {
97+
// Re-clicking the active icon collapses the sidebar.
98+
toggleSidebar();
99+
return;
100+
}
101+
setActivitySection(section);
102+
if (!sidebarOpen) toggleSidebar();
103+
}}
104+
style={{
105+
width: 36,
106+
height: 36,
107+
display: "flex",
108+
alignItems: "center",
109+
justifyContent: "center",
110+
borderRadius: tokens.radius.sm,
111+
color: active ? tokens.fg.primary : tokens.fg.muted,
112+
background: active
113+
? hexA(tokens.accent.primary, 0.25)
114+
: "transparent",
115+
transition: "background 120ms ease, color 120ms ease",
116+
cursor: "pointer",
117+
}}
118+
onMouseEnter={(e) => {
119+
if (!active) {
120+
(e.currentTarget as HTMLElement).style.background = hexA(
121+
tokens.fg.primary,
122+
0.06,
123+
);
124+
}
125+
}}
126+
onMouseLeave={(e) => {
127+
if (!active) {
128+
(e.currentTarget as HTMLElement).style.background =
129+
"transparent";
130+
}
131+
}}
132+
>
133+
<Icon size={20} />
134+
</UnstyledButton>
135+
</Tooltip>
136+
);
137+
})}
138+
</Stack>
139+
);
140+
}

0 commit comments

Comments
 (0)