-
Notifications
You must be signed in to change notification settings - Fork 7
feat(dashnote-starter): add minimal React example + backport revision pre-check to dashnote #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b96db14
feat(dashnote-starter): add minimal React example for notes CRUD
thephez 5429dfb
feat(dashnote-starter): UX polish + optimistic concurrency check
thephez ac476ff
refactor(dashnote-starter): note-card UI polish + add CLAUDE.md
thephez 6e8f223
feat(dashnote): pre-check expected revision before saving notes
thephez a168ea7
feat(dashnote-starter): show sample notes above sign-in form
thephez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code when working in [example-apps/dashnote-starter/](.). | ||
|
|
||
| ## Project Overview | ||
|
|
||
| React + TypeScript + Vite example for Dash Platform notes. Sits between [dashnote-lite.html](../dashnote/public/dashnote-lite.html) (read-only, no build) and the full [dashnote](../dashnote/) app (mobile layouts, activity log, theming, optimistic UI). The starter does full CRUD with mnemonic auth and React as a thin render layer — no Context, no custom hooks, no Tailwind, no state libraries. | ||
|
|
||
| The bar for changes here is "would a learner reading this top-to-bottom be helped by this?" If the answer is no, the change probably belongs in the full app, not here. | ||
|
|
||
| ## Commands | ||
|
|
||
| - `npm run dev` — start Vite dev server | ||
| - `npm run build` — typecheck (`tsc -b`) then bundle | ||
| - `npm run lint` — ESLint | ||
| - `npm run format` / `format:check` — Prettier (double quotes; an empty `.prettierrc.json` shields this app from the repo-root single-quote config) | ||
| - `npm run preview` — serve production build locally | ||
|
|
||
| No test suite. Lint + tsc + manual smoke is the floor. | ||
|
|
||
| ## Architecture | ||
|
|
||
| - **[src/dash/](src/dash/)** — one file per Platform SDK operation. Copied verbatim from the full app's `src/dash/` (minus `loginWithPrivateKey.ts` and `resolveDpnsName.ts`). Each exports an async function with a leading JSDoc block naming the SDK method it wraps. | ||
| - **Shared SDK core** — [src/dash/client.ts](src/dash/client.ts) and [src/dash/keyManager.ts](src/dash/keyManager.ts) re-export `createClient` and `IdentityKeyManager` from `../../../../setupDashClient-core.mjs` at the repo root. Same arrangement as the full app — no vendoring. The `@dashevo/evo-sdk` bare specifier is aliased in [vite.config.ts](vite.config.ts) to this app's local browser bundle. | ||
| - **[src/App.tsx](src/App.tsx)** — top-level component. Holds session state (sdk, keyManager, identityId), notes list, editing state, and the four CRUD handlers + sign-in + sign-out + refresh. **No Context**: state lives here and flows down via props. Mnemonic never enters App state — it lives only in `SignIn`'s local input until it's passed up to `handleSignIn`, where it's consumed by `IdentityKeyManager.create` and immediately discarded. | ||
| - **[src/components/](src/components/)** — three components: `SignIn`, `NoteEditor` (single component for create + edit, mode switched by the `note` prop), and `NoteList`. All functional, all local state via `useState`. No custom hooks. | ||
| - **[src/dash/contract.ts](src/dash/contract.ts)** — only exports `DEFAULT_CONTRACT_ID` (hardcoded testnet contract). The schema lives in the full app's `contract.ts`; if the schema ever changes, both apps need to publish a new contract anyway, so duplicating it here adds maintenance cost with no upside. | ||
| - **[src/lib/logger.ts](src/lib/logger.ts)** — a trimmed copy of the full app's `Logger` type, just enough for the dash/ helpers' `log?.()` calls. The starter wires `log` to a single status string rather than an activity log. | ||
|
|
||
| ## SDK Patterns | ||
|
|
||
| - **Connect**: `createClient("testnet")` from the shared core, dynamically imported in [App.tsx](src/App.tsx) inside `handleSignIn`. | ||
| - **Mnemonic auth**: `IdentityKeyManager.create({ sdk, mnemonic, network, identityIndex })`. Always identity index 0; multi-identity wallets are out of scope. | ||
| - **Create / update / delete** match the full app: `sdk.documents.create`, `sdk.documents.get` + `sdk.documents.replace` (with `revision = BigInt(existing.revision) + 1n`), `sdk.documents.delete`. | ||
| - **List**: `sdk.documents.query` with `where: [["$ownerId", "==", ownerId]]` and `orderBy: [["$ownerId", "asc"], ["$updatedAt", "asc"]]`. | ||
| - **Stale-revision check**: [updateNote.ts](src/dash/updateNote.ts) accepts an optional `expectedRevision`. If the network's revision doesn't match, the write is refused before submitting — basic optimistic concurrency. This is the one SDK pattern the starter teaches that the lite app doesn't and the full app handles with a fancier conflict UI. | ||
|
|
||
| ## Performance — load-anchor rules | ||
|
|
||
| Same as the full app. The `@dashevo/evo-sdk` browser bundle is ~8MB; a top-level value import in any file reachable from `App.tsx` anchors that chunk to the entry graph and silently regresses FCP. | ||
|
|
||
| Three guards keep that win: | ||
|
|
||
| 1. **No top-level value imports from `@dashevo/evo-sdk`** in any file reachable from `App.tsx`. Type-only imports are fine. Value imports (`Document`, `Identifier`) must go through [src/dash/sdkModule.ts](src/dash/sdkModule.ts)'s cached dynamic `import("@dashevo/evo-sdk")`. | ||
| 2. **`App.tsx` dynamically imports `setupDashClient-core.mjs`** via a module-level `loadSdkCore()`. The shared-core import is distinct from the SDK-module import — collapsing them would force one async load to wait on the other. | ||
| 3. **`modulePreload.resolveDependencies` filter in [vite.config.ts](vite.config.ts)** strips the `evo-sdk` chunk from auto-injected `<link rel="modulepreload">` tags. The `<link>` is the regression vector, not the `import()` call. | ||
|
|
||
| Regression check: `grep 'from "[^"]*evo-sdk' dist/assets/index-*.js` after `npm run build` should return nothing — the SDK should appear only behind a dynamic `import()`. | ||
|
|
||
| ## Note contract | ||
|
|
||
| Hardcoded to the same default the full app uses. Notes created here interoperate with the full app and vice versa. Schema and registration logic live in the full app's [contract.ts](../dashnote/src/dash/contract.ts). | ||
|
|
||
| ## Gotchas | ||
|
|
||
| - Update flow **must** fetch the document first to get the current revision. Submitting a replace with the wrong `revision` will fail the state transition. The pattern is `BigInt(existing.revision ?? 0) + 1n` — see [updateNote.ts](src/dash/updateNote.ts). | ||
| - The 8MB WASM bundle is expected on first SDK load; this is not a build error. | ||
| - `allowJs: true` in [tsconfig.app.json](tsconfig.app.json) so TypeScript can import the JSDoc-typed `.mjs` core at the host repo root. | ||
| - Identity-nonce collisions (the network reporting "nonce already present at tip") can happen if writes are submitted faster than the network's nonce propagation. The starter shows the raw error and lets the user retry rather than implementing a conflict UI — see the full app for the resolution pattern. | ||
| - React is treated as a render layer, not the subject. Resist the urge to introduce Context, custom hooks, a state library, Tailwind, a toast library, or animation libraries. The full app exists for those patterns. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| # Dashnote Starter | ||
|
|
||
| A minimal React + Vite example for | ||
| [Dash Platform](https://docs.dash.org/platform). Full CRUD against a notes | ||
| contract on testnet: sign in with a BIP-39 mnemonic, create notes, edit them, | ||
| delete them. No theming, no toasts, no activity log — just the SDK calls and a | ||
| thin render layer. | ||
|
|
||
| Sits between two existing examples in this repo: | ||
|
|
||
| - [dashnote-lite.html](../dashnote/public/dashnote-lite.html) — single static | ||
| HTML file, read-only, no build step. Browse public notes without signing in. | ||
| - **`dashnote-starter`** (this app) — React + Vite, full CRUD, mnemonic auth, no | ||
| UX polish. The "I want to read this top-to-bottom and understand the SDK" | ||
| tier. | ||
| - [dashnote](../dashnote/) — reference app with mobile layouts, activity log, | ||
| theming, DPNS resolution, optimistic updates. Shows what a polished consumer | ||
| of the SDK looks like. | ||
|
|
||
| ## Run it | ||
|
|
||
| You need a funded testnet identity. The fastest way to get one is via | ||
| [Dash Bridge](https://bridge.thepasta.org/) — it generates a mnemonic, registers | ||
| an identity, and tops it up in one flow. Save the mnemonic; you'll paste it on | ||
| the sign-in screen. | ||
|
|
||
| ```sh | ||
| npm install | ||
| npm run dev | ||
| ``` | ||
|
|
||
| Open the printed URL, paste the mnemonic, and you should see your (empty) note | ||
| list. | ||
|
|
||
| ## What's in here | ||
|
|
||
| ``` | ||
| src/ | ||
| ├── main.tsx — React root | ||
| ├── App.tsx — session state + the four CRUD handlers | ||
| ├── styles.css — single plain stylesheet | ||
| ├── components/ | ||
| │ ├── SignIn.tsx — mnemonic paste form | ||
| │ ├── NoteEditor.tsx — shared create/edit form | ||
| │ └── NoteList.tsx — list with edit + delete buttons | ||
| ├── dash/ — one file per Platform SDK operation | ||
| │ ├── client.ts — createClient(network) | ||
| │ ├── keyManager.ts — IdentityKeyManager (mnemonic → DIP-13 keys) | ||
| │ ├── sdkModule.ts — cached dynamic import of @dashevo/evo-sdk | ||
| │ ├── contract.ts — DEFAULT_CONTRACT_ID (see full app for the schema) | ||
| │ ├── createNote.ts — sdk.documents.create | ||
| │ ├── updateNote.ts — sdk.documents.get + sdk.documents.replace | ||
| │ ├── deleteNote.ts — sdk.documents.delete | ||
| │ ├── queries.ts — sdk.documents.query (list by owner) | ||
| │ └── types.ts — shared SDK type aliases | ||
| └── lib/ | ||
| └── logger.ts — tiny logger contract used by the dash/ helpers | ||
| ``` | ||
|
|
||
| The note contract is hardcoded to `8d6heK6CoskLBi6Rs7cChRG9RuckcZqZst28BdviBe8y` | ||
| — the same one the full dashnote app uses by default. Notes you create here show | ||
| up in the full app, and vice versa. | ||
|
|
||
| ## What's here | ||
|
|
||
| - **Sign in / sign out** — paste a mnemonic, identity is derived in-memory and | ||
| never persisted; "Sign out" drops the session | ||
| - **Create / read / update / delete** notes against the hardcoded contract | ||
| - **Refresh** button to re-query the note list | ||
| - **Stale-revision detection** — passing `expectedRevision` to `updateNote` | ||
| refuses the save if the network's revision moved while the editor was open | ||
| (basic optimistic concurrency control) | ||
|
|
||
| ## What's deliberately missing | ||
|
|
||
| In keeping with "render layer, not the subject," this app skips: | ||
|
|
||
| - WIF (private-key) auth — mnemonic only | ||
| - DPNS name resolution | ||
| - Contract registration UI — see the full app for `sdk.contracts.publish()` | ||
| - Activity log, toast notifications, theming | ||
| - Mobile-specific layouts | ||
| - localStorage cache, background revalidation, conflict-resolution UI | ||
| - React Context, custom hooks | ||
| - Tailwind, state libraries, animation libraries | ||
| - Tests — `npm run lint` + `tsc` are the only automated checks | ||
|
|
||
| If you want to see those patterns, read [`../dashnote/`](../dashnote/). If you | ||
| just want to understand which SDK calls are involved, start here. | ||
|
|
||
| ## Note on the SDK chunk | ||
|
|
||
| `@dashevo/evo-sdk` ships ~8MB of WASM. A top-level static import in any file | ||
| reachable from `App.tsx` would block first paint. This app keeps the SDK off the | ||
| entry chunk via: | ||
|
|
||
| - A dynamic `import("../../../setupDashClient-core.mjs")` in `App.tsx` for | ||
| `createClient` + `IdentityKeyManager` | ||
| - A cached dynamic import in `src/dash/sdkModule.ts` for value imports like | ||
| `Document` + `Identifier` | ||
| - A `modulePreload.resolveDependencies` filter in `vite.config.ts` that strips | ||
| the SDK chunk from auto-injected `<link rel="modulepreload">` tags | ||
|
|
||
| See [`../dashnote/CLAUDE.md`](../dashnote/CLAUDE.md) Performance section for the | ||
| full rules. If you're forking this app, don't add a top-level | ||
| `import { … } from "@dashevo/evo-sdk"` — it silently regresses FCP. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import js from "@eslint/js"; | ||
| import globals from "globals"; | ||
| import reactHooks from "eslint-plugin-react-hooks"; | ||
| import reactRefresh from "eslint-plugin-react-refresh"; | ||
| import tseslint from "typescript-eslint"; | ||
| import { defineConfig, globalIgnores } from "eslint/config"; | ||
|
|
||
| export default defineConfig([ | ||
| globalIgnores(["dist"]), | ||
| { | ||
| files: ["**/*.{ts,tsx}"], | ||
| extends: [ | ||
| js.configs.recommended, | ||
| tseslint.configs.recommended, | ||
| reactHooks.configs.flat.recommended, | ||
| reactRefresh.configs.vite, | ||
| ], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| }, | ||
| }, | ||
| ]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Dashnote Starter</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.