create-top-secret-starter 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend-overlay/remove.json +5 -0
- package/backend-overlay/root/_gitignore +2 -0
- package/backend-overlay/web/src/api/index.ts +11 -1
- package/backend-overlay/web/src/api/live/index.ts +97 -0
- package/backend-overlay/web/src/api/orders/index.ts +65 -0
- package/backend-overlay/web/src/components/header-actions.tsx +30 -0
- package/backend-overlay/web/src/components/sidebar-user.tsx +33 -0
- package/backend-overlay/web/src/lib/query-client.test.tsx +22 -0
- package/backend-overlay/web/src/lib/query-client.ts +31 -11
- package/backend-overlay/web/src/routes/_authenticated/index.tsx +19 -54
- package/backend-overlay/web/src/routes/_authenticated/notes/index.tsx +65 -0
- package/backend-overlay/web/src/routes/_authenticated.tsx +1 -1
- package/backend-overlay/web-router/src/routes/sign-in.tsx +17 -3
- package/backend-overlay/web-router/src/routes/sign-up.tsx +18 -4
- package/backend-overlay/web-start/src/api/index.ts +11 -1
- package/backend-overlay/web-start/src/routes/sign-in.tsx +17 -3
- package/backend-overlay/web-start/src/routes/sign-up.tsx +18 -4
- package/features.json +52 -0
- package/index.js +76 -44
- package/markers.js +37 -0
- package/package.json +4 -2
- package/templates/template-api/CLAUDE.md +6 -0
- package/templates/template-api/drizzle/0002_dear_warstar.sql +15 -0
- package/templates/template-api/drizzle/meta/0002_snapshot.json +585 -0
- package/templates/template-api/drizzle/meta/_journal.json +7 -0
- package/templates/template-api/src/app.test.ts +175 -3
- package/templates/template-api/src/app.ts +35 -5
- package/templates/template-api/src/db/schema.ts +36 -0
- package/templates/template-api/src/index.ts +21 -1
- package/templates/template-api/src/modules/live/hub.ts +123 -0
- package/templates/template-api/src/modules/live/index.ts +30 -0
- package/templates/template-api/src/modules/live/model.ts +31 -0
- package/templates/template-api/src/modules/notes/index.ts +1 -1
- package/templates/template-api/src/modules/orders/index.ts +71 -0
- package/templates/template-api/src/modules/orders/model.ts +49 -0
- package/templates/template-api/src/modules/orders/service.ts +78 -0
- package/templates/template-router/.claude/settings.json +1 -1
- package/templates/template-router/.claude/skills/better-accessibility/SKILL.md +111 -0
- package/templates/template-router/.claude/skills/better-accessibility/agents/openai.yaml +3 -0
- package/templates/template-router/.claude/skills/better-accessibility/focus-and-keyboard.md +131 -0
- package/templates/template-router/.claude/skills/better-accessibility/forms.md +84 -0
- package/templates/template-router/.claude/skills/better-accessibility/hit-areas.md +94 -0
- package/templates/template-router/.claude/skills/better-accessibility/motion-and-zoom.md +79 -0
- package/templates/template-router/.claude/skills/better-accessibility/screen-readers.md +101 -0
- package/templates/template-router/.claude/skills/better-accessibility/semantics-and-aria.md +84 -0
- package/templates/template-router/.claude/skills/better-colors/SKILL.md +105 -0
- package/templates/template-router/.claude/skills/better-colors/agents/openai.yaml +3 -0
- package/templates/template-router/.claude/skills/better-colors/color-formats.md +90 -0
- package/templates/template-router/.claude/skills/better-colors/color-usage.md +118 -0
- package/templates/template-router/.claude/skills/better-colors/contrast.md +79 -0
- package/templates/template-router/.claude/skills/better-colors/palette-generation.md +104 -0
- package/templates/template-router/.claude/skills/better-colors/palette-structure.md +76 -0
- package/templates/template-router/.claude/skills/better-colors/token-naming.md +97 -0
- package/templates/template-router/.claude/skills/better-interface/SKILL.md +137 -0
- package/templates/template-router/.claude/skills/better-interface/agents/openai.yaml +3 -0
- package/templates/template-router/.claude/skills/better-interface/review-format.md +46 -0
- package/templates/template-router/.claude/skills/better-layout/SKILL.md +81 -0
- package/templates/template-router/.claude/skills/better-layout/agents/openai.yaml +3 -0
- package/templates/template-router/.claude/skills/better-layout/grouping-and-alignment.md +123 -0
- package/templates/template-router/.claude/skills/better-layout/spacing-and-adaptivity.md +159 -0
- package/templates/template-router/.claude/skills/better-typography/SKILL.md +162 -0
- package/templates/template-router/.claude/skills/better-typography/agents/openai.yaml +3 -0
- package/templates/template-router/.claude/skills/better-typography/choosing-fonts.md +64 -0
- package/templates/template-router/.claude/skills/better-typography/css-cheat-sheet.md +65 -0
- package/templates/template-router/.claude/skills/better-typography/details-and-accessibility.md +119 -0
- package/templates/template-router/.claude/skills/better-typography/spacing-and-sizing.md +121 -0
- package/templates/template-router/.claude/skills/better-typography/variable-fonts-and-opentype.md +105 -0
- package/templates/template-router/.claude/skills/better-typography/wrapping-and-punctuation.md +55 -0
- package/templates/template-router/.claude/skills/better-ui/SKILL.md +112 -0
- package/templates/template-router/.claude/skills/better-ui/agents/openai.yaml +3 -0
- package/templates/template-router/.claude/skills/better-ui/animations.md +205 -0
- package/templates/template-router/.claude/skills/better-ui/enter-exit.md +147 -0
- package/templates/template-router/.claude/skills/better-ui/icon-transitions.md +102 -0
- package/templates/template-router/.claude/skills/better-ui/icons.md +110 -0
- package/templates/template-router/.claude/skills/better-ui/performance.md +88 -0
- package/templates/template-router/.claude/skills/better-ui/surfaces.md +219 -0
- package/templates/template-router/.claude/skills/better-writing/SKILL.md +114 -0
- package/templates/template-router/.claude/skills/better-writing/agents/openai.yaml +3 -0
- package/templates/template-router/.claude/skills/break/SKILL.md +85 -0
- package/templates/template-router/.claude/skills/break/agents/openai.yaml +5 -0
- package/templates/template-router/.claude/skills/break/scenarios.md +78 -0
- package/templates/template-router/.claude/skills/explain-interface/SKILL.md +132 -0
- package/templates/template-router/.claude/skills/explain-interface/agents/openai.yaml +5 -0
- package/templates/template-router/.claude/skills/explain-interface/find-the-effect.md +94 -0
- package/templates/template-router/.claude/skills/explain-interface/from-an-image.md +55 -0
- package/templates/template-router/.claude/skills/explain-interface/no-browser.md +73 -0
- package/templates/template-router/.claude/skills/explain-interface/read-the-system.md +178 -0
- package/templates/template-router/.claude/skills/interface-review/SKILL.md +154 -0
- package/templates/template-router/.claude/skills/interface-review/agents/openai.yaml +5 -0
- package/templates/template-router/.claude/skills/interface-review/removed-signals.md +38 -0
- package/templates/template-router/.claude/skills/interface-review/scope-resolution.md +88 -0
- package/templates/template-router/.claude/skills/project-conventions/SKILL.md +40 -30
- package/templates/template-router/.claude/skills/variant/SKILL.md +110 -0
- package/templates/template-router/.claude/skills/variant/agents/openai.yaml +5 -0
- package/templates/template-router/.claude/skills/variant/picker.md +76 -0
- package/templates/template-router/.oxfmtrc.json +8 -1
- package/templates/template-router/.oxlintrc.json +18 -8
- package/templates/template-router/CLAUDE.md +3 -5
- package/templates/template-router/_gitignore +2 -0
- package/templates/template-router/doctor.config.json +4 -0
- package/templates/template-router/e2e/dashboard.spec.ts +38 -0
- package/templates/template-router/e2e/keyboard.spec.ts +18 -8
- package/templates/template-router/e2e/orders.spec.ts +114 -0
- package/templates/template-router/e2e/utils.ts +1 -2
- package/templates/template-router/index.html +11 -8
- package/templates/template-router/package.json +11 -7
- package/templates/template-router/playwright.config.ts +15 -23
- package/templates/template-router/src/api/auth/session-store.test.ts +14 -4
- package/templates/template-router/src/api/index.ts +59 -28
- package/templates/template-router/src/api/live/feed.ts +14 -0
- package/templates/template-router/src/api/live/index.ts +105 -0
- package/templates/template-router/src/api/live/live.test.ts +67 -0
- package/templates/template-router/src/api/live/schema.ts +33 -0
- package/templates/template-router/src/api/orders/index.ts +63 -0
- package/templates/template-router/src/api/orders/input.ts +48 -0
- package/templates/template-router/src/api/orders/orders.test.ts +48 -0
- package/templates/template-router/src/api/orders/schema.ts +20 -0
- package/templates/template-router/src/api/refresh.test.ts +62 -0
- package/templates/template-router/src/components/app-shell.plain.tsx +29 -0
- package/templates/template-router/src/components/app-shell.tsx +24 -0
- package/templates/template-router/src/components/app-sidebar.tsx +133 -0
- package/templates/template-router/src/components/dashboard/activity-feed.tsx +42 -0
- package/templates/template-router/src/components/dashboard/dashboard.tsx +88 -0
- package/templates/template-router/src/components/dashboard/live-badge.tsx +23 -0
- package/templates/template-router/src/components/dashboard/revenue-chart.tsx +82 -0
- package/templates/template-router/src/components/dashboard/stat-tile.tsx +33 -0
- package/templates/template-router/src/components/error-screen.tsx +2 -2
- package/templates/template-router/src/components/header-actions.tsx +28 -0
- package/templates/template-router/src/components/nav.ts +17 -0
- package/templates/template-router/src/components/orders/create-order-dialog.tsx +143 -0
- package/templates/template-router/src/components/orders/order-status-badge.tsx +28 -0
- package/templates/template-router/src/components/orders/order-status-select.tsx +59 -0
- package/templates/template-router/src/components/orders/order-status.ts +11 -0
- package/templates/template-router/src/components/orders/orders-table-store.ts +50 -0
- package/templates/template-router/src/components/orders/orders-table.tsx +476 -0
- package/templates/template-router/src/components/root-error-boundary.tsx +3 -7
- package/templates/template-router/src/components/sidebar-user.tsx +34 -0
- package/templates/template-router/src/components/ui/badge.tsx +48 -0
- package/templates/template-router/src/components/ui/button.tsx +2 -2
- package/templates/template-router/src/components/ui/card.tsx +87 -0
- package/templates/template-router/src/components/ui/chart.tsx +338 -0
- package/templates/template-router/src/components/ui/dialog.tsx +136 -0
- package/templates/template-router/src/components/ui/dropdown-menu.tsx +255 -0
- package/templates/template-router/src/components/ui/empty.tsx +2 -2
- package/templates/template-router/src/components/ui/field.tsx +6 -6
- package/templates/template-router/src/components/ui/input.tsx +1 -1
- package/templates/template-router/src/components/ui/select.tsx +189 -0
- package/templates/template-router/src/components/ui/separator.tsx +1 -1
- package/templates/template-router/src/components/ui/sheet.tsx +125 -0
- package/templates/template-router/src/components/ui/sidebar.tsx +682 -0
- package/templates/template-router/src/components/ui/skeleton.tsx +13 -0
- package/templates/template-router/src/components/ui/table.tsx +86 -0
- package/templates/template-router/src/components/ui/textarea.tsx +1 -1
- package/templates/template-router/src/components/ui/toast.tsx +3 -3
- package/templates/template-router/src/components/ui/tooltip.tsx +2 -2
- package/templates/template-router/src/hooks/use-mobile.ts +18 -0
- package/templates/template-router/src/index.css +0 -7
- package/templates/template-router/src/lib/format.ts +39 -0
- package/templates/template-router/src/lib/query-client.test.tsx +21 -10
- package/templates/template-router/src/lib/query-client.ts +26 -15
- package/templates/template-router/src/lib/sidebar-store.ts +10 -0
- package/templates/template-router/src/lib/single-flight.test.ts +14 -3
- package/templates/template-router/src/lib/single-flight.ts +9 -7
- package/templates/template-router/src/lib/storage-store.test.ts +63 -0
- package/templates/template-router/src/lib/storage-store.ts +22 -7
- package/templates/template-router/src/mocks/live.ts +110 -0
- package/templates/template-router/src/mocks/orders.ts +39 -0
- package/templates/template-router/src/mocks/people.ts +38 -0
- package/templates/template-router/src/mocks/server.ts +133 -5
- package/templates/template-router/src/providers/index.tsx +9 -15
- package/templates/template-router/src/providers/theme.test.tsx +3 -1
- package/templates/template-router/src/providers/theme.tsx +7 -2
- package/templates/template-router/src/routes/-not-found.tsx +1 -1
- package/templates/template-router/src/routes/-pending.tsx +1 -1
- package/templates/template-router/src/routes/_authenticated/index.tsx +21 -4
- package/templates/template-router/src/routes/_authenticated/notes/$noteId.tsx +5 -5
- package/templates/template-router/src/routes/_authenticated/notes/index.tsx +3 -1
- package/templates/template-router/src/routes/_authenticated/orders/$orderId.tsx +62 -0
- package/templates/template-router/src/routes/_authenticated/orders/index.tsx +189 -0
- package/templates/template-router/src/routes/_authenticated.tsx +7 -42
- package/templates/template-router/src/routes/sign-in.tsx +4 -2
- package/templates/template-router/tsconfig.app.json +1 -1
- package/templates/template-router/tsconfig.node.json +3 -3
- package/templates/template-router/vite.config.ts +2 -15
- package/templates/template-router/vitest.config.ts +16 -0
- package/templates/template-start/.claude/settings.json +1 -1
- package/templates/template-start/.claude/skills/better-accessibility/SKILL.md +111 -0
- package/templates/template-start/.claude/skills/better-accessibility/agents/openai.yaml +3 -0
- package/templates/template-start/.claude/skills/better-accessibility/focus-and-keyboard.md +131 -0
- package/templates/template-start/.claude/skills/better-accessibility/forms.md +84 -0
- package/templates/template-start/.claude/skills/better-accessibility/hit-areas.md +94 -0
- package/templates/template-start/.claude/skills/better-accessibility/motion-and-zoom.md +79 -0
- package/templates/template-start/.claude/skills/better-accessibility/screen-readers.md +101 -0
- package/templates/template-start/.claude/skills/better-accessibility/semantics-and-aria.md +84 -0
- package/templates/template-start/.claude/skills/better-colors/SKILL.md +105 -0
- package/templates/template-start/.claude/skills/better-colors/agents/openai.yaml +3 -0
- package/templates/template-start/.claude/skills/better-colors/color-formats.md +90 -0
- package/templates/template-start/.claude/skills/better-colors/color-usage.md +118 -0
- package/templates/template-start/.claude/skills/better-colors/contrast.md +79 -0
- package/templates/template-start/.claude/skills/better-colors/palette-generation.md +104 -0
- package/templates/template-start/.claude/skills/better-colors/palette-structure.md +76 -0
- package/templates/template-start/.claude/skills/better-colors/token-naming.md +97 -0
- package/templates/template-start/.claude/skills/better-interface/SKILL.md +137 -0
- package/templates/template-start/.claude/skills/better-interface/agents/openai.yaml +3 -0
- package/templates/template-start/.claude/skills/better-interface/review-format.md +46 -0
- package/templates/template-start/.claude/skills/better-layout/SKILL.md +81 -0
- package/templates/template-start/.claude/skills/better-layout/agents/openai.yaml +3 -0
- package/templates/template-start/.claude/skills/better-layout/grouping-and-alignment.md +123 -0
- package/templates/template-start/.claude/skills/better-layout/spacing-and-adaptivity.md +159 -0
- package/templates/template-start/.claude/skills/better-typography/SKILL.md +162 -0
- package/templates/template-start/.claude/skills/better-typography/agents/openai.yaml +3 -0
- package/templates/template-start/.claude/skills/better-typography/choosing-fonts.md +64 -0
- package/templates/template-start/.claude/skills/better-typography/css-cheat-sheet.md +65 -0
- package/templates/template-start/.claude/skills/better-typography/details-and-accessibility.md +119 -0
- package/templates/template-start/.claude/skills/better-typography/spacing-and-sizing.md +121 -0
- package/templates/template-start/.claude/skills/better-typography/variable-fonts-and-opentype.md +105 -0
- package/templates/template-start/.claude/skills/better-typography/wrapping-and-punctuation.md +55 -0
- package/templates/template-start/.claude/skills/better-ui/SKILL.md +112 -0
- package/templates/template-start/.claude/skills/better-ui/agents/openai.yaml +3 -0
- package/templates/template-start/.claude/skills/better-ui/animations.md +205 -0
- package/templates/template-start/.claude/skills/better-ui/enter-exit.md +147 -0
- package/templates/template-start/.claude/skills/better-ui/icon-transitions.md +102 -0
- package/templates/template-start/.claude/skills/better-ui/icons.md +110 -0
- package/templates/template-start/.claude/skills/better-ui/performance.md +88 -0
- package/templates/template-start/.claude/skills/better-ui/surfaces.md +219 -0
- package/templates/template-start/.claude/skills/better-writing/SKILL.md +114 -0
- package/templates/template-start/.claude/skills/better-writing/agents/openai.yaml +3 -0
- package/templates/template-start/.claude/skills/break/SKILL.md +85 -0
- package/templates/template-start/.claude/skills/break/agents/openai.yaml +5 -0
- package/templates/template-start/.claude/skills/break/scenarios.md +78 -0
- package/templates/template-start/.claude/skills/explain-interface/SKILL.md +132 -0
- package/templates/template-start/.claude/skills/explain-interface/agents/openai.yaml +5 -0
- package/templates/template-start/.claude/skills/explain-interface/find-the-effect.md +94 -0
- package/templates/template-start/.claude/skills/explain-interface/from-an-image.md +55 -0
- package/templates/template-start/.claude/skills/explain-interface/no-browser.md +73 -0
- package/templates/template-start/.claude/skills/explain-interface/read-the-system.md +178 -0
- package/templates/template-start/.claude/skills/interface-review/SKILL.md +154 -0
- package/templates/template-start/.claude/skills/interface-review/agents/openai.yaml +5 -0
- package/templates/template-start/.claude/skills/interface-review/removed-signals.md +38 -0
- package/templates/template-start/.claude/skills/interface-review/scope-resolution.md +88 -0
- package/templates/template-start/.claude/skills/project-conventions/SKILL.md +40 -30
- package/templates/template-start/.claude/skills/variant/SKILL.md +110 -0
- package/templates/template-start/.claude/skills/variant/agents/openai.yaml +5 -0
- package/templates/template-start/.claude/skills/variant/picker.md +76 -0
- package/templates/template-start/.oxfmtrc.json +8 -1
- package/templates/template-start/.oxlintrc.json +18 -8
- package/templates/template-start/CLAUDE.md +3 -5
- package/templates/template-start/_gitignore +2 -0
- package/templates/template-start/doctor.config.json +4 -0
- package/templates/template-start/e2e/dashboard.spec.ts +38 -0
- package/templates/template-start/e2e/keyboard.spec.ts +18 -8
- package/templates/template-start/e2e/orders.spec.ts +114 -0
- package/templates/template-start/e2e/utils.ts +1 -1
- package/templates/template-start/package.json +10 -6
- package/templates/template-start/playwright.config.ts +15 -23
- package/templates/template-start/src/api/auth/session-store.test.ts +14 -4
- package/templates/template-start/src/api/index.ts +59 -28
- package/templates/template-start/src/api/live/feed.ts +14 -0
- package/templates/template-start/src/api/live/index.ts +105 -0
- package/templates/template-start/src/api/live/live.test.ts +67 -0
- package/templates/template-start/src/api/live/schema.ts +33 -0
- package/templates/template-start/src/api/orders/index.ts +63 -0
- package/templates/template-start/src/api/orders/input.ts +48 -0
- package/templates/template-start/src/api/orders/orders.test.ts +48 -0
- package/templates/template-start/src/api/orders/schema.ts +20 -0
- package/templates/template-start/src/api/refresh.test.ts +62 -0
- package/templates/template-start/src/components/app-shell.plain.tsx +29 -0
- package/templates/template-start/src/components/app-shell.tsx +24 -0
- package/templates/template-start/src/components/app-sidebar.tsx +133 -0
- package/templates/template-start/src/components/dashboard/activity-feed.tsx +42 -0
- package/templates/template-start/src/components/dashboard/dashboard.tsx +88 -0
- package/templates/template-start/src/components/dashboard/live-badge.tsx +23 -0
- package/templates/template-start/src/components/dashboard/revenue-chart.tsx +82 -0
- package/templates/template-start/src/components/dashboard/stat-tile.tsx +33 -0
- package/templates/template-start/src/components/error-screen.tsx +2 -2
- package/templates/template-start/src/components/header-actions.tsx +28 -0
- package/templates/template-start/src/components/nav.ts +17 -0
- package/templates/template-start/src/components/orders/create-order-dialog.tsx +143 -0
- package/templates/template-start/src/components/orders/order-status-badge.tsx +28 -0
- package/templates/template-start/src/components/orders/order-status-select.tsx +59 -0
- package/templates/template-start/src/components/orders/order-status.ts +11 -0
- package/templates/template-start/src/components/orders/orders-table-store.ts +50 -0
- package/templates/template-start/src/components/orders/orders-table.tsx +476 -0
- package/templates/template-start/src/components/root-error-boundary.tsx +3 -7
- package/templates/template-start/src/components/sidebar-user.tsx +34 -0
- package/templates/template-start/src/components/ui/badge.tsx +48 -0
- package/templates/template-start/src/components/ui/button.tsx +2 -2
- package/templates/template-start/src/components/ui/card.tsx +87 -0
- package/templates/template-start/src/components/ui/chart.tsx +338 -0
- package/templates/template-start/src/components/ui/dialog.tsx +136 -0
- package/templates/template-start/src/components/ui/dropdown-menu.tsx +255 -0
- package/templates/template-start/src/components/ui/empty.tsx +2 -2
- package/templates/template-start/src/components/ui/field.tsx +6 -6
- package/templates/template-start/src/components/ui/input.tsx +1 -1
- package/templates/template-start/src/components/ui/select.tsx +189 -0
- package/templates/template-start/src/components/ui/separator.tsx +1 -1
- package/templates/template-start/src/components/ui/sheet.tsx +125 -0
- package/templates/template-start/src/components/ui/sidebar.tsx +682 -0
- package/templates/template-start/src/components/ui/skeleton.tsx +13 -0
- package/templates/template-start/src/components/ui/table.tsx +86 -0
- package/templates/template-start/src/components/ui/textarea.tsx +1 -1
- package/templates/template-start/src/components/ui/toast.tsx +3 -3
- package/templates/template-start/src/components/ui/tooltip.tsx +2 -2
- package/templates/template-start/src/hooks/use-mobile.ts +18 -0
- package/templates/template-start/src/index.css +0 -7
- package/templates/template-start/src/lib/format.ts +39 -0
- package/templates/template-start/src/lib/query-client.test.tsx +21 -10
- package/templates/template-start/src/lib/query-client.ts +26 -15
- package/templates/template-start/src/lib/sidebar-store.ts +10 -0
- package/templates/template-start/src/lib/single-flight.test.ts +14 -3
- package/templates/template-start/src/lib/single-flight.ts +9 -7
- package/templates/template-start/src/lib/storage-store.test.ts +63 -0
- package/templates/template-start/src/lib/storage-store.ts +27 -7
- package/templates/template-start/src/mocks/live.ts +110 -0
- package/templates/template-start/src/mocks/orders.ts +39 -0
- package/templates/template-start/src/mocks/people.ts +38 -0
- package/templates/template-start/src/mocks/server.ts +133 -5
- package/templates/template-start/src/providers/index.tsx +9 -15
- package/templates/template-start/src/providers/theme.test.tsx +3 -1
- package/templates/template-start/src/providers/theme.tsx +7 -2
- package/templates/template-start/src/routes/-not-found.tsx +1 -1
- package/templates/template-start/src/routes/-pending.tsx +1 -1
- package/templates/template-start/src/routes/__root.tsx +1 -0
- package/templates/template-start/src/routes/_authenticated/index.tsx +21 -4
- package/templates/template-start/src/routes/_authenticated/notes/$noteId.tsx +5 -5
- package/templates/template-start/src/routes/_authenticated/notes/index.tsx +3 -1
- package/templates/template-start/src/routes/_authenticated/orders/$orderId.tsx +62 -0
- package/templates/template-start/src/routes/_authenticated/orders/index.tsx +189 -0
- package/templates/template-start/src/routes/_authenticated.tsx +7 -42
- package/templates/template-start/src/routes/sign-in.tsx +4 -2
- package/templates/template-start/tsconfig.app.json +1 -1
- package/templates/template-start/tsconfig.node.json +3 -3
- package/templates/template-start/tsr.config.json +7 -0
- package/templates/template-start/vite.config.ts +2 -12
- package/templates/template-start/vitest.config.ts +16 -0
- package/backend-overlay/web/src/routeTree.gen.ts +0 -120
- package/backend-overlay/web-start/src/routeTree.gen.ts +0 -191
- package/templates/template-router/e2e/global-setup.ts +0 -24
- package/templates/template-router/src/routeTree.gen.ts +0 -145
- package/templates/template-start/e2e/global-setup.ts +0 -24
- package/templates/template-start/src/routeTree.gen.ts +0 -217
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
[
|
|
2
2
|
"src/mocks",
|
|
3
3
|
"src/api/notes/schema.ts",
|
|
4
|
+
"src/api/orders/schema.ts",
|
|
5
|
+
"src/api/orders/orders.test.ts",
|
|
6
|
+
"src/api/live/schema.ts",
|
|
7
|
+
"src/api/live/feed.ts",
|
|
8
|
+
"src/api/live/live.test.ts",
|
|
4
9
|
"src/api/notes/notes.test.ts",
|
|
5
10
|
"src/routes/_authenticated/notes",
|
|
6
11
|
"src/api/auth/schema.ts",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { treaty } from '@elysia/eden'
|
|
1
|
+
import { EdenFetchError, treaty } from '@elysia/eden'
|
|
2
2
|
|
|
3
3
|
import type { App } from '@repo/api'
|
|
4
4
|
|
|
@@ -12,3 +12,13 @@ export const api = treaty<App>(baseUrl, {
|
|
|
12
12
|
fetch: { credentials: 'include' },
|
|
13
13
|
throwHttpError: true
|
|
14
14
|
}).api // the server's `/api` group
|
|
15
|
+
|
|
16
|
+
// route loaders turn this into `notFound()`; every variant of `api` exports the same helper
|
|
17
|
+
export const isNotFound = (error: unknown) =>
|
|
18
|
+
error instanceof EdenFetchError && error.status === 404
|
|
19
|
+
|
|
20
|
+
// `throwHttpError` already turns failures into exceptions, so a null body is a contract break
|
|
21
|
+
export const unwrap = <T>({ data }: { data: T | null }): T => {
|
|
22
|
+
if (data === null) throw new Error('Empty response')
|
|
23
|
+
return data
|
|
24
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { queryOptions, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { api, unwrap } from '@/api'
|
|
5
|
+
|
|
6
|
+
// types flow from the Elysia routes through Eden — no schema to keep in sync
|
|
7
|
+
export type LiveSnapshot = Awaited<ReturnType<typeof getLiveSnapshot>>
|
|
8
|
+
export type Metrics = LiveSnapshot['metrics'][number]
|
|
9
|
+
export type Activity = LiveSnapshot['activity'][number]
|
|
10
|
+
type LiveEvent = { type: 'metrics'; data: Metrics } | { type: 'activity'; data: Activity }
|
|
11
|
+
|
|
12
|
+
// the same bounds the server's ring buffer uses
|
|
13
|
+
const METRICS_WINDOW = 60
|
|
14
|
+
const ACTIVITY_LIMIT = 20
|
|
15
|
+
|
|
16
|
+
export const getLiveSnapshot = async () => unwrap(await api.live.snapshot.get())
|
|
17
|
+
|
|
18
|
+
// `Infinity`: the socket keeps this entry fresh; a refetch only happens after a reconnect
|
|
19
|
+
export const liveSnapshotQuery = queryOptions({
|
|
20
|
+
queryKey: ['live'],
|
|
21
|
+
queryFn: getLiveSnapshot,
|
|
22
|
+
staleTime: Infinity
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export type LiveStatus = 'connecting' | 'live' | 'reconnecting'
|
|
26
|
+
|
|
27
|
+
const applyLiveEvent = (snapshot: LiveSnapshot, event: LiveEvent): LiveSnapshot => {
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case 'metrics':
|
|
30
|
+
return { ...snapshot, metrics: [...snapshot.metrics.slice(1 - METRICS_WINDOW), event.data] }
|
|
31
|
+
case 'activity':
|
|
32
|
+
return { ...snapshot, activity: [event.data, ...snapshot.activity].slice(0, ACTIVITY_LIMIT) }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const BACKOFF_MS = [1_000, 2_000, 5_000, 10_000]
|
|
37
|
+
|
|
38
|
+
type ConnectOptions = {
|
|
39
|
+
onEvent: (event: LiveEvent) => void
|
|
40
|
+
onStatus: (status: LiveStatus, reconnected: boolean) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// one socket, reconnecting with backoff; the session cookie authorises the upgrade
|
|
44
|
+
export const connectLive = ({ onEvent, onStatus }: ConnectOptions) => {
|
|
45
|
+
let socket: ReturnType<typeof api.live.subscribe> | null = null
|
|
46
|
+
let attempt = 0
|
|
47
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
48
|
+
let closed = false
|
|
49
|
+
|
|
50
|
+
const open = () => {
|
|
51
|
+
socket = api.live.subscribe()
|
|
52
|
+
socket.on('open', () => {
|
|
53
|
+
onStatus('live', attempt > 0)
|
|
54
|
+
attempt = 0
|
|
55
|
+
})
|
|
56
|
+
socket.subscribe(({ data }) => onEvent(data as LiveEvent))
|
|
57
|
+
socket.on('close', () => {
|
|
58
|
+
if (closed) return
|
|
59
|
+
onStatus('reconnecting', false)
|
|
60
|
+
timer = setTimeout(open, BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)])
|
|
61
|
+
attempt += 1
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onStatus('connecting', false)
|
|
66
|
+
open()
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
closed = true
|
|
70
|
+
clearTimeout(timer)
|
|
71
|
+
socket?.close()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const useLiveFeed = () => {
|
|
76
|
+
const queryClient = useQueryClient()
|
|
77
|
+
const { data } = useSuspenseQuery(liveSnapshotQuery)
|
|
78
|
+
const [status, setStatus] = useState<LiveStatus>('connecting')
|
|
79
|
+
|
|
80
|
+
useEffect(
|
|
81
|
+
() =>
|
|
82
|
+
connectLive({
|
|
83
|
+
onEvent: event =>
|
|
84
|
+
queryClient.setQueryData(liveSnapshotQuery.queryKey, snapshot =>
|
|
85
|
+
snapshot ? applyLiveEvent(snapshot, event) : snapshot
|
|
86
|
+
),
|
|
87
|
+
onStatus: (next, reconnected) => {
|
|
88
|
+
setStatus(next)
|
|
89
|
+
// frames missed while offline: the snapshot fills the gap
|
|
90
|
+
if (reconnected) void queryClient.invalidateQueries(liveSnapshotQuery)
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
[queryClient]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return { ...data, status }
|
|
97
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { queryOptions, useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import { api, unwrap } from '@/api'
|
|
4
|
+
import type { CreateOrderInput, OrdersSearch, UpdateOrderStatusInput } from '@/api/orders/input'
|
|
5
|
+
|
|
6
|
+
// types flow from the Elysia routes through Eden — no schema to keep in sync
|
|
7
|
+
export type OrdersPage = Awaited<ReturnType<typeof getOrders>>
|
|
8
|
+
export type Order = OrdersPage['items'][number]
|
|
9
|
+
|
|
10
|
+
export const getOrders = async (search: OrdersSearch) =>
|
|
11
|
+
unwrap(await api.orders.get({ query: search }))
|
|
12
|
+
export const getOrder = async (id: string) => unwrap(await api.orders({ id }).get())
|
|
13
|
+
export const createOrder = async (input: CreateOrderInput) => unwrap(await api.orders.post(input))
|
|
14
|
+
export const updateOrderStatus = async ({
|
|
15
|
+
id,
|
|
16
|
+
...input
|
|
17
|
+
}: UpdateOrderStatusInput & { id: string }) => unwrap(await api.orders({ id }).patch(input))
|
|
18
|
+
|
|
19
|
+
// ['orders', 'list', …] and ['orders', 'detail', …]: one prefix invalidates both
|
|
20
|
+
const ordersKey = ['orders'] as const
|
|
21
|
+
const listKey = [...ordersKey, 'list'] as const
|
|
22
|
+
|
|
23
|
+
export const ordersQuery = (search: OrdersSearch) =>
|
|
24
|
+
queryOptions({ queryKey: [...listKey, search], queryFn: () => getOrders(search) })
|
|
25
|
+
export const orderQuery = (id: string) =>
|
|
26
|
+
queryOptions({ queryKey: [...ordersKey, 'detail', id], queryFn: () => getOrder(id) })
|
|
27
|
+
|
|
28
|
+
export const useOrders = (search: OrdersSearch) => useSuspenseQuery(ordersQuery(search))
|
|
29
|
+
export const useOrder = (id: string) => useSuspenseQuery(orderQuery(id))
|
|
30
|
+
|
|
31
|
+
export const useCreateOrder = () => {
|
|
32
|
+
const queryClient = useQueryClient()
|
|
33
|
+
return useMutation({
|
|
34
|
+
mutationFn: createOrder,
|
|
35
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: listKey })
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const patchStatus = (order: Order, id: string, status: Order['status']) =>
|
|
40
|
+
order.id === id ? { ...order, status } : order
|
|
41
|
+
|
|
42
|
+
// optimistic: the badge flips at once; a failure rolls every touched page and detail back
|
|
43
|
+
export const useUpdateOrderStatus = () => {
|
|
44
|
+
const queryClient = useQueryClient()
|
|
45
|
+
return useMutation({
|
|
46
|
+
mutationFn: updateOrderStatus,
|
|
47
|
+
onMutate: async ({ id, status }) => {
|
|
48
|
+
await queryClient.cancelQueries({ queryKey: ordersKey })
|
|
49
|
+
const lists = queryClient.getQueriesData<OrdersPage>({ queryKey: listKey })
|
|
50
|
+
const detail = queryClient.getQueryData(orderQuery(id).queryKey)
|
|
51
|
+
queryClient.setQueriesData<OrdersPage>({ queryKey: listKey }, page =>
|
|
52
|
+
page ? { ...page, items: page.items.map(order => patchStatus(order, id, status)) } : page
|
|
53
|
+
)
|
|
54
|
+
queryClient.setQueryData(orderQuery(id).queryKey, order =>
|
|
55
|
+
order ? patchStatus(order, id, status) : order
|
|
56
|
+
)
|
|
57
|
+
return { lists, detail }
|
|
58
|
+
},
|
|
59
|
+
onError: (_error, { id }, context) => {
|
|
60
|
+
for (const [key, page] of context?.lists ?? []) queryClient.setQueryData(key, page)
|
|
61
|
+
queryClient.setQueryData(orderQuery(id).queryKey, context?.detail)
|
|
62
|
+
},
|
|
63
|
+
onSettled: () => queryClient.invalidateQueries({ queryKey: ordersKey })
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useRouter } from '@tanstack/react-router'
|
|
2
|
+
import { LogOut } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
import { useSession, useSignOut } from '@/api/auth'
|
|
5
|
+
import { ThemeToggle } from '@/components/theme-toggle'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
|
|
8
|
+
export const HeaderActions = () => {
|
|
9
|
+
const { data: session } = useSession()
|
|
10
|
+
const signOut = useSignOut()
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
|
|
13
|
+
const onSignOut = async () => {
|
|
14
|
+
await signOut.mutateAsync()
|
|
15
|
+
await router.navigate({ to: '/sign-in' })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className='ml-auto flex items-center gap-2 sm:gap-4'>
|
|
20
|
+
<span className='hidden truncate text-sm text-muted-foreground sm:inline'>
|
|
21
|
+
{session?.user.email}
|
|
22
|
+
</span>
|
|
23
|
+
<ThemeToggle />
|
|
24
|
+
<Button variant='outline' size='sm' onClick={onSignOut} disabled={signOut.isPending}>
|
|
25
|
+
<LogOut data-icon='inline-start' />
|
|
26
|
+
Sign out
|
|
27
|
+
</Button>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import { useSession } from '@/api/auth'
|
|
4
|
+
import { SidebarMenuButton, useSidebar } from '@/components/ui/sidebar'
|
|
5
|
+
|
|
6
|
+
const initialsOf = (email: string) => email.slice(0, 2).toUpperCase()
|
|
7
|
+
|
|
8
|
+
// the account row in the sidebar footer, reading the Better Auth session
|
|
9
|
+
export const SidebarUser = () => {
|
|
10
|
+
const { data: session } = useSession()
|
|
11
|
+
const { isMobile, setOpenMobile } = useSidebar()
|
|
12
|
+
if (!session) return null
|
|
13
|
+
|
|
14
|
+
const { email } = session.user
|
|
15
|
+
return (
|
|
16
|
+
<SidebarMenuButton
|
|
17
|
+
size='lg'
|
|
18
|
+
tooltip={email}
|
|
19
|
+
render={<Link to='/' onClick={() => isMobile && setOpenMobile(false)} />}
|
|
20
|
+
>
|
|
21
|
+
<span
|
|
22
|
+
aria-hidden='true'
|
|
23
|
+
className='flex size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-accent text-xs font-medium text-sidebar-accent-foreground'
|
|
24
|
+
>
|
|
25
|
+
{initialsOf(email)}
|
|
26
|
+
</span>
|
|
27
|
+
<span className='flex min-w-0 flex-col leading-tight'>
|
|
28
|
+
<span className='truncate text-xs text-muted-foreground'>Signed in as</span>
|
|
29
|
+
<span className='truncate font-medium'>{email}</span>
|
|
30
|
+
</span>
|
|
31
|
+
</SidebarMenuButton>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -60,6 +60,20 @@ it('always toasts a failed mutation', async () => {
|
|
|
60
60
|
expect(toasts()[0]).toHaveTextContent('nope')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
it('stays silent for a mutation that shows its own errors', async () => {
|
|
64
|
+
await queryClient
|
|
65
|
+
.getMutationCache()
|
|
66
|
+
.build(queryClient, {
|
|
67
|
+
mutationFn: () => Promise.reject(new EdenFetchError(422, { message: 'Email is taken' })),
|
|
68
|
+
meta: { silentError: true }
|
|
69
|
+
})
|
|
70
|
+
.execute(undefined)
|
|
71
|
+
.catch(() => {})
|
|
72
|
+
await flushToasts()
|
|
73
|
+
|
|
74
|
+
expect(toasts()).toHaveLength(0)
|
|
75
|
+
})
|
|
76
|
+
|
|
63
77
|
it('does not retry a 4xx query', async () => {
|
|
64
78
|
const queryFn = vi.fn(() => Promise.reject(httpError(404)))
|
|
65
79
|
|
|
@@ -68,6 +82,14 @@ it('does not retry a 4xx query', async () => {
|
|
|
68
82
|
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
69
83
|
})
|
|
70
84
|
|
|
85
|
+
it('retries a rate-limited query', async () => {
|
|
86
|
+
const queryFn = vi.fn(() => Promise.reject(httpError(429)))
|
|
87
|
+
|
|
88
|
+
await queryClient.query({ queryKey: ['e'], queryFn, retryDelay: 0 }).catch(() => {})
|
|
89
|
+
|
|
90
|
+
expect(queryFn).toHaveBeenCalledTimes(3)
|
|
91
|
+
})
|
|
92
|
+
|
|
71
93
|
it('retries a 5xx query up to the configured limit', async () => {
|
|
72
94
|
const queryFn = vi.fn(() => Promise.reject(httpError(503)))
|
|
73
95
|
|
|
@@ -3,16 +3,32 @@ import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query'
|
|
|
3
3
|
|
|
4
4
|
import { toast } from '@/components/ui/toast'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const getErrorMessage = (error: unknown) => {
|
|
10
|
-
if (error instanceof EdenFetchError) {
|
|
11
|
-
return (
|
|
12
|
-
(error.value as { message?: string } | null)?.message ?? `Request failed (${error.status})`
|
|
13
|
-
)
|
|
6
|
+
declare module '@tanstack/react-query' {
|
|
7
|
+
interface Register {
|
|
8
|
+
mutationMeta: { silentError?: boolean }
|
|
14
9
|
}
|
|
15
|
-
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 408 and 429 are transient despite being 4xx
|
|
13
|
+
const RETRYABLE_CLIENT_ERRORS = new Set([408, 429])
|
|
14
|
+
|
|
15
|
+
const isFinalError = (error: unknown) => {
|
|
16
|
+
if (!(error instanceof EdenFetchError)) return false
|
|
17
|
+
const { status } = error
|
|
18
|
+
return status >= 400 && status < 500 && !RETRYABLE_CLIENT_ERRORS.has(status)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// the server's `{ message }` beats eden's bare status
|
|
22
|
+
const getErrorMessage = (error: unknown): string => {
|
|
23
|
+
if (!(error instanceof EdenFetchError)) {
|
|
24
|
+
return error instanceof Error && error.message.trim() ? error.message : 'Something went wrong'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const message = (error.value as { message?: string } | null)?.message
|
|
28
|
+
|
|
29
|
+
return typeof message === 'string' && message.trim()
|
|
30
|
+
? message
|
|
31
|
+
: `Request failed (${error.status})`
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
const notifyError = (error: unknown) => {
|
|
@@ -25,11 +41,15 @@ export const queryClient = new QueryClient({
|
|
|
25
41
|
if (query.state.data !== undefined) notifyError(error)
|
|
26
42
|
}
|
|
27
43
|
}),
|
|
28
|
-
mutationCache: new MutationCache({
|
|
44
|
+
mutationCache: new MutationCache({
|
|
45
|
+
onError: (error, _variables, _context, mutation) => {
|
|
46
|
+
if (!mutation.meta?.silentError) notifyError(error)
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
29
49
|
defaultOptions: {
|
|
30
50
|
queries: {
|
|
31
51
|
staleTime: 60_000,
|
|
32
|
-
retry: (failureCount, error) => !
|
|
52
|
+
retry: (failureCount, error) => !isFinalError(error) && failureCount < 2
|
|
33
53
|
},
|
|
34
54
|
mutations: {
|
|
35
55
|
retry: false
|
|
@@ -1,65 +1,30 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
-
import { Trash2 } from 'lucide-react'
|
|
3
|
-
import { useState } from 'react'
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import {
|
|
3
|
+
import { useSession } from '@/api/auth'
|
|
4
|
+
// realtime:start
|
|
5
|
+
import { liveSnapshotQuery } from '@/api/live'
|
|
6
|
+
import { Dashboard } from '@/components/dashboard/dashboard'
|
|
7
|
+
// realtime:end
|
|
8
8
|
|
|
9
9
|
export const Route = createFileRoute('/_authenticated/')({
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// realtime:start
|
|
11
|
+
loader: ({ context }) => context.queryClient.query({ ...liveSnapshotQuery, staleTime: 'static' }),
|
|
12
|
+
// realtime:end
|
|
13
|
+
component: HomePage
|
|
12
14
|
})
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const notes = useNotes()
|
|
17
|
-
const createNote = useCreateNote()
|
|
18
|
-
const deleteNote = useDeleteNote()
|
|
19
|
-
const [title, setTitle] = useState('')
|
|
20
|
-
|
|
21
|
-
const onSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
|
22
|
-
e.preventDefault()
|
|
23
|
-
const trimmed = title.trim()
|
|
24
|
-
if (!trimmed) return
|
|
25
|
-
createNote.mutate(trimmed, { onSuccess: () => setTitle('') })
|
|
26
|
-
}
|
|
16
|
+
function HomePage() {
|
|
17
|
+
const { data: session } = useSession()
|
|
27
18
|
|
|
28
19
|
return (
|
|
29
|
-
<main className='
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
onChange={e => setTitle(e.target.value)}
|
|
38
|
-
maxLength={200}
|
|
39
|
-
/>
|
|
40
|
-
<Button type='submit' disabled={createNote.isPending}>
|
|
41
|
-
Add
|
|
42
|
-
</Button>
|
|
43
|
-
</form>
|
|
44
|
-
|
|
45
|
-
{notes.data?.length === 0 && <p className='text-muted-foreground text-sm'>No notes yet.</p>}
|
|
46
|
-
|
|
47
|
-
<ul className='divide-y rounded-lg border'>
|
|
48
|
-
{notes.data?.map(note => (
|
|
49
|
-
<li key={note.id} className='flex items-center justify-between px-3 py-2'>
|
|
50
|
-
<span className='text-sm'>{note.title}</span>
|
|
51
|
-
<Button
|
|
52
|
-
variant='ghost'
|
|
53
|
-
size='icon-sm'
|
|
54
|
-
aria-label={`Delete ${note.title}`}
|
|
55
|
-
disabled={deleteNote.isPending}
|
|
56
|
-
onClick={() => deleteNote.mutate(note.id)}
|
|
57
|
-
>
|
|
58
|
-
<Trash2 />
|
|
59
|
-
</Button>
|
|
60
|
-
</li>
|
|
61
|
-
))}
|
|
62
|
-
</ul>
|
|
20
|
+
<main className='flex flex-1 flex-col gap-6'>
|
|
21
|
+
<div className='flex flex-col gap-1'>
|
|
22
|
+
<h1 className='text-2xl font-semibold'>PLACEHOLDER</h1>
|
|
23
|
+
<p className='text-sm text-muted-foreground'>Signed in as {session?.user.email}</p>
|
|
24
|
+
</div>
|
|
25
|
+
{/* realtime:start */}
|
|
26
|
+
<Dashboard />
|
|
27
|
+
{/* realtime:end */}
|
|
63
28
|
</main>
|
|
64
29
|
)
|
|
65
30
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { Trash2 } from 'lucide-react'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useCreateNote, useDeleteNote, useNotes, notesQuery } from '@/api/notes'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Input } from '@/components/ui/input'
|
|
8
|
+
|
|
9
|
+
export const Route = createFileRoute('/_authenticated/notes/')({
|
|
10
|
+
loader: ({ context }) => context.queryClient.query({ ...notesQuery, staleTime: 'static' }),
|
|
11
|
+
component: NotesPage
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// demo of the Drizzle → Elysia → Eden → React Query path; delete with the notes modules
|
|
15
|
+
function NotesPage() {
|
|
16
|
+
const notes = useNotes()
|
|
17
|
+
const createNote = useCreateNote()
|
|
18
|
+
const deleteNote = useDeleteNote()
|
|
19
|
+
const [title, setTitle] = useState('')
|
|
20
|
+
|
|
21
|
+
const onSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
const trimmed = title.trim()
|
|
24
|
+
if (!trimmed) return
|
|
25
|
+
createNote.mutate(trimmed, { onSuccess: () => setTitle('') })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<main className='mx-auto w-full max-w-md space-y-4'>
|
|
30
|
+
<h1 className='text-2xl font-semibold'>Notes</h1>
|
|
31
|
+
|
|
32
|
+
<form onSubmit={onSubmit} className='flex gap-2'>
|
|
33
|
+
<Input
|
|
34
|
+
aria-label='New note'
|
|
35
|
+
placeholder='New note…'
|
|
36
|
+
value={title}
|
|
37
|
+
onChange={e => setTitle(e.target.value)}
|
|
38
|
+
maxLength={200}
|
|
39
|
+
/>
|
|
40
|
+
<Button type='submit' disabled={createNote.isPending}>
|
|
41
|
+
Add
|
|
42
|
+
</Button>
|
|
43
|
+
</form>
|
|
44
|
+
|
|
45
|
+
{notes.data?.length === 0 && <p className='text-sm text-muted-foreground'>No notes yet.</p>}
|
|
46
|
+
|
|
47
|
+
<ul className='divide-y rounded-lg border'>
|
|
48
|
+
{notes.data?.map(note => (
|
|
49
|
+
<li key={note.id} className='flex items-center justify-between px-3 py-2'>
|
|
50
|
+
<span className='text-sm'>{note.title}</span>
|
|
51
|
+
<Button
|
|
52
|
+
variant='ghost'
|
|
53
|
+
size='icon-sm'
|
|
54
|
+
aria-label={`Delete ${note.title}`}
|
|
55
|
+
disabled={deleteNote.isPending}
|
|
56
|
+
onClick={() => deleteNote.mutate(note.id)}
|
|
57
|
+
>
|
|
58
|
+
<Trash2 />
|
|
59
|
+
</Button>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
</main>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -25,7 +25,7 @@ function AuthenticatedLayout() {
|
|
|
25
25
|
<>
|
|
26
26
|
<header className='flex h-14 items-center justify-between border-b px-6'>
|
|
27
27
|
<div className='flex items-center gap-4'>
|
|
28
|
-
<span className='text-muted-foreground
|
|
28
|
+
<span className='text-sm text-muted-foreground'>{session?.user.email}</span>
|
|
29
29
|
<ThemeToggle />
|
|
30
30
|
<Button variant='outline' size='sm' onClick={onSignOut} disabled={signOut.isPending}>
|
|
31
31
|
<LogOut data-icon='inline-start' />
|
|
@@ -16,13 +16,26 @@ const SignInFormSchema = z.object({
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
const FIELDS = [
|
|
19
|
-
{
|
|
20
|
-
|
|
19
|
+
{
|
|
20
|
+
name: 'email',
|
|
21
|
+
label: 'Email',
|
|
22
|
+
type: 'email',
|
|
23
|
+
autoComplete: 'email',
|
|
24
|
+
placeholder: 'you@example.com'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'password',
|
|
28
|
+
label: 'Password',
|
|
29
|
+
type: 'password',
|
|
30
|
+
autoComplete: 'current-password',
|
|
31
|
+
placeholder: '••••••••'
|
|
32
|
+
}
|
|
21
33
|
] as const satisfies ReadonlyArray<{
|
|
22
34
|
name: keyof SignInInput
|
|
23
35
|
label: string
|
|
24
36
|
type: string
|
|
25
37
|
autoComplete: string
|
|
38
|
+
placeholder: string
|
|
26
39
|
}>
|
|
27
40
|
|
|
28
41
|
export const Route = createFileRoute('/sign-in')({
|
|
@@ -61,6 +74,7 @@ function SignInPage() {
|
|
|
61
74
|
id={field.name}
|
|
62
75
|
type={field.type}
|
|
63
76
|
autoComplete={field.autoComplete}
|
|
77
|
+
placeholder={field.placeholder}
|
|
64
78
|
aria-invalid={Boolean(error)}
|
|
65
79
|
aria-describedby={error ? `${field.name}-error` : undefined}
|
|
66
80
|
{...register(field.name)}
|
|
@@ -74,7 +88,7 @@ function SignInPage() {
|
|
|
74
88
|
{formState.isSubmitting ? 'Signing in…' : 'Sign in'}
|
|
75
89
|
</Button>
|
|
76
90
|
|
|
77
|
-
<p className='text-
|
|
91
|
+
<p className='text-center text-sm text-muted-foreground'>
|
|
78
92
|
No account?{' '}
|
|
79
93
|
<Link to='/sign-up' search={{ redirect: redirectTo }} className='underline'>
|
|
80
94
|
Sign up
|
|
@@ -18,14 +18,27 @@ const SignUpFormSchema = z.object({
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
const FIELDS = [
|
|
21
|
-
{ name: 'name', label: 'Name', type: 'text', autoComplete: 'name' },
|
|
22
|
-
{
|
|
23
|
-
|
|
21
|
+
{ name: 'name', label: 'Name', type: 'text', autoComplete: 'name', placeholder: 'Ada Lovelace' },
|
|
22
|
+
{
|
|
23
|
+
name: 'email',
|
|
24
|
+
label: 'Email',
|
|
25
|
+
type: 'email',
|
|
26
|
+
autoComplete: 'email',
|
|
27
|
+
placeholder: 'you@example.com'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'password',
|
|
31
|
+
label: 'Password',
|
|
32
|
+
type: 'password',
|
|
33
|
+
autoComplete: 'new-password',
|
|
34
|
+
placeholder: 'At least 8 characters'
|
|
35
|
+
}
|
|
24
36
|
] as const satisfies ReadonlyArray<{
|
|
25
37
|
name: keyof SignUpInput
|
|
26
38
|
label: string
|
|
27
39
|
type: string
|
|
28
40
|
autoComplete: string
|
|
41
|
+
placeholder: string
|
|
29
42
|
}>
|
|
30
43
|
|
|
31
44
|
export const Route = createFileRoute('/sign-up')({
|
|
@@ -64,6 +77,7 @@ function SignUpPage() {
|
|
|
64
77
|
id={field.name}
|
|
65
78
|
type={field.type}
|
|
66
79
|
autoComplete={field.autoComplete}
|
|
80
|
+
placeholder={field.placeholder}
|
|
67
81
|
aria-invalid={Boolean(error)}
|
|
68
82
|
aria-describedby={error ? `${field.name}-error` : undefined}
|
|
69
83
|
{...register(field.name)}
|
|
@@ -77,7 +91,7 @@ function SignUpPage() {
|
|
|
77
91
|
{formState.isSubmitting ? 'Creating…' : 'Sign up'}
|
|
78
92
|
</Button>
|
|
79
93
|
|
|
80
|
-
<p className='text-
|
|
94
|
+
<p className='text-center text-sm text-muted-foreground'>
|
|
81
95
|
Already have an account?{' '}
|
|
82
96
|
<Link to='/sign-in' search={{ redirect: redirectTo }} className='underline'>
|
|
83
97
|
Sign in
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Start override: the SSR runtime has no `window`, so the origin cannot be read there.
|
|
2
|
-
import { treaty } from '@elysia/eden'
|
|
2
|
+
import { EdenFetchError, treaty } from '@elysia/eden'
|
|
3
3
|
|
|
4
4
|
import type { App } from '@repo/api'
|
|
5
5
|
|
|
@@ -15,3 +15,13 @@ export const api = treaty<App>(baseUrl, {
|
|
|
15
15
|
fetch: { credentials: 'include' },
|
|
16
16
|
throwHttpError: true
|
|
17
17
|
}).api // the server's `/api` group
|
|
18
|
+
|
|
19
|
+
// route loaders turn this into `notFound()`; every variant of `api` exports the same helper
|
|
20
|
+
export const isNotFound = (error: unknown) =>
|
|
21
|
+
error instanceof EdenFetchError && error.status === 404
|
|
22
|
+
|
|
23
|
+
// `throwHttpError` already turns failures into exceptions, so a null body is a contract break
|
|
24
|
+
export const unwrap = <T>({ data }: { data: T | null }): T => {
|
|
25
|
+
if (data === null) throw new Error('Empty response')
|
|
26
|
+
return data
|
|
27
|
+
}
|