Repository navigation
feat(web): add settings page, vault detail modal, and skeleton loading states - #761
Merged
OnlyReeda merged 3 commits intoOct 22, 2026
Conversation
Found while implementing OdyxeeeLabs#682/OdyxeeeLabs#684/OdyxeeeLabs#685 and verifying lint/typecheck/ test/build per the required process — all pre-date this branch and are unrelated to the three issues, but were blocking verification entirely: - app/layout.tsx and pages/_app.tsx each had duplicated/unreconciled JSX trees (two merged features' provider trees left side by side instead of combined), making them fail to parse at all. Reconciled into single, coherent trees that keep every provider/feature from both branches (ErrorBoundary, MotionProvider, WalletProvider, ToastProvider, AnnouncerProvider, SkipLink, NetworkStatusBanner, RpcFallbackBanner, CommandPaletteLauncher, Analytics, ServiceWorkerManager). - pages/index.tsx had a duplicated second results column (two versions of the same UploadZone/ContractInteraction block). Merged into one, keeping the newer ErrorBoundary-wrapped ResultViewer and the OdyxeeeLabs#470 live-region announcements from both. Also replaces the dynamic-import loading fallback and in-flight-simulation state with the previously-unused `ResultViewerSkeleton` instead of plain "Loading…" text (issue OdyxeeeLabs#685). - web/package.json was missing the `@tanstack/react-virtual` dependency that `yarn.lock`, `VaultList.tsx` and `useVirtualVaultList.ts` already depend on (present in the lockfile but never added to package.json), so a clean install couldn't resolve it. Added as a direct dependency; package-lock.json updated accordingly. (yarn.lock intentionally left alone — yarn 4.9.1 crashes with an unrelated internal error in this sandbox; a maintainer with a working yarn should run `yarn install` once to sync it for yarn-based installs.) - web/package.json's `test` script glob (`./lib/**/*.test.cjs` etc.) was silently dropping every direct child test file under `lib/` and `__tests__/` when run through `sh -c` (POSIX glob treats `**` as a single `*`, requiring exactly one directory level — only nested test files matched). Quoted the globs so Node's own test runner expands them (it supports true recursive globs), surfacing ~190 previously-dead tests, including the new ones added for OdyxeeeLabs#682/OdyxeeeLabs#684. - hooks/useFocusTrap.ts's `FocusTrap` interface hardcoded `ref: RefObject<HTMLElement>` instead of being generic over `T`, so any new caller of the generic `useFocusTrap<T>()` (used here for the new vault detail modal, OdyxeeeLabs#684) failed to typecheck when attaching the ref to a typed element — the same bug already affected NavDrawer.tsx. Made the interface generic. - lib/vault-metadata.ts's `normalizeVaultData` read fields directly off a `Partial<Vault | VaultRecord | VaultMetadataInput>` union, which doesn't typecheck since the members don't share those fields. Read through an explicit `Record<string, unknown>` view instead (every value is already re-validated via String()/Number() immediately after). Confirmed via diffing `tsc --noEmit` output before/after: no new errors introduced, and the fixes above remove 4 pre-existing error sites for free. `npm run build` still fails on a clean upstream/main checkout for unrelated reasons (literal App Router vs Pages Router route collisions on /, /admin/managers and /vault/[id] — web/Vault.md documents this migration as still in progress) and `npm run lint` fails project-wide because `@typescript-eslint`'s rules aren't resolving under the installed eslint-config-next/eslint version combination (every file in the repo reports it, including ones untouched by this branch) — both pre-exist on upstream/main and are out of scope for this change.
Closes OdyxeeeLabs#682. Comprehensive settings page at /settings, organized into five tabs: - Network: shows the build-time-resolved active network (reads lib/contracts.config.ts getStage(), the same source NetworkStatusBanner already uses — not duplicated), plus a persisted "preferred network" display preference, custom RPC endpoint and chain ID override for a future runtime network switcher. - Trading: slippage tolerance (0.1-10%, clamped), gas price strategy (slow/standard/fast/custom with a custom gas price field), transaction deadline in minutes. - Display: currency, number format, theme (reuses the existing <ThemeToggle /> component as-is rather than re-implementing `<html class="dark">` toggling), and a preferred-language preference. - Security: 2FA toggle (flagged as requiring backend enrollment, not implemented), session timeout, and connected wallets — reuses useWallet() from context/WalletContext.tsx rather than re-reading wallet localStorage keys directly. - Advanced: debug mode, feature flags (reuses the existing featureFlagService singleton directly — the React <FeatureFlagProvider> is only mounted in the Pages Router tree, not available to this App Router page), and a "download settings as JSON" data export. Persistence: there is no backend settings service, so this is a client-only first cut — lib/settings.ts holds validated, pure get/merge/persist logic (deep-merges onto defaults so a corrupted or partial localStorage blob never crashes the UI), and lib/settingsStore.ts is a thin React binding following the same createStore/createUseStore pattern context/WalletContext.tsx already uses. The page defers reading localStorage until after mount (useHydrateSettings) and shows the new FormSkeleton (OdyxeeeLabs#685) for that brief window instead of a flash of default values or a spinner. Tests: lib/settings.test.cjs covers clamping, merge-with-defaults (including recovery from corrupted/out-of-range stored values), and JSON parse/export, following the existing config.test.cjs convention (mirrors the pure logic in plain JS + a source guard against drift).
Closes OdyxeeeLabs#684. Expandable vault detail modal/drawer, data loaded on demand: - types/vault.ts gains VaultDetail (strategy config, performance metrics, risk parameters, token info, historical points) alongside the existing lean Vault type used for list rows — kept separate since the detail view needs much richer data than every row fetch should pay for. - lib/vault-detail.ts fetches it on demand. vaultService.get() only returns id/name/status/config_json today (no strategy/risk/performance fields), so this generates deterministic, seeded mock data from the vault id (same id -> same numbers, verified in vault-detail.test.cjs) rather than inventing a fake "looks real" backend contract that would need to be un-mocked later. buildVaultDetail is the single seam to start reading real fields from config_json once the backend has them. - hooks/useVaultDetail.ts only starts fetching once an id is passed in, so a closed modal never pays the fetch cost. - components/vault/HistoricalPerformanceChart.tsx is a small inline SVG sparkline — no charting library dependency added. package.json has none today, and next.config.js already deliberately splits vendor chunks to manage bundle size (OdyxeeeLabs#686); one sparkline isn't worth a new dependency. - components/vault/VaultDetailContent.tsx renders the sections (token info, performance, chart, strategy, risk, actions) and is shared by VaultDetailModal (overlay) and the app/vault/[id] + app/vaults/[id] page routes (full page), so list-triggered and deep-linked views never drift apart. Deposit/withdraw/manage actions are wired to a toast placeholder — building real transactions is out of scope here. - VaultDetailModal reuses the existing useFocusTrap hook (OdyxeeeLabs#703) rather than a bespoke focus trap, and ships with its own matching skeleton (VaultDetailSkeleton, OdyxeeeLabs#685) instead of a spinner. - components/VaultRow.tsx gains an optional onClick, making the whole row an interactive trigger (role="button", keyboard support) without breaking the existing href/PrefetchLink case; components/vault/VaultList wires it to the modal and also had its VaultRow import fixed (it pointed at a nonexistent ./VaultRow instead of ../VaultRow). - app/vault/[id]/page.tsx and app/vaults/[id]/page.tsx were placeholder stubs; both now call generateMetadata (via lib/vault-metadata.ts's existing buildVaultMetadata/buildVaultJsonLd) and render the vault detail content, with Next.js 15+'s async `params` handled correctly (previously typed as a synchronous object). This also satisfies two pre-existing tests in __tests__/vault-metadata.test.cjs and __tests__/vault-routing.test.cjs that were never actually running before this branch's test-glob fix and were failing against the old stubs. - app/vaults/page.tsx is a small new list page (fixed demo id set — there is no "list all vaults for a manager" flow wired up yet) that demonstrates VaultList -> VaultDetailModal end to end, since neither component had any caller in the app before this change. Also includes the remaining skeleton loading states for OdyxeeeLabs#685: components/TransactionRowSkeleton.tsx (sized to the existing TRANSACTION_ROW_HEIGHT constant — no TransactionRow component exists yet to retrofit) and components/ui/DataTableSkeleton.tsx (mirrors components/ui/DataTable.tsx's header/filter/row layout). VaultCardSkeleton and VaultDetailSkeleton ship in this same commit since OdyxeeeLabs#684 needed them immediately; see the previous commit for FormSkeleton and the ResultViewerSkeleton wiring into pages/index.tsx. All new skeletons follow the accessibility/visual convention already established by NutritionLabelSkeleton/ResultViewerSkeleton (animate-pulse, role="table"/"row"/"cell", aria-busy="true"), using the color palette of the content each one stands in for (slate for vault/transaction rows to match VaultRow, the existing #161b22 GitHub-dark palette for the generic table/form skeletons). Deliberately left alone: the small Loader2 icons inside DynamicForm's and WasmUpload's active submit buttons (components/DynamicForm.tsx, components/WasmUpload.tsx). Those are transient busy-state indicators on a button that already has a label ("Simulating…", "Invoking…", a per-file upload row), not full-content loading screens — replacing a 16px icon inside an already-visible, already-labeled control with a skeleton would be the wrong pattern, per components/ui/Spinner.tsx's own documented purpose.
|
@ThatCodeBabe Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Implements three Stellar Wave Program issues in one branch (#685 depends on loading states introduced by #684, so these were done in order: #682 → #684 → #685).
#682 — Settings page
/settings, five tabs (Network, Trading, Display, Security, Advanced). No backend settings service exists yet, so this is a client-only first cut backed bylocalStorage(web/lib/settings.ts+web/lib/settingsStore.ts, following the samecreateStore/createUseStorepatterncontext/WalletContext.tsxalready uses). Reuses existing state instead of duplicating it where it already exists:lib/contracts.config.ts'sgetStage()(same sourceNetworkStatusBanneruses) rather than re-deriving it, and persists a UI-only "preferred network" + custom RPC endpoint + chain ID for a future runtime switcher.<ThemeToggle />as-is.useWallet()for the connected-wallets list.featureFlagServicesingleton directly (the React<FeatureFlagProvider>is only mounted in the Pages Router tree, not available to this App Router page).#684 — Vault detail modal
Expandable modal/drawer with strategy config, performance metrics, risk parameters, a historical APY sparkline, token info, and deposit/withdraw/manage actions (wired to a toast placeholder — no transaction-building). Data loads on demand (
useVaultDetail, only fetches once the modal actually opens).There's no backend endpoint yet returning strategy/risk/performance data, so
lib/vault-detail.tsgenerates deterministic, seeded mock data from the vault id (same id → same numbers, covered bylib/vault-detail.test.cjs) rather than inventing a fake "looks real" contract. The historical chart is a small inline SVG sparkline — no charting library added (none exists inpackage.json, andnext.config.jsalready splits vendor chunks deliberately for bundle size, #686).VaultDetailContentis shared between the modal and the/vault/[id]and/vaults/[id]page routes (previously placeholder stubs), so the list-triggered and deep-linked views never drift apart. Both page routes now also exportgenerateMetadatavia the existingbuildVaultMetadata/buildVaultJsonLdhelpers, and correctly type Next.js 15+'s asyncparams.components/VaultRow.tsxgained an optionalonClick(keyboard-accessible, doesn't conflict with the existinghref/PrefetchLinkcase), andcomponents/vault/VaultList.tsxhad a broken import (./VaultRowinstead of../VaultRow) fixed so it actually compiles. A new/vaultslist page wiresVaultList→VaultDetailModalend-to-end, since neither component had a caller anywhere in the app before this change.#685 — Skeleton loading states
New skeletons:
VaultCardSkeleton/VaultListSkeleton,VaultDetailSkeleton(ships with #684 from the start, per the issue's own sequencing note),TransactionRowSkeleton(sized to the existingTRANSACTION_ROW_HEIGHTconstant — noTransactionRowcomponent exists yet to retrofit),DataTableSkeleton(mirrorscomponents/ui/DataTable.tsx), andFormSkeleton(used by the Settings page's hydration guard). All follow the accessibility/visual convention already established byNutritionLabelSkeleton/ResultViewerSkeleton(animate-pulse,role="table"/"row"/"cell",aria-busy="true"), colored to match the content each stands in for.Also wired the two existing-but-previously-unused skeletons into real call sites:
ResultViewerSkeletonnow replaces both the dynamic-import "Loading result viewer…" text fallback and the in-flight-simulation state inpages/index.tsx.Left alone intentionally: the small
Loader2icons insideDynamicForm's andWasmUpload's active submit buttons — those are transient busy-state indicators on an already-labeled control ("Simulating…", "Invoking…"), not full-content loading screens; swapping a 16px icon for a skeleton there would be the wrong pattern.Pre-existing issues found and fixed (unrelated to these 3 issues, but blocking verification)
app/layout.tsxandpages/_app.tsxeach had duplicated/unreconciled JSX from two previously-merged features, failing to parse at all. Reconciled into single trees keeping every provider from both branches.pages/index.tsxhad a duplicated second results column; merged into one.web/package.jsonwas missing the@tanstack/react-virtualdependency thatyarn.lockand existing vault-list code already required (present in the lockfile, never added topackage.json) — added it;package-lock.jsonupdated. (yarn.lockintentionally left alone — yarn 4.9.1 crashed with an unrelated internal error in the sandbox used for this change; a maintainer with a workingyarnshould runyarn installonce to sync it.)web/package.json'stestscript glob was silently dropping every direct-child test file underlib/and__tests__/when run throughsh(**degrades to a single*under POSIX glob, requiring exactly one directory level). Quoted the globs so Node's own recursive glob engine handles them — this surfaced ~190 previously-dead tests, including the new ones added here, and 6 pre-existing, unrelated failures (component-docs registry drift, an App Router migration placeholder check, a mobile-responsiveness class-string check, and a CSPconnect-srccheck) that were never actually being run before. None of those 6 are touched by this PR.hooks/useFocusTrap.ts'sFocusTrapinterface hardcoded a non-generic ref type, breaking any typed caller (already affectedNavDrawer.tsx; now needed for the new vault detail modal). Made it generic.lib/vault-metadata.ts'snormalizeVaultDataread fields off a union type that didn't typecheck. Read through an explicitRecord<string, unknown>view instead.Verification
npm test(web/): 545/551 passing. The 6 failures are 100% pre-existing on a cleanupstream/main(confirmed by running the suite on a pristine checkout with only the test-glob fix applied) and unrelated to these 3 issues — see above.npm run typecheck(web/): fails, but only with the same pre-existing errors present on a cleanupstream/mainbefore this branch (missing optional devDependencies for storybook/playwright/testing-library/axe-core, a sentry SDK version mismatch,next-intlconfig types,lib/api.tsDTO narrowing, an unrelated dead-variable bug inErrorBoundary.tsx, etc.) — confirmed by diffing the error list before/after this branch's changes. No new errors introduced; several pre-existing ones fixed (see above).npm run lint(web/): fails project-wide, including on files this PR never touches — every file reportsDefinition for rule '@typescript-eslint/...' was not found, aneslint/eslint-config-nextversion-compatibility break pre-existing onupstream/main. Filtering that specific noise out,npx eslinton every file this PR adds/touches reports zero other issues.npm run build(web/): fails on a cleanupstream/mainbefore this branch too — Next 16 treats the App Router/Pages Router path collisions on/,/admin/managersand/vault/[id]as hard build errors (web/Vault.mddocuments this App Router migration as still in progress, Phase 2 of 4). This branch adds no new colliding routes (/settings,/vaults,/vaults/[id]have no Pages Router equivalent) and doesn't change this pre-existing failure mode.Judgment calls
localStorage-backed client store, no backend — documented above and inlib/settings.ts's file header.NEXT_PUBLIC_STELLAR_NETWORK) and shown read-only.LanguageSwitcher(which depends onnext/router, unavailable under the App Router page this Settings page renders on).Closes #682
Closes #684
Closes #685