Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
48dcdaf
feat(example-apps): add DashRate review-rating example app
thephez Jun 23, 2026
3bf7861
style(dashrate): refine review UI copy and layout
thephez Jun 23, 2026
c096c22
feat(dashrate): set a default testnet review contract
thephez Jun 23, 2026
78b963b
refactor(dashrate): partial-fill stars and divider-based review layout
thephez Jun 24, 2026
fc1c960
feat(dashrate): rating distribution histogram and filter-by-rating
thephez Jun 24, 2026
0adbc4c
feat(dashrate): show DPNS names for reviewers
thephez Jun 24, 2026
413b79b
fix(dashrate): clear stale reviews on switch and show a loading state
thephez Jun 24, 2026
1d85517
chore: update comments, run format, update readme
thephez Jun 24, 2026
eb20c86
test(dashrate): expand coverage and add coverage tooling
thephez Jun 24, 2026
1c1be48
chore: update gitignore
thephez Jun 24, 2026
7f84659
feat(dashrate): improve the sign-in form
thephez Jun 24, 2026
8526c7c
fix(dashrate): refresh recent reviews after saving a review
thephez Jun 24, 2026
c81ae39
ci(dashrate): run test and build on source changes
thephez Jun 24, 2026
59f652a
refactor(dashrate): extract App into components, hooks, and lib modules
thephez Jun 25, 2026
7e74c33
feat(dashrate): polish the review composer and history
thephez Jun 25, 2026
cca77fb
chore(dashrate): trim console noise and add small-phone breakpoints
thephez Jun 25, 2026
dc12d59
feat(dashrate): sign in on Enter in the mnemonic field
thephez Jun 25, 2026
2bf9824
docs(dashrate): align CLAUDE.md architecture with components/hooks la…
thephez Jun 25, 2026
57c2c3a
feat(dashrate): scroll to detail and pin the header on mobile
thephez Jun 25, 2026
367fd29
feat(dashrate): polish the rating histogram and flatten the sidebar
thephez Jun 25, 2026
18c753c
fix(dashrate): correct mobile scroll offset and touch-hover on the hi…
thephez Jun 25, 2026
dd1d384
feat(dashrate): add a View on GitHub footer link
thephez Jun 25, 2026
1c5d129
test(dashrate): add component, hook, and Playwright e2e suites
thephez Jun 25, 2026
f82021e
test(dashrate): cover App orchestration and errorMessage shapes
thephez Jun 25, 2026
00e2f4b
feat(dashrate): mask the mnemonic field as a password input
thephez Jun 29, 2026
8aa698a
fix(dashrate): address PR review feedback on accessibility, queries, …
thephez Jun 29, 2026
1493dd1
chore(dashrate): exclude test output dirs from prettier
thephez Jun 29, 2026
1d039db
fix(dashrate): guard contract-id localStorage writes
thephez Jun 29, 2026
d7ea596
feat(dashrate): expand the How it works page into a guided walkthrough
thephez Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/dashrate-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: DashRate CI

on:
push:
branches: [main]
paths:
- 'example-apps/dashrate/**'
- '.github/workflows/dashrate-ci.yml'
pull_request:
branches: [main]
paths:
- 'example-apps/dashrate/**'
- '.github/workflows/dashrate-ci.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: dashrate-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: test + build
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: example-apps/dashrate
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: example-apps/dashrate/package-lock.json

- name: Show Node/npm versions
run: |
node -v
npm -v

- name: Install
run: npm ci

- name: Test
run: npm test

# tsc -b runs as part of build, so this covers the typecheck too.
- name: Build
run: npm run build
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ node_modules/
example-apps/*/node_modules/
example-apps/*/dist/
example-apps/*/dist-ssr/
example-apps/*/coverage/
example-apps/*/*.local

# Example apps (Playwright artifacts)
example-apps/*/playwright-report/
example-apps/*/test-results/
1 change: 1 addition & 0 deletions example-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Stand-alone applications built on top of the same `@dashevo/evo-sdk` used by the
- [dashmint-lab/](./dashmint-lab/) — React + TypeScript + Vite SPA for minting, viewing, transferring, and trading NFT-style collectible cards on Dash Platform testnet. Shares the browser-safe SDK core (`setupDashClient-core.mjs`) with the Node tutorials at the repo root.
- [dashproof-lab/](./dashproof-lab/) — React + TypeScript + Vite proof-of-existence tutorial app that hashes files locally in the browser, anchors SHA-256 proofs on Dash Platform testnet, verifies files by hash, and reviews proof history by owner or chain ID. Also uses the shared browser-safe SDK core from the parent repo.
- [dashnote/](./dashnote/) — React + TypeScript + Vite notes app for Dash Platform testnet. Create, edit, and delete notes against a small `note` data contract; supports a "Remember Me" read-only browse mode, optimistic localStorage cache, and ships a single-file zero-build read-only companion at `dashnote-lite.html`. Also uses the shared browser-safe SDK core from the parent repo.
- [dashrate/](./dashrate/) — React + TypeScript + Vite app for rating Platform tutorial resources on Dash Platform testnet, built to showcase Platform 4.0's relational document queries: provable `count`, grouped `count` (`GROUP BY`) for the per-star distribution, range counts, and `where` filtering. One review per identity per resource (editable, with document history); read-only browsing works without signing in. Also uses the shared browser-safe SDK core from the parent repo.
5 changes: 5 additions & 0 deletions example-apps/dashrate/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist
node_modules
coverage
playwright-report
test-results
1 change: 1 addition & 0 deletions example-apps/dashrate/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
80 changes: 80 additions & 0 deletions example-apps/dashrate/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CLAUDE.md

This file provides guidance to Claude Code when working in [example-apps/dashrate/](.).

## Project Overview

React + TypeScript + Vite app for rating and reviewing Dash Platform resources (tutorials and example apps) on testnet. A review has a required integer `rating` (1–5), an optional `reviewText`, and a `resourceId` pointing at a catalog entry. One review per identity per resource (enforced by a unique index) — saving again edits the existing document. The shell is a four-view app (`resources` / `my-reviews` / `settings` / `how`): the Resources view is a sidebar resource list + a detail panel showing the aggregate rating, a per-star distribution histogram, a review form, and recent reviews. Read-only browse works without auth; writing a review requires signing in with a mnemonic in Settings.

This app is the showcase for Platform 4.0's relational query features — provable `count`, grouped `count` (`GROUP BY`), range counts, and `where` filtering. See [SDK query patterns](#sdk-query-patterns).

## Commands

- `npm run dev` — start Vite dev server
- `npm run build` — typecheck (`tsc -b`) then bundle
- `npm run lint` — ESLint
- `npm run test` — Vitest suite in [test/](test/) (unit, component, and hook tests)
- `npm run test:coverage` — Vitest suite under v8 coverage
- `npm run test:e2e` — Playwright suite in [test/e2e/](test/e2e/) (auto-boots Vite on :5182)
- `npm run test:e2e:ui` — Playwright with the interactive UI runner
- `npm run format` / `format:check` — Prettier
- `npm run preview` — serve production build locally

## Architecture

`App.tsx` is the orchestrator: it owns the cross-cutting state and handlers, calls the data hooks, and renders one presentational view component per `view`. The layers:

- **[src/dash/](src/dash/)** — one file per Platform SDK concern, each with a leading JSDoc block naming the SDK method it wraps: [contract.ts](src/dash/contract.ts) (schema + register/store contract ID), [queries.ts](src/dash/queries.ts) (all reads — count, grouped count, document query, normalization, summary derivation), [review.ts](src/dash/review.ts) (create/replace a review), [history.ts](src/dash/history.ts) (document revision history), [resolveDpnsName.ts](src/dash/resolveDpnsName.ts) (DPNS username lookup for reviewer names, via `sdk.dpns.username`), [types.ts](src/dash/types.ts) (shared SDK type aliases incl. the `DashSdk` shape), [sdkModule.ts](src/dash/sdkModule.ts) (cached dynamic `import("@dashevo/evo-sdk")`), [sdkCore.ts](src/dash/sdkCore.ts) (cached dynamic `import` of the shared core at `../../../../setupDashClient-core.mjs`, exposing `loadSdkCore()`), [client.ts](src/dash/client.ts) / [keyManager.ts](src/dash/keyManager.ts) (re-export `createClient` / `IdentityKeyManager` from that shared core).
- **[src/catalog/resources.ts](src/catalog/resources.ts)** — the hardcoded `RESOURCES` list (tutorials + example apps). Each has `id` (used as `resourceId`), `title`, `category`, `summary`, `href`. Resources are compile-time static — there's no user-facing "add a resource" flow.
- **[src/App.tsx](src/App.tsx)** — owns top-level state (session, `contractId`/`contractInput`, `selectedResourceId`, `view`, sign-in form fields, `history`, `status`, `busy`), the action handlers (`handleSignIn`, `handleSaveReview`, `handleRegisterContract`, `handleLoadHistory`, contract submit/clear, sign-out, resource select, edit-my-review), and the `connectReadOnly` read-only SDK factory. It loads the shared SDK core via [sdkCore.ts](src/dash/sdkCore.ts)'s `loadSdkCore()`.
- **[src/hooks/](src/hooks/)** — data-fetching hooks, each returning state plus refresh callbacks: [useResourceRatings.ts](src/hooks/useResourceRatings.ts) (per-resource summaries/distributions, recent reviews, the review-composer state — `rating`/`hoverRating`/`reviewText`/`mySelectedReview` — `reviewFilter`, and the `loadResourceData`/`refreshReviews` effects with request-id guards against stale responses), [useMyReviews.ts](src/hooks/useMyReviews.ts) (the signed-in user's reviews + derived average, loaded lazily when the `my-reviews` view is active), [useDpnsNames.ts](src/hooks/useDpnsNames.ts) (best-effort DPNS name cache keyed by owner ID, resolved lazily for reviewers in view).
- **[src/components/](src/components/)** — presentational components, props-only (no SDK imports): the four view shells [ResourcesView](src/components/ResourcesView.tsx), [MyReviewsView](src/components/MyReviewsView.tsx), [SettingsView](src/components/SettingsView.tsx), [HowItWorks](src/components/HowItWorks.tsx); [TopNav](src/components/TopNav.tsx) (owns the `View` type); [AppNotices](src/components/AppNotices.tsx) (status banner + "no contract" notice); and the pieces [ReviewForm](src/components/ReviewForm.tsx), [RecentReviews](src/components/RecentReviews.tsx), [ReviewRow](src/components/ReviewRow.tsx), [MyReviewCard](src/components/MyReviewCard.tsx), [ReviewHistory](src/components/ReviewHistory.tsx), [StarMeter](src/components/StarMeter.tsx) (partial-fill star renderer).
- **[src/session/types.ts](src/session/types.ts)** — the `Session` shape (`{ sdk, keyManager, identityId }`) shared by App and the hooks.
- **[src/lib/](src/lib/)** — pure utilities, no SDK references: [logger.ts](src/lib/logger.ts) (`Logger`/`LogLevel` types, `errorMessage`, `consoleLogger`), [format.ts](src/lib/format.ts) (`formatAverage`, `formatDate`, `shortId`), [ratings.ts](src/lib/ratings.ts) (`RATING_ROWS`, `emptySummary`/`emptyDistribution`, `ownerLabel`, `reviewCountLine`, text `stars`).
- **[test/](test/)** — Vitest + Testing Library, flat directory, named after the subject. Three kinds: unit tests over `src/dash/` and `src/lib/` that stub the `DashSdk` shape (`*.test.ts`); component tests rendered with Testing Library (`*.test.tsx`); and hook tests via `renderHook`. Default Vitest env is `node`; component/hook tests opt into DOM with a `// @vitest-environment jsdom` pragma at the top of the file (the vitest `include` covers `**/*.test.{ts,tsx}`). Component/hook tests **mock the SDK loaders** (`vi.mock("../src/dash/sdkModule", …)` / `vi.mock("../src/dash/sdkCore", …)`) so the 8 MB bundle never imports — never let a test pull `@dashevo/evo-sdk` into the jsdom process. [tsconfig.app.json](tsconfig.app.json) includes `test`, so `tsc -b` (run by `build`) strict-typechecks the `.test.tsx` files — keep mock factories and stub props fully typed. `npm run test:coverage` runs the suite under v8 coverage.
- **[test/e2e/](test/e2e/)** — Playwright specs plus shared `fixtures.ts`, driven by [playwright.config.ts](playwright.config.ts), which auto-starts `npx vite` on port 5182. Two projects (`chromium-desktop` / `chromium-mobile`) so every spec exercises both layouts. Runs against real testnet — no SDK mocks. The specs are read-only shell smoke tests ([smoke](test/e2e/smoke.spec.ts) — navigation, browse a resource, How-it-works; [settings](test/e2e/settings.spec.ts) — the sign-in form renders/gates without signing in); they assert rendering, not live rating data. Run locally via `npm run test:e2e`. [tsconfig.app.json](tsconfig.app.json)'s `exclude: ["test/e2e"]` keeps these out of the app `tsc -b` (Playwright typechecks them itself).

## Review contract

Schema lives in [src/dash/contract.ts](src/dash/contract.ts) as `REVIEW_SCHEMAS`. One document type, `review`:

- `resourceId` — required string, 1–63 chars, position 0
- `rating` — required integer, 1–5, position 1
- `reviewText` — optional string, max 1000 chars, position 2
- `$createdAt`, `$updatedAt` — required (Platform-managed)
- `documentsMutable: true`, `documentsKeepHistory: true`, `canBeDeleted: false` — reviews are editable, keep revision history, and can't be deleted

Indices:

- `ownerAndResource` — unique (`$ownerId`, `resourceId`); enforces one review per identity per resource
- `ownerReviews` — (`$ownerId`, `$updatedAt`); lists a user's reviews
- `resourceRatingAggregate` — (`resourceId`), `countable`; total review count per resource
- `resourceRatingDistribution` — (`resourceId`, `rating`), `countable` + `rangeCountable: true`; backs the grouped rating distribution AND the `rating == N` filter

`DEFAULT_CONTRACT_ID` is `BdgTqaTAPYMyhp1WdeWdcvYSgoD7AuJ7tVCaCSXyQgyP`. Overrides are stored under `localStorage['dashrate.contractId']`. Settings can register a fresh contract and switch to it; the contract-ID input is controlled and auto-fills on register.

## SDK query patterns

This app deliberately demonstrates the relational query surface. The query types in play:

- **Total count** — `sdk.documents.count({ where: [["resourceId","==",id]] })` over the single-property `resourceRatingAggregate` index. Ungrouped result is a one-entry `Map` keyed `""`; read with `firstMapValue`. (`getRatingCount` in [queries.ts](src/dash/queries.ts).)
- **Grouped distribution count** — `sdk.documents.count({ where: [["resourceId","==",id], ["rating","between",[1,5]]], groupBy: ["rating"], orderBy: [["rating","asc"]] })` over `resourceRatingDistribution`. Returns one entry per present rating. The average is **derived in JS** from these per-star counts (`summaryFromDistribution`) — there is no `sum`/`average` query. (`getRatingDistribution` in [queries.ts](src/dash/queries.ts).)
- **Filter by rating** — `listResourceReviews` adds `["rating","==",N]` to the `where` (a point lookup covered by `[resourceId, rating]`). Server-side on purpose, to demonstrate `where` filtering and stay correct past the fetch limit.
- **Document query / history** — `sdk.documents.query` for the review list; `sdk.documents.history` for revisions.

`normalizeReviews` / `normalizeSingleReview` in [queries.ts](src/dash/queries.ts) flatten whatever shape `query`/`get` returns (array, Map, plain object) into `ReviewRecord[]`.

## Performance — load-anchor rules

Same as the sibling apps: the `@dashevo/evo-sdk` browser bundle is ~8 MB and must stay off the boot critical path. **Never add a top-level value import from `@dashevo/evo-sdk`** to any file reachable from `App.tsx` — go through [sdkModule.ts](src/dash/sdkModule.ts)'s cached dynamic import (type-only imports are fine). The shared core is loaded via [sdkCore.ts](src/dash/sdkCore.ts)'s `loadSdkCore()` — two distinct loaders, don't merge. The `modulePreload.resolveDependencies` filter in [vite.config.ts](vite.config.ts) strips the `evo-sdk` chunk so Vite doesn't inject a `<link rel="modulepreload">` that re-blocks first paint. The synchronous exports of [contract.ts](src/dash/contract.ts) (`REVIEW_SCHEMAS`, `loadStoredContractId`, `saveContractId`, `clearStoredContractId`, `DEFAULT_CONTRACT_ID`) must stay synchronous — they run during initial render before the SDK loads.

## Gotchas

- **Grouped-count map keys are raw index-key bytes, NOT the value.** `count` with `groupBy: ["rating"]` returns a `Map` keyed by the hex of the property's order-preserving index-key encoding, not the integer. For a small positive integer that's the sign-flipped single byte `0x80 | value` → rating 5 is key `"85"`, rating 1 is `"81"` (verified against the live contract — it is _not_ an 8-byte big-endian form). `ratingKeyHex(r) = (0x80 | r).toString(16)` in [queries.ts](src/dash/queries.ts) re-encodes each known rating to look it up. The SDK exposes no decoder, so the client encodes the values it's looking for rather than decoding what comes back.
- **`rangeCountable` is a separate flag from `countable`, required for range/grouped counts.** A range count (the `between` on `rating` that drives the grouped distinct walk) needs `rangeCountable: true` on the index, with the range field as the **last** index property. A `countable`-only index fails at query time with `range count requires a range_countable: true index whose last property matches the range field`.
- **Don't mix `summable` and a deeper count-only index on a shared prefix** — it registers fine but breaks every document insert (`NotCountedOrSummed-wrapping is only supported for the six sum-bearing tree variants`). `resourceRatingAggregate` is intentionally count-only (no `summable`) so its `resourceId` value tree isn't count+sum and can host `resourceRatingDistribution`'s count-only `rating` continuation. The average is derived from the distribution instead of a `sum`/`average` query. Full analysis: [dashpay/platform#3960](https://github.com/dashpay/platform/issues/3960).
- **`between` value is a 2-element array, inclusive.** `["rating","between",[1,5]]` matches `1 <= rating <= 5`. The drive expects exactly two bounds.
- **A document query's `orderBy` field must be the serving index's TRAILING property — even for equality filters.** The index matcher (`Index::matches`) reserves the order-by field from the _back_ of the index. So filtering reviews by rating (`where resourceId== AND rating==`) must use `orderBy: [["rating","asc"]]` (the last property of `[resourceId, rating]`); ordering by `resourceId` there strips `rating` from the usable prefix and the query is rejected as `where clause on non indexed property … query must be for valid indexes`. `listResourceReviews` switches `orderBy` based on whether a `ratingFilter` is set. (Server `orderBy` only drives index selection here; the list is re-sorted client-side by `createdAt`.)
- **Update flow** (`saveReview` replacing an existing review) bumps the revision off the fetched document; the unique `ownerAndResource` index means a second save edits rather than duplicates.
- **The contract-ID input is controlled** (`contractInput` state in `App.tsx`). Register/Use/Clear all sync it; an uncontrolled `defaultValue` would not reflect a freshly-registered ID.
- The Evo SDK WASM bundle is ~8 MB; that's expected, not a build error. See [Performance](#performance--load-anchor-rules).
61 changes: 61 additions & 0 deletions example-apps/dashrate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# DashRate

DashRate is a React + TypeScript + Vite example app for rating Platform tutorial resources on Dash
Platform testnet.

It is intentionally small and centered on Platform v4's relational document queries:

- `sdk.documents.query` for resource reviews, identity reviews, and existing-review lookup
- `sdk.documents.count` for the total review count per resource (a plain countable index)
- `sdk.documents.count` with `groupBy: ["rating"]` for the per-star rating distribution — the
count/sum/average shown per resource is derived in JS from this one grouped count, not a separate
`sum`/`average` query
- a `where rating == N` clause for filtering reviews by star rating
- `sdk.documents.history` for review edit history

Users sign in with a mnemonic only. After signing in, they can register a local testnet contract,
paste an existing DashRate contract ID, create one review per resource, edit that review, and
inspect its document history. Read-only browsing (resources, aggregates, reviews) works without
signing in.

## Quick start

```bash
npm install
npm run dev
```

Other scripts:

```bash
npm run build
npm run test
npm run test:coverage
npm run lint
npm run preview
```

## Contract

The app defines one mutable document type, `review`, in
[`src/dash/contract.ts`](./src/dash/contract.ts). Each identity can review a resource once, enforced
by the unique `$ownerId + resourceId` index. Saving a review creates the document on first use and
replaces the same document on later edits, with document history retained by `documentsKeepHistory`.

The read paths are intentionally index-shaped:

- resource detail and recent reviews query by `resourceId`
- My reviews queries by `$ownerId` and sorts by `$updatedAt`
- edit detection queries by `$ownerId + resourceId`
- the total review count uses the standalone `resourceId` index (`countable: "countable"`)
- the rating distribution and the `rating == N` filter use the compound `resourceId + rating` index
(`countable: "countable"` plus `rangeCountable: true`)

Neither aggregate index uses `summable`: the average shown per resource is computed in JS from the
grouped distribution count (which also backs the histogram), while the total review count comes from
the standalone `resourceId` count index.

`DEFAULT_CONTRACT_ID` is set to a published testnet DashRate contract
(`BdgTqaTAPYMyhp1WdeWdcvYSgoD7AuJ7tVCaCSXyQgyP`), so fresh installs can read aggregates and reviews
immediately. The active ID is stored under `localStorage['dashrate.contractId']`; clearing it falls
back to this default. Register your own contract from the Settings tab to override it.
Loading