@pylonsync/create-pylon 0.19.0 → 0.20.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/bin/create-pylon.js +30 -7
- package/package.json +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/README.md +28 -3
- package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
- package/templates/agency/app/(marketing)/page.tsx +3 -3
- package/templates/agency/app/(marketing)/work/page.tsx +3 -3
- package/templates/agency/app/auth-form.tsx +4 -4
- package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
- package/templates/agency/app/dashboard/page.tsx +14 -16
- package/templates/agency/app/icon.svg +3 -0
- package/templates/agency/app/login/page.tsx +12 -14
- package/templates/agency/components/marketing.tsx +12 -5
- package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
- package/templates/agency/lib/agency.ts +22 -0
- package/templates/agency/lib/demo.ts +118 -0
- package/templates/agency/lib/site.config.ts +19 -13
- package/templates/agency/public/favicon.ico +0 -0
- package/templates/agency/tests/agency.test.ts +57 -0
- package/templates/ai-chat/.env.example +13 -27
- package/templates/ai-chat/README.md +53 -60
- package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
- package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
- package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
- package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
- package/templates/ai-chat/app/(chat)/page.tsx +7 -11
- package/templates/ai-chat/app/auth-form.tsx +45 -38
- package/templates/ai-chat/app/error.tsx +9 -12
- package/templates/ai-chat/app/globals.css +464 -102
- package/templates/ai-chat/app/layout.tsx +11 -19
- package/templates/ai-chat/app/login/page.tsx +39 -24
- package/templates/ai-chat/app.ts +118 -43
- package/templates/ai-chat/components/chat/composer.tsx +193 -0
- package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
- package/templates/ai-chat/components/chat/icons.tsx +198 -0
- package/templates/ai-chat/components/chat/markdown.tsx +164 -0
- package/templates/ai-chat/components/chat/message.tsx +183 -0
- package/templates/ai-chat/components/chat/setup.tsx +71 -0
- package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
- package/templates/ai-chat/functions/_beginTurn.ts +145 -0
- package/templates/ai-chat/functions/_finishTurn.ts +55 -0
- package/templates/ai-chat/functions/assistantStatus.ts +13 -0
- package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
- package/templates/ai-chat/functions/createConversation.ts +28 -0
- package/templates/ai-chat/functions/deleteConversation.ts +15 -20
- package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
- package/templates/ai-chat/functions/renameConversation.ts +19 -0
- package/templates/ai-chat/functions/respond.ts +88 -0
- package/templates/ai-chat/functions/seedExamples.ts +46 -0
- package/templates/ai-chat/functions/stopResponse.ts +30 -0
- package/templates/ai-chat/lib/brand-icon.ts +7 -0
- package/templates/ai-chat/lib/chat.ts +277 -0
- package/templates/ai-chat/lib/claim.ts +15 -0
- package/templates/ai-chat/lib/examples.ts +179 -0
- package/templates/ai-chat/lib/highlight.ts +107 -0
- package/templates/ai-chat/lib/markdown.ts +317 -0
- package/templates/ai-chat/lib/provider.ts +38 -0
- package/templates/ai-chat/lib/site.config.ts +55 -39
- package/templates/ai-chat/tests/chat.test.ts +174 -0
- package/templates/ai-chat/tests/components.test.tsx +57 -0
- package/templates/ai-chat/tests/markdown.test.ts +88 -0
- package/templates/ai-chat/tests/provider.test.ts +32 -0
- package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
- package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
- package/templates/backend/consumer/apps/api/app.ts +166 -0
- package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
- package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
- package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
- package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
- package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
- package/templates/backend/consumer/apps/api/package.json +8 -2
- package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
- package/templates/chat/README.md +43 -21
- package/templates/chat/app/chat-client.tsx +495 -85
- package/templates/chat/app/error.tsx +1 -1
- package/templates/chat/app/globals.css +170 -54
- package/templates/chat/app/layout.tsx +12 -9
- package/templates/chat/app/not-found.tsx +4 -2
- package/templates/chat/app/page.tsx +9 -19
- package/templates/chat/app.ts +96 -17
- package/templates/chat/components/chat/avatar.tsx +45 -0
- package/templates/chat/components/chat/composer.tsx +110 -0
- package/templates/chat/components/chat/message-list.tsx +285 -0
- package/templates/chat/components/chat/sidebar.tsx +190 -0
- package/templates/chat/functions/joinChat.ts +83 -0
- package/templates/chat/functions/renameMember.ts +25 -0
- package/templates/chat/lib/chat.ts +188 -0
- package/templates/chat/lib/seed.ts +78 -0
- package/templates/chat/tests/chat.test.ts +159 -0
- package/templates/chat/tests/composer.test.tsx +54 -0
- package/templates/consumer/README.md +3 -3
- package/templates/consumer/app/globals.css +10 -3
- package/templates/consumer/app/icon.svg +1 -0
- package/templates/consumer/components/social/avatar.tsx +25 -11
- package/templates/consumer/functions/seedFeed.ts +3 -3
- package/templates/consumer/lib/seed.ts +43 -85
- package/templates/consumer/public/favicon.ico +0 -0
- package/templates/consumer/public/images/posts/arch.jpg +0 -0
- package/templates/consumer/public/images/posts/coffee.jpg +0 -0
- package/templates/consumer/public/images/posts/dog.jpg +0 -0
- package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
- package/templates/consumer/public/images/posts/ramen.jpg +0 -0
- package/templates/consumer/public/images/posts/run.jpg +0 -0
- package/templates/consumer/public/images/posts/sunset.jpg +0 -0
- package/templates/consumer/tests/seed.test.ts +39 -0
- package/templates/creator/.env.example +7 -0
- package/templates/creator/README.md +24 -5
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
- package/templates/creator/app/auth-form.tsx +7 -7
- package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
- package/templates/creator/app/dashboard/page.tsx +18 -18
- package/templates/creator/app/icon.svg +2 -0
- package/templates/creator/app/login/page.tsx +14 -14
- package/templates/creator/app.ts +4 -4
- package/templates/creator/functions/seedDemo.ts +32 -0
- package/templates/creator/functions/subscribe.ts +4 -15
- package/templates/creator/lib/demo.ts +94 -0
- package/templates/creator/lib/site.config.ts +15 -10
- package/templates/creator/lib/stats.ts +35 -0
- package/templates/creator/public/favicon.ico +0 -0
- package/templates/creator/tests/counter.test.ts +92 -0
- package/templates/creator/tests/live-count.test.tsx +45 -0
- package/templates/crm/README.md +22 -1
- package/templates/crm/app/apple-icon.png +0 -0
- package/templates/crm/app/companies/companies-view.tsx +32 -14
- package/templates/crm/app/companies/page.tsx +2 -1
- package/templates/crm/app/contacts/contacts-view.tsx +34 -10
- package/templates/crm/app/contacts/page.tsx +2 -1
- package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
- package/templates/crm/app/deals/[id]/page.tsx +2 -1
- package/templates/crm/app/globals.css +29 -14
- package/templates/crm/app/icon.svg +1 -0
- package/templates/crm/app/layout.tsx +7 -5
- package/templates/crm/app/page.tsx +2 -1
- package/templates/crm/app/pipeline-view.tsx +5 -5
- package/templates/crm/app/workspace.tsx +29 -17
- package/templates/crm/app.ts +5 -5
- package/templates/crm/components/activity-timeline.tsx +14 -4
- package/templates/crm/components/app-shell.tsx +94 -0
- package/templates/crm/components/auth-form.tsx +35 -5
- package/templates/crm/components/avatar.tsx +5 -1
- package/templates/crm/components/board-skeleton.tsx +5 -5
- package/templates/crm/components/brand-mark.tsx +25 -0
- package/templates/crm/components/command-palette.tsx +17 -17
- package/templates/crm/components/data-table.tsx +97 -36
- package/templates/crm/components/deal-card.tsx +23 -27
- package/templates/crm/components/filter-tabs.tsx +101 -0
- package/templates/crm/components/metrics-bar.tsx +6 -6
- package/templates/crm/components/nav.tsx +15 -0
- package/templates/crm/components/page-header.tsx +43 -7
- package/templates/crm/components/pipeline-board.tsx +16 -10
- package/templates/crm/components/require-auth.tsx +24 -26
- package/templates/crm/components/sidebar.tsx +54 -49
- package/templates/crm/components/stage-badge.tsx +4 -2
- package/templates/crm/components/ui/button.tsx +5 -4
- package/templates/crm/components/ui/dialog.tsx +1 -1
- package/templates/crm/components/ui/input.tsx +1 -1
- package/templates/crm/components/ui/select.tsx +1 -1
- package/templates/crm/components/ui/textarea.tsx +1 -1
- package/templates/crm/functions/seedWorkspace.ts +32 -17
- package/templates/crm/lib/brand.ts +9 -0
- package/templates/crm/lib/demo-team.ts +51 -0
- package/templates/crm/lib/pipeline.ts +37 -0
- package/templates/crm/lib/search.ts +4 -3
- package/templates/crm/lib/seed.ts +53 -8
- package/templates/crm/public/favicon.ico +0 -0
- package/templates/crm/tests/components.test.tsx +5 -5
- package/templates/crm/tests/demo-team.test.ts +18 -0
- package/templates/crm/tests/pipeline.test.ts +59 -3
- package/templates/directory/.env.example +7 -0
- package/templates/directory/README.md +22 -3
- package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
- package/templates/directory/app/(marketing)/page.tsx +6 -4
- package/templates/directory/app/auth-form.tsx +4 -4
- package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
- package/templates/directory/app/dashboard/page.tsx +16 -14
- package/templates/directory/app/icon.svg +4 -0
- package/templates/directory/app/login/page.tsx +16 -15
- package/templates/directory/functions/seedDemo.ts +43 -0
- package/templates/directory/lib/demo.ts +145 -0
- package/templates/directory/lib/directory.ts +27 -0
- package/templates/directory/lib/site.config.ts +21 -16
- package/templates/directory/public/favicon.ico +0 -0
- package/templates/directory/tests/directory.test.ts +46 -0
- package/templates/expo/consumer/apps/expo/App.tsx +360 -288
- package/templates/expo/consumer/apps/expo/app.json +9 -1
- package/templates/expo/consumer/apps/expo/package.json +2 -1
- package/templates/helpdesk/README.md +21 -0
- package/templates/helpdesk/app/apple-icon.png +0 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
- package/templates/helpdesk/app/customers/page.tsx +2 -1
- package/templates/helpdesk/app/globals.css +29 -14
- package/templates/helpdesk/app/icon.svg +1 -0
- package/templates/helpdesk/app/inbox-view.tsx +66 -47
- package/templates/helpdesk/app/layout.tsx +7 -5
- package/templates/helpdesk/app/page.tsx +4 -7
- package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
- package/templates/helpdesk/app/workspace.tsx +33 -22
- package/templates/helpdesk/app.ts +2 -2
- package/templates/helpdesk/components/app-shell.tsx +94 -0
- package/templates/helpdesk/components/auth-form.tsx +35 -5
- package/templates/helpdesk/components/avatar.tsx +5 -1
- package/templates/helpdesk/components/brand-mark.tsx +28 -0
- package/templates/helpdesk/components/command-palette.tsx +17 -17
- package/templates/helpdesk/components/data-table.tsx +97 -36
- package/templates/helpdesk/components/filter-tabs.tsx +101 -0
- package/templates/helpdesk/components/nav.tsx +14 -0
- package/templates/helpdesk/components/page-header.tsx +43 -7
- package/templates/helpdesk/components/priority-badge.tsx +49 -6
- package/templates/helpdesk/components/require-auth.tsx +24 -26
- package/templates/helpdesk/components/sidebar.tsx +54 -48
- package/templates/helpdesk/components/sla-indicator.tsx +5 -8
- package/templates/helpdesk/components/thread.tsx +7 -7
- package/templates/helpdesk/components/ticket-list.tsx +79 -19
- package/templates/helpdesk/components/ui/button.tsx +5 -4
- package/templates/helpdesk/components/ui/dialog.tsx +1 -1
- package/templates/helpdesk/components/ui/input.tsx +1 -1
- package/templates/helpdesk/components/ui/select.tsx +1 -1
- package/templates/helpdesk/components/ui/textarea.tsx +1 -1
- package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
- package/templates/helpdesk/lib/brand.ts +9 -0
- package/templates/helpdesk/lib/demo-team.ts +51 -0
- package/templates/helpdesk/lib/search.ts +4 -3
- package/templates/helpdesk/lib/seed.ts +43 -2
- package/templates/helpdesk/lib/tickets.ts +68 -20
- package/templates/helpdesk/public/favicon.ico +0 -0
- package/templates/helpdesk/tests/components.test.tsx +10 -0
- package/templates/helpdesk/tests/demo-team.test.ts +18 -0
- package/templates/helpdesk/tests/tickets.test.ts +70 -0
- package/templates/imessage/.env.example +49 -0
- package/templates/imessage/AGENTS.md +153 -0
- package/templates/imessage/CLAUDE.md +7 -0
- package/templates/imessage/README.md +187 -0
- package/templates/imessage/app/console.tsx +382 -0
- package/templates/imessage/app/contacts/page.tsx +13 -0
- package/templates/imessage/app/error.tsx +16 -0
- package/templates/imessage/app/gate.tsx +31 -0
- package/templates/imessage/app/globals.css +183 -0
- package/templates/imessage/app/layout.tsx +17 -0
- package/templates/imessage/app/login/login-form.tsx +115 -0
- package/templates/imessage/app/login/page.tsx +30 -0
- package/templates/imessage/app/not-found.tsx +13 -0
- package/templates/imessage/app/page.tsx +14 -0
- package/templates/imessage/app/robots.ts +6 -0
- package/templates/imessage/app/setup/page.tsx +13 -0
- package/templates/imessage/app/sign-out.tsx +20 -0
- package/templates/imessage/app.ts +299 -0
- package/templates/imessage/bunfig +4 -0
- package/templates/imessage/components/avatar.tsx +46 -0
- package/templates/imessage/components/contact-panel.tsx +263 -0
- package/templates/imessage/components/contacts-view.tsx +172 -0
- package/templates/imessage/components/conversation-list.tsx +129 -0
- package/templates/imessage/components/nav-rail.tsx +67 -0
- package/templates/imessage/components/setup-view.tsx +408 -0
- package/templates/imessage/components/thread.tsx +229 -0
- package/templates/imessage/components/ui/badge.tsx +37 -0
- package/templates/imessage/components/ui/button.tsx +56 -0
- package/templates/imessage/components/ui/card.tsx +90 -0
- package/templates/imessage/components/ui/dialog.tsx +110 -0
- package/templates/imessage/components/ui/input.tsx +30 -0
- package/templates/imessage/components/ui/label.tsx +21 -0
- package/templates/imessage/components/ui/select.tsx +39 -0
- package/templates/imessage/components/ui/table.tsx +124 -0
- package/templates/imessage/components/ui/textarea.tsx +24 -0
- package/templates/imessage/components.json +20 -0
- package/templates/imessage/functions/answerNow.ts +34 -0
- package/templates/imessage/functions/assistant.ts +96 -0
- package/templates/imessage/functions/beginTurn.ts +101 -0
- package/templates/imessage/functions/cancelReminder.ts +13 -0
- package/templates/imessage/functions/claimOutbound.ts +44 -0
- package/templates/imessage/functions/clearDemo.ts +18 -0
- package/templates/imessage/functions/deleteNote.ts +11 -0
- package/templates/imessage/functions/deliverOutbound.ts +55 -0
- package/templates/imessage/functions/finishTurn.ts +84 -0
- package/templates/imessage/functions/fireReminder.ts +42 -0
- package/templates/imessage/functions/ingestInbound.ts +22 -0
- package/templates/imessage/functions/ownerAccount.ts +13 -0
- package/templates/imessage/functions/processTurn.ts +74 -0
- package/templates/imessage/functions/relayApply.ts +102 -0
- package/templates/imessage/functions/relaySync.ts +63 -0
- package/templates/imessage/functions/releaseOutbound.ts +18 -0
- package/templates/imessage/functions/resolveTurn.ts +25 -0
- package/templates/imessage/functions/saveContact.ts +27 -0
- package/templates/imessage/functions/seedDemo.ts +70 -0
- package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
- package/templates/imessage/functions/sendblueWebhook.ts +49 -0
- package/templates/imessage/functions/setContactStatus.ts +17 -0
- package/templates/imessage/functions/settleOutbound.ts +35 -0
- package/templates/imessage/functions/setupStatus.ts +33 -0
- package/templates/imessage/functions/sweep.ts +58 -0
- package/templates/imessage/functions/toolCancelReminder.ts +17 -0
- package/templates/imessage/functions/toolForgetNote.ts +15 -0
- package/templates/imessage/functions/toolListNotes.ts +16 -0
- package/templates/imessage/functions/toolListReminders.ts +17 -0
- package/templates/imessage/functions/toolSaveNote.ts +22 -0
- package/templates/imessage/functions/toolSetReminder.ts +55 -0
- package/templates/imessage/functions/updateSettings.ts +48 -0
- package/templates/imessage/gitignore +11 -0
- package/templates/imessage/lib/agent-call.ts +251 -0
- package/templates/imessage/lib/assistant.ts +74 -0
- package/templates/imessage/lib/chunk.ts +135 -0
- package/templates/imessage/lib/demo.ts +68 -0
- package/templates/imessage/lib/format.ts +170 -0
- package/templates/imessage/lib/gate.ts +104 -0
- package/templates/imessage/lib/handles.ts +67 -0
- package/templates/imessage/lib/provider.ts +38 -0
- package/templates/imessage/lib/relay-protocol.ts +95 -0
- package/templates/imessage/lib/reminders.ts +41 -0
- package/templates/imessage/lib/secrets.ts +59 -0
- package/templates/imessage/lib/sendblue.ts +173 -0
- package/templates/imessage/lib/store.ts +407 -0
- package/templates/imessage/lib/transport.ts +135 -0
- package/templates/imessage/lib/utils.ts +10 -0
- package/templates/imessage/package.json +40 -0
- package/templates/imessage/relay/.env.example +14 -0
- package/templates/imessage/relay/applescript.ts +70 -0
- package/templates/imessage/relay/chatdb.ts +165 -0
- package/templates/imessage/relay/client.ts +41 -0
- package/templates/imessage/relay/main.ts +137 -0
- package/templates/imessage/relay/state.ts +35 -0
- package/templates/imessage/tests/agent-call.test.ts +112 -0
- package/templates/imessage/tests/applescript.test.ts +88 -0
- package/templates/imessage/tests/chatdb.test.ts +139 -0
- package/templates/imessage/tests/chunk.test.ts +61 -0
- package/templates/imessage/tests/fake-ctx.ts +88 -0
- package/templates/imessage/tests/ingest.test.ts +141 -0
- package/templates/imessage/tests/lib.test.ts +99 -0
- package/templates/imessage/tests/review-fixes.test.ts +138 -0
- package/templates/imessage/tests/secrets.test.ts +36 -0
- package/templates/imessage/tests/sendblue.test.ts +143 -0
- package/templates/imessage/tests/setup.ts +5 -0
- package/templates/imessage/tsconfig.json +18 -0
- package/templates/inventory/README.md +21 -0
- package/templates/inventory/app/apple-icon.png +0 -0
- package/templates/inventory/app/globals.css +29 -14
- package/templates/inventory/app/icon.svg +1 -0
- package/templates/inventory/app/layout.tsx +7 -5
- package/templates/inventory/app/movements/movements-view.tsx +32 -10
- package/templates/inventory/app/movements/page.tsx +2 -1
- package/templates/inventory/app/page.tsx +4 -6
- package/templates/inventory/app/products/[id]/page.tsx +2 -1
- package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
- package/templates/inventory/app/products-view.tsx +74 -33
- package/templates/inventory/app/workspace.tsx +34 -16
- package/templates/inventory/app.ts +10 -2
- package/templates/inventory/components/app-shell.tsx +94 -0
- package/templates/inventory/components/auth-form.tsx +35 -5
- package/templates/inventory/components/avatar.tsx +5 -1
- package/templates/inventory/components/brand-mark.tsx +25 -0
- package/templates/inventory/components/category-tile.tsx +49 -0
- package/templates/inventory/components/command-palette.tsx +17 -17
- package/templates/inventory/components/data-table.tsx +97 -36
- package/templates/inventory/components/filter-tabs.tsx +101 -0
- package/templates/inventory/components/nav.tsx +14 -0
- package/templates/inventory/components/page-header.tsx +43 -7
- package/templates/inventory/components/require-auth.tsx +24 -26
- package/templates/inventory/components/sidebar.tsx +54 -48
- package/templates/inventory/components/stock-level.tsx +30 -14
- package/templates/inventory/components/summary-bar.tsx +8 -15
- package/templates/inventory/components/ui/button.tsx +5 -4
- package/templates/inventory/components/ui/dialog.tsx +1 -1
- package/templates/inventory/components/ui/input.tsx +1 -1
- package/templates/inventory/components/ui/select.tsx +1 -1
- package/templates/inventory/components/ui/textarea.tsx +1 -1
- package/templates/inventory/functions/seedWorkspace.ts +18 -8
- package/templates/inventory/lib/brand.ts +9 -0
- package/templates/inventory/lib/demo-team.ts +51 -0
- package/templates/inventory/lib/search.ts +4 -3
- package/templates/inventory/lib/seed.ts +29 -2
- package/templates/inventory/lib/stock.ts +32 -0
- package/templates/inventory/public/favicon.ico +0 -0
- package/templates/inventory/tests/demo-team.test.ts +18 -0
- package/templates/inventory/tests/stock.test.ts +29 -0
- package/templates/invoices/README.md +16 -0
- package/templates/invoices/app/apple-icon.png +0 -0
- package/templates/invoices/app/clients/clients-view.tsx +18 -6
- package/templates/invoices/app/clients/page.tsx +2 -1
- package/templates/invoices/app/globals.css +29 -14
- package/templates/invoices/app/icon.svg +1 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
- package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
- package/templates/invoices/app/invoices-view.tsx +81 -32
- package/templates/invoices/app/layout.tsx +7 -5
- package/templates/invoices/app/page.tsx +4 -6
- package/templates/invoices/app/workspace.tsx +26 -16
- package/templates/invoices/app.ts +2 -2
- package/templates/invoices/components/app-shell.tsx +94 -0
- package/templates/invoices/components/auth-form.tsx +35 -5
- package/templates/invoices/components/avatar.tsx +5 -1
- package/templates/invoices/components/brand-mark.tsx +26 -0
- package/templates/invoices/components/command-palette.tsx +17 -17
- package/templates/invoices/components/data-table.tsx +97 -36
- package/templates/invoices/components/filter-tabs.tsx +101 -0
- package/templates/invoices/components/line-items.tsx +19 -14
- package/templates/invoices/components/nav.tsx +14 -0
- package/templates/invoices/components/page-header.tsx +43 -7
- package/templates/invoices/components/require-auth.tsx +24 -26
- package/templates/invoices/components/sidebar.tsx +54 -48
- package/templates/invoices/components/status-badge.tsx +7 -7
- package/templates/invoices/components/summary-bar.tsx +5 -5
- package/templates/invoices/components/ui/button.tsx +5 -4
- package/templates/invoices/components/ui/dialog.tsx +1 -1
- package/templates/invoices/components/ui/input.tsx +1 -1
- package/templates/invoices/components/ui/select.tsx +1 -1
- package/templates/invoices/components/ui/textarea.tsx +1 -1
- package/templates/invoices/functions/recordPayment.ts +3 -0
- package/templates/invoices/lib/billing.ts +40 -0
- package/templates/invoices/lib/brand.ts +9 -0
- package/templates/invoices/lib/search.ts +4 -3
- package/templates/invoices/public/favicon.ico +0 -0
- package/templates/invoices/tests/billing.test.ts +27 -0
- package/templates/ios/consumer/apps/ios/Package.swift +4 -3
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
- package/templates/ios/consumer/apps/ios/project.yml +13 -9
- package/templates/local-service/.env.example +7 -0
- package/templates/local-service/README.md +26 -2
- package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
- package/templates/local-service/app/(marketing)/page.tsx +66 -42
- package/templates/local-service/app/auth-form.tsx +8 -8
- package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
- package/templates/local-service/app/dashboard/page.tsx +14 -14
- package/templates/local-service/app/icon.svg +2 -0
- package/templates/local-service/app/login/page.tsx +15 -15
- package/templates/local-service/functions/seedDemo.ts +49 -0
- package/templates/local-service/lib/demo.ts +135 -0
- package/templates/local-service/lib/hours.ts +44 -0
- package/templates/local-service/lib/site.config.ts +8 -3
- package/templates/local-service/lib/slots.ts +3 -0
- package/templates/local-service/public/favicon.ico +0 -0
- package/templates/local-service/tests/booking.test.ts +104 -0
- package/templates/marketplace/README.md +23 -15
- package/templates/marketplace/app/globals.css +172 -114
- package/templates/marketplace/app/layout.tsx +75 -30
- package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
- package/templates/marketplace/app/me/page.tsx +4 -4
- package/templates/marketplace/app/page.tsx +221 -298
- package/templates/marketplace/app/sell/page.tsx +8 -11
- package/templates/marketplace/app.ts +32 -7
- package/templates/marketplace/client/ActivityFeed.tsx +311 -0
- package/templates/marketplace/client/AuthNav.tsx +24 -16
- package/templates/marketplace/client/ListingCard.tsx +118 -0
- package/templates/marketplace/client/LiveGrid.tsx +92 -0
- package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
- package/templates/marketplace/client/LoginCard.tsx +114 -136
- package/templates/marketplace/client/MarketProvider.tsx +3 -5
- package/templates/marketplace/client/MyMarket.tsx +371 -185
- package/templates/marketplace/client/OfferPanel.tsx +421 -287
- package/templates/marketplace/client/SellForm.tsx +271 -192
- package/templates/marketplace/client/ThemeToggle.tsx +1 -0
- package/templates/marketplace/client/WatchButton.tsx +16 -16
- package/templates/marketplace/client/market.ts +61 -14
- package/templates/marketplace/functions/seedMarket.ts +143 -36
- package/templates/marketplace/functions/seedOffers.ts +101 -0
- package/templates/marketplace/lib/catalog.ts +72 -1
- package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
- package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
- package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
- package/templates/marketplace/public/images/listings/watch.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +64 -2
- package/templates/marketplace/tests/listing-card.test.tsx +61 -0
- package/templates/projects/README.md +24 -3
- package/templates/projects/app/apple-icon.png +0 -0
- package/templates/projects/app/clients/clients-view.tsx +20 -6
- package/templates/projects/app/clients/page.tsx +2 -1
- package/templates/projects/app/globals.css +29 -14
- package/templates/projects/app/icon.svg +1 -0
- package/templates/projects/app/layout.tsx +7 -5
- package/templates/projects/app/page.tsx +3 -5
- package/templates/projects/app/projects/[id]/page.tsx +2 -1
- package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
- package/templates/projects/app/projects-view.tsx +105 -52
- package/templates/projects/app/workspace.tsx +41 -20
- package/templates/projects/app.ts +2 -2
- package/templates/projects/components/app-shell.tsx +94 -0
- package/templates/projects/components/auth-form.tsx +35 -5
- package/templates/projects/components/avatar.tsx +5 -1
- package/templates/projects/components/board-skeleton.tsx +3 -3
- package/templates/projects/components/brand-mark.tsx +25 -0
- package/templates/projects/components/command-palette.tsx +17 -17
- package/templates/projects/components/data-table.tsx +97 -36
- package/templates/projects/components/filter-tabs.tsx +101 -0
- package/templates/projects/components/meter.tsx +48 -0
- package/templates/projects/components/nav.tsx +14 -0
- package/templates/projects/components/page-header.tsx +43 -7
- package/templates/projects/components/require-auth.tsx +24 -26
- package/templates/projects/components/sidebar.tsx +54 -48
- package/templates/projects/components/summary-bar.tsx +49 -0
- package/templates/projects/components/task-board.tsx +10 -9
- package/templates/projects/components/time-dialog.tsx +29 -5
- package/templates/projects/components/ui/button.tsx +5 -4
- package/templates/projects/components/ui/dialog.tsx +1 -1
- package/templates/projects/components/ui/input.tsx +1 -1
- package/templates/projects/components/ui/select.tsx +1 -1
- package/templates/projects/components/ui/textarea.tsx +1 -1
- package/templates/projects/functions/seedWorkspace.ts +26 -12
- package/templates/projects/lib/brand.ts +9 -0
- package/templates/projects/lib/demo-team.ts +51 -0
- package/templates/projects/lib/search.ts +4 -3
- package/templates/projects/lib/seed.ts +35 -1
- package/templates/projects/lib/work.ts +63 -0
- package/templates/projects/public/favicon.ico +0 -0
- package/templates/projects/tests/demo-team.test.ts +18 -0
- package/templates/projects/tests/work.test.ts +65 -0
- package/templates/restaurant/.env.example +7 -0
- package/templates/restaurant/README.md +29 -3
- package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
- package/templates/restaurant/app/(marketing)/page.tsx +47 -23
- package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
- package/templates/restaurant/app/auth-form.tsx +8 -8
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
- package/templates/restaurant/app/dashboard/page.tsx +14 -14
- package/templates/restaurant/app/icon.svg +4 -0
- package/templates/restaurant/app/login/page.tsx +15 -15
- package/templates/restaurant/functions/seedDemo.ts +44 -0
- package/templates/restaurant/lib/demo.ts +144 -0
- package/templates/restaurant/lib/hours.ts +45 -0
- package/templates/restaurant/lib/site.config.ts +3 -3
- package/templates/restaurant/public/favicon.ico +0 -0
- package/templates/restaurant/tests/reservations.test.ts +67 -0
- package/templates/saas/README.md +54 -35
- package/templates/saas/app/(auth)/auth-form.tsx +25 -23
- package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/layout.tsx +27 -33
- package/templates/saas/app/(auth)/login/page.tsx +1 -1
- package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/signup/page.tsx +1 -1
- package/templates/saas/app/(marketing)/layout.tsx +127 -253
- package/templates/saas/app/(marketing)/page.tsx +133 -372
- package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
- package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
- package/templates/saas/app/dashboard/board-client.tsx +416 -0
- package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
- package/templates/saas/app/dashboard/layout.tsx +27 -13
- package/templates/saas/app/dashboard/overview-client.tsx +332 -0
- package/templates/saas/app/dashboard/page.tsx +29 -35
- package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
- package/templates/saas/app/dashboard/projects/page.tsx +11 -7
- package/templates/saas/app/dashboard/projects-client.tsx +398 -0
- package/templates/saas/app/globals.css +5 -4
- package/templates/saas/app/icon.svg +1 -0
- package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
- package/templates/saas/app/onboarding/page.tsx +6 -2
- package/templates/saas/app.ts +68 -13
- package/templates/saas/components/dashboard-shell.tsx +212 -132
- package/templates/saas/components/marketing.tsx +137 -274
- package/templates/saas/components/pricing-table.tsx +12 -11
- package/templates/saas/components/setup-checklist.tsx +31 -29
- package/templates/saas/components/task-ui.tsx +199 -0
- package/templates/saas/functions/createProject.ts +7 -13
- package/templates/saas/functions/createTask.ts +67 -0
- package/templates/saas/functions/deleteProject.ts +17 -0
- package/templates/saas/functions/deleteTask.ts +14 -0
- package/templates/saas/functions/seedWorkspace.ts +46 -0
- package/templates/saas/functions/setProjectStatus.ts +41 -0
- package/templates/saas/functions/updateProfile.ts +13 -0
- package/templates/saas/functions/updateProject.ts +4 -5
- package/templates/saas/functions/updateTask.ts +88 -0
- package/templates/saas/lib/members.ts +53 -0
- package/templates/saas/lib/names.ts +53 -0
- package/templates/saas/lib/plans.ts +11 -0
- package/templates/saas/lib/sample-workspace.ts +117 -0
- package/templates/saas/lib/site.config.ts +200 -463
- package/templates/saas/lib/tasks.ts +101 -0
- package/templates/saas/lib/use-live.ts +25 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/public/screenshots/board.png +0 -0
- package/templates/saas/public/screenshots/members.png +0 -0
- package/templates/saas/public/screenshots/overview.png +0 -0
- package/templates/saas/public/screenshots/projects.png +0 -0
- package/templates/saas/scripts/capture-screenshots.mjs +156 -0
- package/templates/saas/tests/names.test.ts +35 -0
- package/templates/saas/tests/plans.test.ts +12 -0
- package/templates/saas/tests/sample-workspace.test.ts +46 -0
- package/templates/saas/tests/tasks.test.ts +81 -0
- package/templates/shop/README.md +25 -13
- package/templates/shop/app/(marketing)/layout.tsx +94 -67
- package/templates/shop/app/(marketing)/not-found.tsx +13 -12
- package/templates/shop/app/(marketing)/page.tsx +120 -59
- package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
- package/templates/shop/app/(marketing)/success/page.tsx +75 -22
- package/templates/shop/app/globals.css +94 -1
- package/templates/shop/app/layout.tsx +4 -4
- package/templates/shop/app/login/page.tsx +4 -4
- package/templates/shop/app.ts +18 -6
- package/templates/shop/components/cart-button.tsx +33 -0
- package/templates/shop/components/marketing.tsx +17 -87
- package/templates/shop/components/shelf-count.tsx +36 -0
- package/templates/shop/functions/checkout.ts +17 -1
- package/templates/shop/lib/cart-store.ts +57 -0
- package/templates/shop/lib/shelf-store.ts +35 -0
- package/templates/shop/lib/shop.ts +27 -0
- package/templates/shop/lib/site.config.ts +108 -98
- package/templates/shop/public/images/hero.jpg +0 -0
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/shop/public/images/products/sea-salt.jpg +0 -0
- package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
- package/templates/shop/tests/shop.test.ts +64 -0
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/README.md +29 -13
- package/templates/waitlist/app/(marketing)/page.tsx +9 -7
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
- package/templates/waitlist/app/auth-form.tsx +8 -8
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
- package/templates/waitlist/app/dashboard/page.tsx +17 -16
- package/templates/waitlist/app/icon.svg +2 -0
- package/templates/waitlist/app/login/page.tsx +14 -14
- package/templates/waitlist/app.ts +6 -6
- package/templates/waitlist/functions/joinWaitlist.ts +6 -18
- package/templates/waitlist/functions/seedDemo.ts +32 -0
- package/templates/waitlist/lib/demo.ts +94 -0
- package/templates/waitlist/lib/launch.ts +19 -0
- package/templates/waitlist/lib/site.config.ts +14 -9
- package/templates/waitlist/lib/stats.ts +35 -0
- package/templates/waitlist/public/favicon.ico +0 -0
- package/templates/waitlist/tests/counter.test.ts +107 -0
- package/templates/waitlist/tests/live-count.test.tsx +48 -0
- package/templates/ai-chat/tests/example.test.ts +0 -12
- package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
- package/templates/backend/consumer/apps/api/schema.ts +0 -143
- package/templates/chat/functions/_keep.ts +0 -13
- package/templates/chat/tests/example.test.ts +0 -12
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
- package/templates/marketplace/client/LiveTicker.tsx +0 -116
- package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
- package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Task vocabulary and pure helpers. The server functions (createTask,
|
|
2
|
+
// updateTask) validate with these, and the dashboard renders from the same
|
|
3
|
+
// lists, so a status or priority can't exist in one place and not the other.
|
|
4
|
+
// tests/tasks.test.ts covers them without a running server.
|
|
5
|
+
|
|
6
|
+
export const TASK_STATUSES = ["todo", "in_progress", "in_review", "done"] as const;
|
|
7
|
+
export type TaskStatus = (typeof TASK_STATUSES)[number];
|
|
8
|
+
|
|
9
|
+
export const STATUS_LABEL: Record<TaskStatus, string> = {
|
|
10
|
+
todo: "To do",
|
|
11
|
+
in_progress: "In progress",
|
|
12
|
+
in_review: "In review",
|
|
13
|
+
done: "Done",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const TASK_PRIORITIES = ["urgent", "high", "medium", "low", "none"] as const;
|
|
17
|
+
export type TaskPriority = (typeof TASK_PRIORITIES)[number];
|
|
18
|
+
|
|
19
|
+
export const PRIORITY_LABEL: Record<TaskPriority, string> = {
|
|
20
|
+
urgent: "Urgent",
|
|
21
|
+
high: "High",
|
|
22
|
+
medium: "Medium",
|
|
23
|
+
low: "Low",
|
|
24
|
+
none: "No priority",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface TaskRow {
|
|
28
|
+
id: string;
|
|
29
|
+
orgId: string;
|
|
30
|
+
projectId: string;
|
|
31
|
+
title: string;
|
|
32
|
+
status: string;
|
|
33
|
+
priority?: string | null;
|
|
34
|
+
assigneeId?: string | null;
|
|
35
|
+
assigneeName?: string | null;
|
|
36
|
+
dueDate?: string | null;
|
|
37
|
+
completedAt?: string | null;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isTaskStatus(value: unknown): value is TaskStatus {
|
|
42
|
+
return typeof value === "string" && (TASK_STATUSES as readonly string[]).includes(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isTaskPriority(value: unknown): value is TaskPriority {
|
|
46
|
+
return typeof value === "string" && (TASK_PRIORITIES as readonly string[]).includes(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Trimmed title, or null when it is empty or longer than 200 characters. */
|
|
50
|
+
export function normalizeTaskTitle(raw: string): string | null {
|
|
51
|
+
const title = raw.trim().replace(/\s+/g, " ");
|
|
52
|
+
if (title.length < 1 || title.length > 200) return null;
|
|
53
|
+
return title;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `YYYY-MM-DD` that is a real calendar date, or null. */
|
|
57
|
+
export function normalizeDueDate(raw: string | null | undefined): string | null {
|
|
58
|
+
if (!raw) return null;
|
|
59
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) return null;
|
|
60
|
+
const d = new Date(`${raw}T00:00:00Z`);
|
|
61
|
+
if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== raw) return null;
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Done / total counts and a 0–100 percentage for one project's tasks. */
|
|
66
|
+
export function progressOf(tasks: Pick<TaskRow, "status">[]): {
|
|
67
|
+
done: number;
|
|
68
|
+
total: number;
|
|
69
|
+
percent: number;
|
|
70
|
+
} {
|
|
71
|
+
const total = tasks.length;
|
|
72
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
73
|
+
return { done, total, percent: total === 0 ? 0 : Math.round((done / total) * 100) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PRIORITY_RANK: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3, none: 4 };
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Board order inside a column: priority first, then the earliest due date,
|
|
80
|
+
* then the newest task. Undated tasks sort after dated ones.
|
|
81
|
+
*/
|
|
82
|
+
export function compareTasks(a: TaskRow, b: TaskRow): number {
|
|
83
|
+
const pa = PRIORITY_RANK[a.priority ?? "none"] ?? 4;
|
|
84
|
+
const pb = PRIORITY_RANK[b.priority ?? "none"] ?? 4;
|
|
85
|
+
if (pa !== pb) return pa - pb;
|
|
86
|
+
const da = a.dueDate ?? "9999-12-31";
|
|
87
|
+
const db = b.dueDate ?? "9999-12-31";
|
|
88
|
+
if (da !== db) return da < db ? -1 : 1;
|
|
89
|
+
return a.createdAt < b.createdAt ? 1 : -1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Whole days from `today` to `due` (both `YYYY-MM-DD`). Negative when overdue. */
|
|
93
|
+
export function daysUntil(due: string, today: string): number {
|
|
94
|
+
const ms = Date.parse(`${due}T00:00:00Z`) - Date.parse(`${today}T00:00:00Z`);
|
|
95
|
+
return Math.round(ms / 86_400_000);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `YYYY-MM-DD` for a Date, in UTC. */
|
|
99
|
+
export function isoDay(d: Date): string {
|
|
100
|
+
return d.toISOString().slice(0, 10);
|
|
101
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { db } from "@pylonsync/react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rows of `entity` for one workspace, live. The server render and the first
|
|
6
|
+
* client paint use `initial` (resolved with `serverData` in the page), so the
|
|
7
|
+
* HTML and the hydrated tree match and nothing flashes empty. After mount the
|
|
8
|
+
* list switches to the synced replica, which updates on every write from any
|
|
9
|
+
* tab or teammate.
|
|
10
|
+
*
|
|
11
|
+
* The entity's read policy already limits the replica to the active
|
|
12
|
+
* workspace; the `orgId` filter drops rows from a previous workspace that
|
|
13
|
+
* can linger for a moment after switching.
|
|
14
|
+
*/
|
|
15
|
+
export function useLiveRows<T extends { orgId: string }>(
|
|
16
|
+
entity: string,
|
|
17
|
+
initial: T[],
|
|
18
|
+
orgId: string,
|
|
19
|
+
): T[] {
|
|
20
|
+
const [hydrated, setHydrated] = useState(false);
|
|
21
|
+
useEffect(() => setHydrated(true), []);
|
|
22
|
+
const { data, loading } = db.useQuery<T>(entity);
|
|
23
|
+
if (!hydrated || loading) return initial;
|
|
24
|
+
return data.filter((row) => row.orgId === orgId);
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regenerates the product screenshots the landing page shows
|
|
3
|
+
// (public/screenshots/*.png) from this app's own dashboard.
|
|
4
|
+
//
|
|
5
|
+
// 1. Start the app: pylon dev
|
|
6
|
+
// 2. Install Playwright once: npm i -D playwright && npx playwright install chromium
|
|
7
|
+
// 3. Capture: node scripts/capture-screenshots.mjs
|
|
8
|
+
//
|
|
9
|
+
// The script signs up a workspace owner and three teammates through the auth
|
|
10
|
+
// API, invites the teammates, adds the sample projects (seedWorkspace), then
|
|
11
|
+
// opens the dashboard in Chromium and saves each screen. Run it against a
|
|
12
|
+
// fresh development database: it reuses the accounts on a second run, but the
|
|
13
|
+
// data then reflects whatever you changed in between.
|
|
14
|
+
//
|
|
15
|
+
// Set BASE_URL to capture from another port.
|
|
16
|
+
|
|
17
|
+
import { mkdir } from "node:fs/promises";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const BASE = (process.env.BASE_URL ?? "http://localhost:4321").replace(/\/$/, "");
|
|
22
|
+
const OUT = join(dirname(fileURLToPath(import.meta.url)), "..", "public", "screenshots");
|
|
23
|
+
const PASSWORD = "sample-password-2941";
|
|
24
|
+
const OWNER = { email: "dana@northwind.test", name: "Dana Reyes" };
|
|
25
|
+
const TEAM = [
|
|
26
|
+
{ email: "sam@northwind.test", name: "Sam Okafor" },
|
|
27
|
+
{ email: "priya@northwind.test", name: "Priya Raman" },
|
|
28
|
+
{ email: "leo@northwind.test", name: "Leo Park" },
|
|
29
|
+
];
|
|
30
|
+
const PENDING = ["maria@northwind.test", "jonas@northwind.test"];
|
|
31
|
+
|
|
32
|
+
let chromium;
|
|
33
|
+
try {
|
|
34
|
+
({ chromium } = await import("playwright"));
|
|
35
|
+
} catch {
|
|
36
|
+
try {
|
|
37
|
+
({ chromium } = await import("playwright-core"));
|
|
38
|
+
} catch {
|
|
39
|
+
console.error("Playwright is not installed. Run: npm i -D playwright && npx playwright install chromium");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function api(path, { token, body, method = "POST" } = {}) {
|
|
45
|
+
const res = await fetch(BASE + path, {
|
|
46
|
+
method,
|
|
47
|
+
headers: {
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
50
|
+
},
|
|
51
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
52
|
+
});
|
|
53
|
+
const data = await res.json().catch(() => ({}));
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const err = new Error(`${method} ${path} → ${res.status} ${data?.error?.code ?? ""} ${data?.error?.message ?? ""}`);
|
|
56
|
+
err.code = data?.error?.code;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Register the account, or sign in when it already exists. Returns a token. */
|
|
63
|
+
async function account({ email, name }) {
|
|
64
|
+
try {
|
|
65
|
+
const s = await api("/api/auth/password/register", { body: { email, password: PASSWORD, displayName: name } });
|
|
66
|
+
return s.token;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err.code !== "EMAIL_TAKEN" && err.code !== "USER_EXISTS") throw err;
|
|
69
|
+
const s = await api("/api/auth/password/login", { body: { email, password: PASSWORD } });
|
|
70
|
+
return s.token;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function setUp() {
|
|
75
|
+
const owner = await account(OWNER);
|
|
76
|
+
const orgs = await api("/api/auth/orgs", { token: owner, method: "GET" });
|
|
77
|
+
const list = Array.isArray(orgs) ? orgs : orgs.orgs ?? [];
|
|
78
|
+
let org = list.find((o) => o.name === "Northwind");
|
|
79
|
+
if (!org) org = await api("/api/auth/orgs", { token: owner, body: { name: "Northwind" } });
|
|
80
|
+
await api("/api/auth/select-org", { token: owner, body: { orgId: org.id } });
|
|
81
|
+
|
|
82
|
+
const members = await api(`/api/auth/orgs/${org.id}/members`, { token: owner, method: "GET" });
|
|
83
|
+
const roster = Array.isArray(members) ? members : members.members ?? [];
|
|
84
|
+
for (const person of TEAM) {
|
|
85
|
+
const token = await account(person);
|
|
86
|
+
if (roster.some((m) => m.email === person.email)) continue;
|
|
87
|
+
const invite = await api(`/api/auth/orgs/${org.id}/invites`, {
|
|
88
|
+
token: owner,
|
|
89
|
+
body: { email: person.email, role: "member" },
|
|
90
|
+
});
|
|
91
|
+
await api(`/api/auth/invites/${encodeURIComponent(invite.token)}/accept`, { token, body: {} });
|
|
92
|
+
}
|
|
93
|
+
for (const email of PENDING) {
|
|
94
|
+
await api(`/api/auth/orgs/${org.id}/invites`, { token: owner, body: { email, role: "member" } }).catch(() => {});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await api("/api/fn/seedWorkspace", { token: owner, body: { orgId: org.id } });
|
|
98
|
+
await api("/api/fn/completeOnboarding", { token: owner, body: { orgId: org.id } });
|
|
99
|
+
await api("/api/fn/dismissSetup", { token: owner, body: { orgId: org.id } });
|
|
100
|
+
return org;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Hide the development-only overlay badge that `pylon dev` adds.
|
|
104
|
+
async function hideDevBadge(page) {
|
|
105
|
+
await page.evaluate(() => {
|
|
106
|
+
for (const el of document.querySelectorAll("body *")) {
|
|
107
|
+
if (el.children.length === 0 && el.textContent?.trim() === "pylon") {
|
|
108
|
+
let node = el;
|
|
109
|
+
while (node && getComputedStyle(node).position !== "fixed") node = node.parentElement;
|
|
110
|
+
if (node) node.style.display = "none";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function shoot(page, path, file) {
|
|
117
|
+
await page.goto(BASE + path, { waitUntil: "load" });
|
|
118
|
+
await page.waitForTimeout(1500);
|
|
119
|
+
await hideDevBadge(page);
|
|
120
|
+
await page.screenshot({ path: join(OUT, file) });
|
|
121
|
+
console.log(`saved public/screenshots/${file}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const org = await setUp();
|
|
125
|
+
await mkdir(OUT, { recursive: true });
|
|
126
|
+
|
|
127
|
+
const browser = await chromium.launch();
|
|
128
|
+
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
|
|
129
|
+
const page = await context.newPage();
|
|
130
|
+
// Signing in from the page stores the session cookie in this browser
|
|
131
|
+
// context. The active workspace belongs to the session, so select it here too.
|
|
132
|
+
await page.goto(BASE + "/login");
|
|
133
|
+
await page.evaluate(
|
|
134
|
+
async ({ email, password, orgId }) => {
|
|
135
|
+
const post = (path, body) =>
|
|
136
|
+
fetch(path, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
credentials: "include",
|
|
140
|
+
body: JSON.stringify(body),
|
|
141
|
+
});
|
|
142
|
+
await post("/api/auth/password/login", { email, password });
|
|
143
|
+
await post("/api/auth/select-org", { orgId });
|
|
144
|
+
},
|
|
145
|
+
{ email: OWNER.email, password: PASSWORD, orgId: org.id },
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await shoot(page, "/dashboard", "overview.png");
|
|
149
|
+
await shoot(page, "/dashboard/projects", "projects.png");
|
|
150
|
+
await shoot(page, "/dashboard/members", "members.png");
|
|
151
|
+
await page.goto(BASE + "/dashboard/projects", { waitUntil: "load" });
|
|
152
|
+
await page.getByRole("link", { name: /Website relaunch/ }).first().click();
|
|
153
|
+
await page.waitForURL(/\/dashboard\/projects\/.+/);
|
|
154
|
+
await shoot(page, new URL(page.url()).pathname, "board.png");
|
|
155
|
+
|
|
156
|
+
await browser.close();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { firstName, initials, nameFromEmail, personName } from "../lib/names";
|
|
3
|
+
|
|
4
|
+
test("a name built from an email drops the +tag and digits", () => {
|
|
5
|
+
expect(nameFromEmail("dana+s13596@example.com")).toBe("Dana");
|
|
6
|
+
expect(nameFromEmail("dana.reyes@example.com")).toBe("Dana Reyes");
|
|
7
|
+
expect(nameFromEmail("sam_okafor-2@example.com")).toBe("Sam Okafor");
|
|
8
|
+
expect(nameFromEmail("1234@example.com")).toBe("");
|
|
9
|
+
expect(nameFromEmail(null)).toBe("");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("the saved display name wins", () => {
|
|
13
|
+
expect(personName({ displayName: "Dana Reyes", email: "d@example.com" })).toBe("Dana Reyes");
|
|
14
|
+
expect(personName({ displayName: " ", email: "leo.park@example.com" })).toBe("Leo Park");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("a name that is an email address counts as no name", () => {
|
|
18
|
+
expect(personName({ name: "priya@example.com", email: "priya@example.com" })).toBe("Priya");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("an email with nothing readable falls back to the email", () => {
|
|
22
|
+
expect(personName({ email: "42@example.com" })).toBe("42@example.com");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("greetings use the first word; avatars use two initials", () => {
|
|
26
|
+
expect(firstName({ displayName: "Dana Reyes" })).toBe("Dana");
|
|
27
|
+
expect(initials("Dana Reyes")).toBe("DR");
|
|
28
|
+
expect(initials("Priya Anand Raman")).toBe("PR");
|
|
29
|
+
expect(initials("Leo")).toBe("L");
|
|
30
|
+
expect(initials("")).toBe("?");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("names longer than the UI limit are cut", () => {
|
|
34
|
+
expect(personName({ displayName: "x".repeat(500) })).toHaveLength(60);
|
|
35
|
+
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
canCreateProject,
|
|
7
7
|
planById,
|
|
8
8
|
planFromSubscription,
|
|
9
|
+
planFromSubscriptions,
|
|
9
10
|
} from "../lib/plans";
|
|
10
11
|
|
|
11
12
|
// Tier 1 (pure logic) — the cap and the plan derivation that
|
|
@@ -33,3 +34,14 @@ test("annual pricing derives its total and saving from the catalog", () => {
|
|
|
33
34
|
expect(annualSavingsPercent(pro)).toBeGreaterThan(0);
|
|
34
35
|
expect(annualSavingsPercent(planById("free")!)).toBe(0);
|
|
35
36
|
});
|
|
37
|
+
|
|
38
|
+
test("a usable Pro row wins over an older canceled row", () => {
|
|
39
|
+
expect(
|
|
40
|
+
planFromSubscriptions([
|
|
41
|
+
{ plan: "pro", status: "canceled" },
|
|
42
|
+
{ plan: "pro", status: "trialing" },
|
|
43
|
+
]),
|
|
44
|
+
).toBe("pro");
|
|
45
|
+
expect(planFromSubscriptions([{ plan: "pro", status: "canceled" }])).toBe("free");
|
|
46
|
+
expect(planFromSubscriptions([])).toBe("free");
|
|
47
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { FREE_PROJECT_LIMIT } from "../lib/plans";
|
|
3
|
+
import { SAMPLE_PROJECTS, planSampleWorkspace } from "../lib/sample-workspace";
|
|
4
|
+
import { isTaskPriority, isTaskStatus, normalizeTaskTitle } from "../lib/tasks";
|
|
5
|
+
|
|
6
|
+
const NOW = new Date("2026-09-26T12:00:00Z");
|
|
7
|
+
|
|
8
|
+
test("the samples leave a free workspace room for one project of its own", () => {
|
|
9
|
+
const active = SAMPLE_PROJECTS.filter((p) => p.status === "active").length;
|
|
10
|
+
expect(active).toBeLessThan(FREE_PROJECT_LIMIT);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("every sample task passes the same validation as createTask", () => {
|
|
14
|
+
for (const p of SAMPLE_PROJECTS) {
|
|
15
|
+
for (const t of p.tasks) {
|
|
16
|
+
expect(normalizeTaskTitle(t.title)).toBe(t.title);
|
|
17
|
+
expect(isTaskStatus(t.status)).toBe(true);
|
|
18
|
+
expect(isTaskPriority(t.priority)).toBe(true);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("due dates are relative to now, and done tasks get a completion time", () => {
|
|
24
|
+
const plan = planSampleWorkspace([], NOW);
|
|
25
|
+
const first = plan[0].tasks.find((t) => t.title === "Fix the mobile nav overlap on Safari")!;
|
|
26
|
+
expect(first.dueDate).toBe("2026-09-26");
|
|
27
|
+
const done = plan[0].tasks.find((t) => t.status === "done")!;
|
|
28
|
+
expect(done.completedAt).not.toBeNull();
|
|
29
|
+
expect(plan[0].tasks.filter((t) => t.status !== "done").every((t) => t.completedAt === null)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("tasks go only to the members passed in", () => {
|
|
33
|
+
const solo = planSampleWorkspace([{ userId: "u1", name: "Dana Reyes" }], NOW);
|
|
34
|
+
const assigned = solo.flatMap((p) => p.tasks).filter((t) => t.assigneeId);
|
|
35
|
+
expect(assigned.length).toBeGreaterThan(0);
|
|
36
|
+
expect(assigned.every((t) => t.assigneeId === "u1" && t.assigneeName === "Dana Reyes")).toBe(true);
|
|
37
|
+
|
|
38
|
+
const nobody = planSampleWorkspace([], NOW);
|
|
39
|
+
expect(nobody.flatMap((p) => p.tasks).every((t) => t.assigneeId === null)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("a team gets the work spread across people", () => {
|
|
43
|
+
const team = ["a", "b", "c", "d"].map((id) => ({ userId: id, name: id }));
|
|
44
|
+
const ids = new Set(planSampleWorkspace(team, NOW).flatMap((p) => p.tasks).map((t) => t.assigneeId));
|
|
45
|
+
expect(ids).toEqual(new Set(["a", "b", "c", "d", null]));
|
|
46
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
compareTasks,
|
|
4
|
+
daysUntil,
|
|
5
|
+
isTaskPriority,
|
|
6
|
+
isTaskStatus,
|
|
7
|
+
normalizeDueDate,
|
|
8
|
+
normalizeTaskTitle,
|
|
9
|
+
progressOf,
|
|
10
|
+
type TaskRow,
|
|
11
|
+
} from "../lib/tasks";
|
|
12
|
+
|
|
13
|
+
// Tier 1 (pure logic): the validation createTask/updateTask run on the
|
|
14
|
+
// server, and the ordering and progress math the dashboard renders.
|
|
15
|
+
|
|
16
|
+
test("statuses and priorities are closed lists", () => {
|
|
17
|
+
expect(isTaskStatus("in_review")).toBe(true);
|
|
18
|
+
expect(isTaskStatus("blocked")).toBe(false);
|
|
19
|
+
expect(isTaskStatus(undefined)).toBe(false);
|
|
20
|
+
expect(isTaskPriority("urgent")).toBe(true);
|
|
21
|
+
expect(isTaskPriority("critical")).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("titles are trimmed, collapsed, and bounded", () => {
|
|
25
|
+
expect(normalizeTaskTitle(" Ship the build ")).toBe("Ship the build");
|
|
26
|
+
expect(normalizeTaskTitle(" ")).toBeNull();
|
|
27
|
+
expect(normalizeTaskTitle("x".repeat(200))).toHaveLength(200);
|
|
28
|
+
expect(normalizeTaskTitle("x".repeat(201))).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("due dates must be real calendar days", () => {
|
|
32
|
+
expect(normalizeDueDate("2026-02-28")).toBe("2026-02-28");
|
|
33
|
+
expect(normalizeDueDate("2026-02-30")).toBeNull();
|
|
34
|
+
expect(normalizeDueDate("26-2-3")).toBeNull();
|
|
35
|
+
expect(normalizeDueDate("")).toBeNull();
|
|
36
|
+
expect(normalizeDueDate(null)).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("progress counts done tasks", () => {
|
|
40
|
+
expect(progressOf([])).toEqual({ done: 0, total: 0, percent: 0 });
|
|
41
|
+
expect(progressOf([{ status: "done" }, { status: "todo" }, { status: "done" }])).toEqual({
|
|
42
|
+
done: 2,
|
|
43
|
+
total: 3,
|
|
44
|
+
percent: 67,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("daysUntil is negative when overdue", () => {
|
|
49
|
+
expect(daysUntil("2026-09-26", "2026-09-26")).toBe(0);
|
|
50
|
+
expect(daysUntil("2026-10-01", "2026-09-26")).toBe(5);
|
|
51
|
+
expect(daysUntil("2026-09-20", "2026-09-26")).toBe(-6);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("board order: priority, then due date, then newest", () => {
|
|
55
|
+
const row = (id: string, priority: string, dueDate: string | null, createdAt: string): TaskRow => ({
|
|
56
|
+
id,
|
|
57
|
+
orgId: "o",
|
|
58
|
+
projectId: "p",
|
|
59
|
+
title: id,
|
|
60
|
+
status: "todo",
|
|
61
|
+
priority,
|
|
62
|
+
dueDate,
|
|
63
|
+
createdAt,
|
|
64
|
+
});
|
|
65
|
+
const sorted = [
|
|
66
|
+
row("low-soon", "low", "2026-09-27", "2026-09-01"),
|
|
67
|
+
row("urgent", "urgent", null, "2026-09-01"),
|
|
68
|
+
row("high-late", "high", "2026-10-09", "2026-09-01"),
|
|
69
|
+
row("high-soon", "high", "2026-09-28", "2026-09-01"),
|
|
70
|
+
row("high-undated-new", "high", null, "2026-09-03"),
|
|
71
|
+
row("high-undated-old", "high", null, "2026-09-02"),
|
|
72
|
+
].sort(compareTasks);
|
|
73
|
+
expect(sorted.map((t) => t.id)).toEqual([
|
|
74
|
+
"urgent",
|
|
75
|
+
"high-soon",
|
|
76
|
+
"high-late",
|
|
77
|
+
"high-undated-new",
|
|
78
|
+
"high-undated-old",
|
|
79
|
+
"low-soon",
|
|
80
|
+
]);
|
|
81
|
+
});
|
package/templates/shop/README.md
CHANGED
|
@@ -22,8 +22,10 @@ out in one tab, then watch it change to "Sold out" in another.
|
|
|
22
22
|
|
|
23
23
|
## How the realtime works
|
|
24
24
|
|
|
25
|
-
- `Product` is a public-read entity holding live stock;
|
|
26
|
-
reads it with `db.useQuery`, so every
|
|
25
|
+
- `Product` is a public-read entity holding live stock;
|
|
26
|
+
`app/(marketing)/shop-client.tsx` reads it with `db.useQuery`, so every
|
|
27
|
+
card's stock count is live. The hero's shelf count reads the same totals
|
|
28
|
+
through `lib/shelf-store.ts`.
|
|
27
29
|
- `functions/checkout.ts` is a public **action**. It calls
|
|
28
30
|
`functions/reserveCart.ts` (an internal **mutation**) to re-check stock under a
|
|
29
31
|
per-product advisory lock and HOLD it before the order is recorded — so two
|
|
@@ -57,21 +59,28 @@ Checkout is a single public `checkout` action that holds stock, then:
|
|
|
57
59
|
(all cart lines priced inline from your catalog — no Stripe Products to set
|
|
58
60
|
up) and redirects the shopper to it. The signed `stripeWebhook` action (at
|
|
59
61
|
`/api/webhooks/stripeWebhook`) marks the order **paid** on success and
|
|
60
|
-
**returns held stock** if a checkout is abandoned.
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
**returns held stock** if a checkout is abandoned. Stripe collects a US
|
|
63
|
+
shipping address and charges the shipping terms from `lib/site.config.ts`
|
|
64
|
+
(flat rate, free over a threshold), the same numbers the cart shows. The
|
|
65
|
+
webhook signature is verified with `@pylonsync/stripe`'s constant-time
|
|
66
|
+
verifier before any event is trusted.
|
|
63
67
|
- **Without Stripe keys** → the order is held as **reserved** for you to follow
|
|
64
|
-
up on
|
|
65
|
-
zero setup.
|
|
68
|
+
up on, and the shopper lands on `/success?order=reserved`. The store still
|
|
69
|
+
works end-to-end, so you can demo live inventory with zero setup.
|
|
66
70
|
|
|
67
71
|
See `.env.example` for the two env vars and the Stripe dashboard / `stripe
|
|
68
72
|
listen` setup.
|
|
69
73
|
|
|
70
74
|
## Rebrand it
|
|
71
75
|
|
|
72
|
-
Brand, colors,
|
|
73
|
-
live in **`lib/site.config.ts`**. Editing that
|
|
74
|
-
fresh database seeds products from it.
|
|
76
|
+
Brand, colors, hero and studio copy, products and starting stock, shipping
|
|
77
|
+
terms, reviews, and policies live in **`lib/site.config.ts`**. Editing that
|
|
78
|
+
file updates the store, and a fresh database seeds products from it.
|
|
79
|
+
|
|
80
|
+
Photos live in `public/images/`. Product photos are 4:5 portrait JPEGs
|
|
81
|
+
(1200x1500 works well); the hero is a wide landscape photo with open space on
|
|
82
|
+
the left for the headline. `tests/shop.test.ts` fails if the config points at
|
|
83
|
+
an image that is not in `public/`.
|
|
75
84
|
|
|
76
85
|
## Layout
|
|
77
86
|
|
|
@@ -85,9 +94,12 @@ functions/stripeWebhook.ts public webhook: verify signature, settle the order
|
|
|
85
94
|
functions/{markGroupPaid,releaseGroup}.ts internal: settle/restore on webhook
|
|
86
95
|
functions/ordersForOwner.ts owner-only query: orders + customer PII
|
|
87
96
|
functions/{fulfill,cancel}Order.ts, restockProduct.ts owner-only mutations
|
|
88
|
-
app/
|
|
89
|
-
app/
|
|
90
|
-
app/
|
|
97
|
+
app/(marketing)/layout.tsx announcement bar, nav, footer
|
|
98
|
+
app/(marketing)/page.tsx the storefront (server-rendered)
|
|
99
|
+
app/(marketing)/shop-client.tsx client island: live product grid + cart + checkout
|
|
100
|
+
app/(marketing)/success/page.tsx order confirmation (Stripe or reserved)
|
|
101
|
+
lib/cart-store.ts, lib/shelf-store.ts state shared between client islands
|
|
102
|
+
public/images/ hero and product photos
|
|
91
103
|
app/dashboard/ owner dashboard (auth-gated, live)
|
|
92
104
|
```
|
|
93
105
|
|