@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
|
@@ -1,33 +1,86 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link, type Metadata } from "@pylonsync/react";
|
|
3
|
-
import {
|
|
2
|
+
import { Link, type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
+
import { Check } from "lucide-react";
|
|
4
|
+
import { WRAP, BUTTON_DARK } from "@/components/marketing";
|
|
4
5
|
import { siteConfig } from "@/lib/site.config";
|
|
5
6
|
|
|
6
7
|
export const metadata: Metadata = {
|
|
7
|
-
title: `Thank you
|
|
8
|
+
title: `Thank you | ${siteConfig.brand.name}`,
|
|
8
9
|
description: "Your order is confirmed.",
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
// `app/success/page.tsx` → `/success`.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
12
|
+
// `app/(marketing)/success/page.tsx` → `/success`. Two ways in:
|
|
13
|
+
// • Stripe sends the shopper here after a completed Checkout (the
|
|
14
|
+
// `successUrl` passed to the `checkout` action). The signed `stripeWebhook`
|
|
15
|
+
// settles the order, so this page doesn't read the session id Stripe appends.
|
|
16
|
+
// • With no Stripe key, the cart sends the shopper here with `?order=reserved`
|
|
17
|
+
// (plus `&sold=` naming any line that sold out during checkout).
|
|
18
|
+
// The page shows no order details: Order rows hold PII and are owner-only.
|
|
19
|
+
export default function CheckoutSuccess({ searchParams }: PageProps) {
|
|
20
|
+
const reserved = searchParams.order === "reserved";
|
|
21
|
+
const sold = (searchParams.sold ?? "").split(",").filter(Boolean);
|
|
22
|
+
const { checkout, brand } = siteConfig;
|
|
23
|
+
|
|
24
|
+
const steps = reserved
|
|
25
|
+
? ["Order reserved", "Payment link sent by email", "Packed and shipped from Dallas"]
|
|
26
|
+
: ["Payment received", "Packed in the studio", "Shipped from Dallas"];
|
|
27
|
+
|
|
17
28
|
return (
|
|
18
|
-
<section className={`${WRAP}
|
|
19
|
-
<div
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
<section className={`${WRAP} grid min-h-[72vh] items-center gap-14 py-16 md:grid-cols-[1.4fr_1fr] md:gap-20 md:py-20`}>
|
|
30
|
+
<div>
|
|
31
|
+
<h1 className="rise font-display text-[3rem] font-light leading-[1] tracking-[-0.03em] text-ink sm:text-[4.25rem]">
|
|
32
|
+
{reserved ? "Your order is reserved." : "Thank you for your order."}
|
|
33
|
+
</h1>
|
|
34
|
+
<p
|
|
35
|
+
className="rise mt-6 max-w-[46ch] text-[17px] leading-[1.65] text-ink-2"
|
|
36
|
+
style={{ animationDelay: "100ms" }}
|
|
37
|
+
>
|
|
38
|
+
{reserved ? checkout.reservedMessage : checkout.paidMessage}
|
|
39
|
+
</p>
|
|
40
|
+
{sold.length > 0 ? (
|
|
41
|
+
<p className="mt-4 max-w-[46ch] text-[14.5px] leading-relaxed text-brand">
|
|
42
|
+
{sold.join(", ")} sold out before we could hold {sold.length === 1 ? "it" : "them"}, so{" "}
|
|
43
|
+
{sold.length === 1 ? "it is" : "they are"} not in this order.
|
|
44
|
+
</p>
|
|
45
|
+
) : null}
|
|
46
|
+
|
|
47
|
+
<ol className="rise mt-10 max-w-md border-t border-ink/15" style={{ animationDelay: "200ms" }}>
|
|
48
|
+
{steps.map((step, i) => (
|
|
49
|
+
<li key={step} className="flex items-center gap-4 border-b border-ink/15 py-4 text-[15px]">
|
|
50
|
+
<span
|
|
51
|
+
className={
|
|
52
|
+
"flex size-7 shrink-0 items-center justify-center rounded-full text-[12px] tabular-nums " +
|
|
53
|
+
(i === 0 ? "bg-ink text-warm-white" : "border border-ink/20 text-ink-2")
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
{i === 0 ? <Check className="size-3.5" strokeWidth={2} /> : i + 1}
|
|
57
|
+
</span>
|
|
58
|
+
<span className={i === 0 ? "font-medium text-ink" : "text-ink-2"}>{step}</span>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ol>
|
|
62
|
+
|
|
63
|
+
<div className="rise mt-10 flex flex-wrap items-center gap-5" style={{ animationDelay: "300ms" }}>
|
|
64
|
+
<Link href="/#shop" className={BUTTON_DARK}>
|
|
65
|
+
Back to the shop
|
|
66
|
+
</Link>
|
|
67
|
+
<a
|
|
68
|
+
href={`mailto:${brand.email}`}
|
|
69
|
+
className="text-[14px] text-ink-2 underline decoration-ink/20 underline-offset-4 transition-colors hover:text-ink hover:decoration-ink"
|
|
70
|
+
>
|
|
71
|
+
Questions? {brand.email}
|
|
72
|
+
</a>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="relative aspect-[4/5] overflow-hidden bg-stone">
|
|
77
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
78
|
+
<img
|
|
79
|
+
src={checkout.image}
|
|
80
|
+
alt=""
|
|
81
|
+
className="settle size-full object-cover"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
31
84
|
</section>
|
|
32
85
|
);
|
|
33
86
|
}
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
--brand: #4f46e5;
|
|
20
20
|
--brand-soft: #eef2ff;
|
|
21
21
|
--paper: #fafafa;
|
|
22
|
+
/* Storefront palette: bone page, warm near-black type, stone for image
|
|
23
|
+
wells, and a dark espresso band. */
|
|
24
|
+
--ink: #1f1b17;
|
|
25
|
+
--ink-2: #6d645a;
|
|
26
|
+
--warm-white: #f6f2eb;
|
|
27
|
+
--stone: #e6ded2;
|
|
28
|
+
--espresso: #221d18;
|
|
29
|
+
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
|
|
22
30
|
--background: oklch(1 0 0);
|
|
23
31
|
--foreground: oklch(0.141 0.005 285.823);
|
|
24
32
|
--card: oklch(1 0 0);
|
|
@@ -112,6 +120,12 @@
|
|
|
112
120
|
--color-brand: var(--brand);
|
|
113
121
|
--color-brand-soft: var(--brand-soft);
|
|
114
122
|
--color-paper: var(--paper);
|
|
123
|
+
--color-ink: var(--ink);
|
|
124
|
+
--color-ink-2: var(--ink-2);
|
|
125
|
+
--color-warm-white: var(--warm-white);
|
|
126
|
+
--color-stone: var(--stone);
|
|
127
|
+
--color-espresso: var(--espresso);
|
|
128
|
+
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
|
|
115
129
|
--color-chart-1: var(--chart-1);
|
|
116
130
|
--color-chart-2: var(--chart-2);
|
|
117
131
|
--color-chart-3: var(--chart-3);
|
|
@@ -141,7 +155,7 @@
|
|
|
141
155
|
color: var(--color-foreground);
|
|
142
156
|
font-family: var(
|
|
143
157
|
--font-sans,
|
|
144
|
-
|
|
158
|
+
"DM Sans",
|
|
145
159
|
ui-sans-serif,
|
|
146
160
|
system-ui,
|
|
147
161
|
-apple-system,
|
|
@@ -153,3 +167,82 @@
|
|
|
153
167
|
cursor: pointer;
|
|
154
168
|
}
|
|
155
169
|
}
|
|
170
|
+
|
|
171
|
+
.font-display {
|
|
172
|
+
font-family: var(--font-display, "Newsreader", Georgia, "Times New Roman", serif);
|
|
173
|
+
font-optical-sizing: auto;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Storefront motion. Every animation uses transform and opacity only, and
|
|
177
|
+
all of it stops under prefers-reduced-motion. */
|
|
178
|
+
@keyframes rise {
|
|
179
|
+
from {
|
|
180
|
+
opacity: 0;
|
|
181
|
+
transform: translateY(18px);
|
|
182
|
+
}
|
|
183
|
+
to {
|
|
184
|
+
opacity: 1;
|
|
185
|
+
transform: none;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
@keyframes settle {
|
|
189
|
+
from {
|
|
190
|
+
transform: scale(1.06);
|
|
191
|
+
}
|
|
192
|
+
to {
|
|
193
|
+
transform: scale(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
@keyframes stock-tick {
|
|
197
|
+
0% {
|
|
198
|
+
background-color: color-mix(in srgb, var(--brand) 22%, transparent);
|
|
199
|
+
}
|
|
200
|
+
100% {
|
|
201
|
+
background-color: transparent;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
@keyframes ping-soft {
|
|
205
|
+
0% {
|
|
206
|
+
transform: scale(1);
|
|
207
|
+
opacity: 0.55;
|
|
208
|
+
}
|
|
209
|
+
80%,
|
|
210
|
+
100% {
|
|
211
|
+
transform: scale(2.6);
|
|
212
|
+
opacity: 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.rise {
|
|
217
|
+
animation: rise 1s var(--ease-out-quint) both;
|
|
218
|
+
}
|
|
219
|
+
.settle {
|
|
220
|
+
animation: settle 2.2s var(--ease-out-quint) both;
|
|
221
|
+
}
|
|
222
|
+
.stock-tick {
|
|
223
|
+
animation: stock-tick 1.6s var(--ease-out-quint) both;
|
|
224
|
+
}
|
|
225
|
+
.ping-soft {
|
|
226
|
+
animation: ping-soft 2s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@media (prefers-reduced-motion: reduce) {
|
|
230
|
+
.rise,
|
|
231
|
+
.settle,
|
|
232
|
+
.stock-tick,
|
|
233
|
+
.ping-soft {
|
|
234
|
+
animation: none;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Paper grain over the storefront. Fixed and non-interactive, so it never
|
|
239
|
+
repaints on scroll. */
|
|
240
|
+
.grain::after {
|
|
241
|
+
content: "";
|
|
242
|
+
position: fixed;
|
|
243
|
+
inset: 0;
|
|
244
|
+
z-index: 60;
|
|
245
|
+
pointer-events: none;
|
|
246
|
+
opacity: 0.035;
|
|
247
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
248
|
+
}
|
|
@@ -31,10 +31,10 @@ export default function RootLayout({ children }: LayoutProps) {
|
|
|
31
31
|
<meta charSet="utf-8" />
|
|
32
32
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
33
33
|
{/* No <title> here — each page's exported `metadata` sets it. */}
|
|
34
|
-
{/*
|
|
35
|
-
|
|
36
|
-
size-adjusted fallback here automatically.
|
|
37
|
-
|
|
34
|
+
{/* DM Sans and Newsreader are declared in app.ts (fonts: [...]) and
|
|
35
|
+
self-hosted by the build. The runtime injects @font-face + <link
|
|
36
|
+
rel=preload> + a size-adjusted fallback here automatically. Change
|
|
37
|
+
the families in app.ts. */}
|
|
38
38
|
{/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
|
|
39
39
|
</head>
|
|
40
40
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
@@ -18,14 +18,14 @@ export default function LoginPage({ auth, response }: PageProps) {
|
|
|
18
18
|
if (auth.user_id && !auth.user_id.startsWith("guest_")) response.redirect("/dashboard");
|
|
19
19
|
const { brand } = siteConfig;
|
|
20
20
|
return (
|
|
21
|
-
<div className="flex min-h-screen items-center justify-center bg-white px-6 py-12">
|
|
22
|
-
<div className="w-full max-w-[400px] rounded-2xl border border-
|
|
21
|
+
<div className="flex min-h-screen items-center justify-center bg-warm-white px-6 py-12">
|
|
22
|
+
<div className="w-full max-w-[400px] rounded-2xl border border-ink/10 bg-white p-8">
|
|
23
23
|
<Link href="/" className="inline-flex">
|
|
24
|
-
<span className="flex size-9 items-center justify-center rounded-
|
|
24
|
+
<span className="font-display flex size-9 items-center justify-center rounded-full bg-ink text-[18px] text-warm-white">
|
|
25
25
|
{brand.letter}
|
|
26
26
|
</span>
|
|
27
27
|
</Link>
|
|
28
|
-
<h1 className="mt-5 text-[
|
|
28
|
+
<h1 className="font-display mt-5 text-[26px] font-normal tracking-[-0.01em] text-ink">
|
|
29
29
|
{brand.name} dashboard
|
|
30
30
|
</h1>
|
|
31
31
|
<p className="mt-1 text-[13px] text-zinc-500">
|
package/templates/shop/app.ts
CHANGED
|
@@ -46,7 +46,7 @@ const Product = entity(
|
|
|
46
46
|
name: field.string(),
|
|
47
47
|
priceCents: field.int(),
|
|
48
48
|
description: field.string().optional(),
|
|
49
|
-
image: field.string().optional(), //
|
|
49
|
+
image: field.string().optional(), // photo URL or /public path
|
|
50
50
|
stock: field.int().default(0),
|
|
51
51
|
},
|
|
52
52
|
{ indexes: [{ name: "by_slug", fields: ["slug"], unique: true }] },
|
|
@@ -133,15 +133,27 @@ const manifest = buildManifest({
|
|
|
133
133
|
actions: fns.actions,
|
|
134
134
|
policies: [productPolicy, orderPolicy, userPolicy],
|
|
135
135
|
auth: auth(),
|
|
136
|
-
// Self-hosted
|
|
136
|
+
// Self-hosted fonts (next/font parity): the build fetches the woff2, serves it
|
|
137
137
|
// same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
|
|
138
|
-
// size-adjusted fallback face so there's no layout shift. globals.css reads
|
|
139
|
-
// via `var(--font-sans, …)`; layout.tsx
|
|
138
|
+
// size-adjusted fallback face so there's no layout shift. globals.css reads
|
|
139
|
+
// them via `var(--font-sans, …)` and `var(--font-display, …)`; layout.tsx
|
|
140
|
+
// carries no font <link>.
|
|
140
141
|
fonts: [
|
|
142
|
+
// Body text, labels, buttons, prices.
|
|
141
143
|
font({
|
|
142
|
-
family: "
|
|
144
|
+
family: "DM Sans",
|
|
143
145
|
variable: "--font-sans",
|
|
144
|
-
weights: ["400", "500", "600"
|
|
146
|
+
weights: ["400", "500", "600"],
|
|
147
|
+
subsets: ["latin"],
|
|
148
|
+
display: "swap",
|
|
149
|
+
preload: true,
|
|
150
|
+
}),
|
|
151
|
+
// The serif for the brand name, headings and product names.
|
|
152
|
+
font({
|
|
153
|
+
family: "Newsreader",
|
|
154
|
+
variable: "--font-display",
|
|
155
|
+
weights: ["300", "400", "500"],
|
|
156
|
+
styles: ["normal", "italic"],
|
|
145
157
|
subsets: ["latin"],
|
|
146
158
|
display: "swap",
|
|
147
159
|
preload: true,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { ShoppingBag } from "lucide-react";
|
|
5
|
+
import { cart, countItems, useCart } from "@/lib/cart-store";
|
|
6
|
+
|
|
7
|
+
// The nav cart button. Reads the shared cart store so the count matches the
|
|
8
|
+
// grid, and opens the cart panel rendered by the shop island. The count
|
|
9
|
+
// re-mounts on change, which replays its pop-in animation.
|
|
10
|
+
export function CartButton() {
|
|
11
|
+
const { lines } = useCart();
|
|
12
|
+
const count = countItems(lines);
|
|
13
|
+
return (
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
onClick={cart.open}
|
|
17
|
+
aria-label={count === 1 ? "Cart, 1 item" : `Cart, ${count} items`}
|
|
18
|
+
className="inline-flex h-10 items-center gap-2 rounded-full pr-1 pl-3 text-[14px] text-ink transition-colors duration-300 hover:bg-ink/[0.05]"
|
|
19
|
+
>
|
|
20
|
+
<ShoppingBag className="size-[18px]" strokeWidth={1.25} />
|
|
21
|
+
<span className="hidden sm:inline">Cart</span>
|
|
22
|
+
<span
|
|
23
|
+
key={count}
|
|
24
|
+
className={
|
|
25
|
+
"inline-flex h-6 min-w-6 items-center justify-center rounded-full px-1.5 text-[12px] font-medium tabular-nums transition-colors duration-300 " +
|
|
26
|
+
(count > 0 ? "animate-in zoom-in-50 bg-ink text-warm-white duration-300" : "bg-ink/[0.07] text-ink/60")
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
{count}
|
|
30
|
+
</span>
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -1,96 +1,26 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
|
|
3
|
+
// Server-rendered layout helpers for the storefront pages. Restyle here and
|
|
4
|
+
// every page follows. The brand accent (`text-brand`, `bg-brand`) comes from
|
|
5
|
+
// CSS vars set on <html> in app/layout.tsx, which read lib/site.config.ts.
|
|
7
6
|
|
|
8
|
-
// Shared container
|
|
9
|
-
export const WRAP = "mx-auto w-full max-w-
|
|
7
|
+
// Shared container. The storefront runs wide so product photos stay large.
|
|
8
|
+
export const WRAP = "mx-auto w-full max-w-[1360px] px-5 sm:px-8 lg:px-12";
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
);
|
|
17
|
-
}
|
|
10
|
+
// Pill buttons. `BUTTON_DARK` is the primary action; `BUTTON_LINE` sits beside it.
|
|
11
|
+
export const BUTTON_DARK =
|
|
12
|
+
"inline-flex h-12 items-center justify-center gap-2 rounded-full bg-ink px-7 text-[14.5px] font-medium text-warm-white transition-[background-color,transform] duration-300 ease-out-quint hover:bg-brand active:scale-[0.98]";
|
|
13
|
+
export const BUTTON_LINE =
|
|
14
|
+
"inline-flex h-12 items-center justify-center gap-2 rounded-full border border-ink/25 px-7 text-[14.5px] font-medium text-ink transition-[background-color,border-color,transform] duration-300 ease-out-quint hover:border-ink hover:bg-ink/[0.04] active:scale-[0.98]";
|
|
18
15
|
|
|
19
|
-
//
|
|
20
|
-
export function
|
|
16
|
+
// A section heading in the display serif.
|
|
17
|
+
export function SectionHeading({ id, children }: { id?: string; children: React.ReactNode }) {
|
|
21
18
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
19
|
+
<h2
|
|
20
|
+
id={id}
|
|
21
|
+
className="font-display text-[2.5rem] font-light leading-[1.02] tracking-[-0.02em] text-balance sm:text-[3.5rem]"
|
|
22
|
+
>
|
|
24
23
|
{children}
|
|
25
|
-
</
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function Divider() {
|
|
30
|
-
return (
|
|
31
|
-
<div className={WRAP}>
|
|
32
|
-
<div className="border-t border-zinc-200/70" />
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function SectionHead({
|
|
38
|
-
eyebrow,
|
|
39
|
-
title,
|
|
40
|
-
body,
|
|
41
|
-
}: {
|
|
42
|
-
eyebrow: string;
|
|
43
|
-
title: string;
|
|
44
|
-
body?: string;
|
|
45
|
-
}) {
|
|
46
|
-
return (
|
|
47
|
-
<div>
|
|
48
|
-
<Eyebrow>{eyebrow}</Eyebrow>
|
|
49
|
-
<h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
|
|
50
|
-
{title}
|
|
51
|
-
</h2>
|
|
52
|
-
{body ? (
|
|
53
|
-
<p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
|
|
54
|
-
{body}
|
|
55
|
-
</p>
|
|
56
|
-
) : null}
|
|
57
|
-
</div>
|
|
24
|
+
</h2>
|
|
58
25
|
);
|
|
59
26
|
}
|
|
60
|
-
|
|
61
|
-
// A grid of value props — icon + title + body.
|
|
62
|
-
export function FeatureGrid({
|
|
63
|
-
items,
|
|
64
|
-
}: {
|
|
65
|
-
items: { title: string; body: string; icon?: string }[];
|
|
66
|
-
}) {
|
|
67
|
-
return (
|
|
68
|
-
<div className="grid gap-6 sm:grid-cols-3">
|
|
69
|
-
{items.map((f) => (
|
|
70
|
-
<div key={f.title}>
|
|
71
|
-
{f.icon ? (
|
|
72
|
-
<span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
|
|
73
|
-
{f.icon}
|
|
74
|
-
</span>
|
|
75
|
-
) : null}
|
|
76
|
-
<h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
|
|
77
|
-
{f.title}
|
|
78
|
-
</h3>
|
|
79
|
-
<p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
|
|
80
|
-
{f.body}
|
|
81
|
-
</p>
|
|
82
|
-
</div>
|
|
83
|
-
))}
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Initials for testimonial avatars, so the cards look finished without a photo.
|
|
89
|
-
export function initials(name: string) {
|
|
90
|
-
return name
|
|
91
|
-
.split(/\s+/)
|
|
92
|
-
.map((w) => w[0])
|
|
93
|
-
.join("")
|
|
94
|
-
.slice(0, 2)
|
|
95
|
-
.toUpperCase();
|
|
96
|
-
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useShelf } from "@/lib/shelf-store";
|
|
5
|
+
|
|
6
|
+
// The live stock line in the hero. The product grid publishes shelf totals to
|
|
7
|
+
// lib/shelf-store.ts from its live Product query, so this count drops for
|
|
8
|
+
// every open page when someone checks out. Renders a fixed-width placeholder
|
|
9
|
+
// until the first totals arrive, so the hero does not shift.
|
|
10
|
+
export function ShelfCount() {
|
|
11
|
+
const shelf = useShelf();
|
|
12
|
+
return (
|
|
13
|
+
<p className="flex items-center gap-3 text-[13.5px] text-ink/75" aria-live="polite">
|
|
14
|
+
<span className="relative flex size-2">
|
|
15
|
+
<span className="ping-soft absolute inset-0 rounded-full bg-[#5d7a4a]" />
|
|
16
|
+
<span className="relative size-2 rounded-full bg-[#5d7a4a]" />
|
|
17
|
+
</span>
|
|
18
|
+
{shelf ? (
|
|
19
|
+
<span>
|
|
20
|
+
<span key={shelf.units} className="stock-tick rounded px-0.5 font-medium tabular-nums text-ink">
|
|
21
|
+
{shelf.units}
|
|
22
|
+
</span>{" "}
|
|
23
|
+
pieces on the studio shelf
|
|
24
|
+
{shelf.low > 0 ? (
|
|
25
|
+
<>
|
|
26
|
+
<span className="mx-2 text-ink/30">/</span>
|
|
27
|
+
<span className="tabular-nums">{shelf.low}</span> running low
|
|
28
|
+
</>
|
|
29
|
+
) : null}
|
|
30
|
+
</span>
|
|
31
|
+
) : (
|
|
32
|
+
<span className="inline-block h-4 w-56 rounded bg-ink/[0.06]" />
|
|
33
|
+
)}
|
|
34
|
+
</p>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { action, v } from "@pylonsync/functions";
|
|
2
2
|
import { stripeRequest, assertSafeRedirectUrl } from "@pylonsync/stripe";
|
|
3
|
-
import type
|
|
3
|
+
import { shippingCents, type CheckoutResult } from "../lib/shop";
|
|
4
|
+
import { siteConfig } from "../lib/site.config";
|
|
4
5
|
|
|
5
6
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6
7
|
const CURRENCY = "usd";
|
|
@@ -92,6 +93,11 @@ export default action<
|
|
|
92
93
|
// If the session can't be created (bad key, network error, Stripe down) we
|
|
93
94
|
// MUST release the held stock — there's no session that would ever expire to
|
|
94
95
|
// do it for us, so without this the units would be stranded off the shelf.
|
|
96
|
+
// Shipping follows the same terms the cart shows: a flat rate, free at or
|
|
97
|
+
// above the threshold. Prices come from the held lines, not the client.
|
|
98
|
+
const subtotal = held.lines.reduce((s, l) => s + l.unitPriceCents * l.qty, 0);
|
|
99
|
+
const shipping = shippingCents(subtotal, siteConfig.checkout.shipping);
|
|
100
|
+
|
|
95
101
|
let session: { id: string; url: string } | undefined;
|
|
96
102
|
try {
|
|
97
103
|
session = await stripeRequest<{ id: string; url: string }>(
|
|
@@ -114,6 +120,16 @@ export default action<
|
|
|
114
120
|
product_data: { name: l.name },
|
|
115
121
|
},
|
|
116
122
|
})),
|
|
123
|
+
shipping_address_collection: { allowed_countries: ["US"] },
|
|
124
|
+
shipping_options: [
|
|
125
|
+
{
|
|
126
|
+
shipping_rate_data: {
|
|
127
|
+
type: "fixed_amount",
|
|
128
|
+
display_name: shipping === 0 ? "Free shipping" : "Standard shipping",
|
|
129
|
+
fixed_amount: { amount: shipping, currency: CURRENCY },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
117
133
|
},
|
|
118
134
|
);
|
|
119
135
|
} catch {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A tiny client-side cart store shared by the product grid (app/(marketing)/
|
|
2
|
+
// shop-client.tsx) and the nav cart button (components/cart-button.tsx). The
|
|
3
|
+
// two live in different client islands, so React state can't be lifted between
|
|
4
|
+
// them; this module holds the cart and both islands subscribe with
|
|
5
|
+
// `useSyncExternalStore`. Cart contents are slug → qty; the grid clamps them to
|
|
6
|
+
// live stock when it renders.
|
|
7
|
+
|
|
8
|
+
import { useSyncExternalStore } from "react";
|
|
9
|
+
|
|
10
|
+
export type CartLines = Record<string, number>;
|
|
11
|
+
|
|
12
|
+
type CartState = { lines: CartLines; open: boolean };
|
|
13
|
+
|
|
14
|
+
let state: CartState = { lines: {}, open: false };
|
|
15
|
+
const listeners = new Set<() => void>();
|
|
16
|
+
|
|
17
|
+
function emit() {
|
|
18
|
+
listeners.forEach((l) => l());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function subscribe(listener: () => void) {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => listeners.delete(listener);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getSnapshot() {
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The server render has no cart; every island sees the same empty snapshot.
|
|
31
|
+
const SERVER_STATE: CartState = { lines: {}, open: false };
|
|
32
|
+
function getServerSnapshot() {
|
|
33
|
+
return SERVER_STATE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useCart() {
|
|
37
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const cart = {
|
|
41
|
+
setLines(update: (lines: CartLines) => CartLines) {
|
|
42
|
+
state = { ...state, lines: update(state.lines) };
|
|
43
|
+
emit();
|
|
44
|
+
},
|
|
45
|
+
open() {
|
|
46
|
+
state = { ...state, open: true };
|
|
47
|
+
emit();
|
|
48
|
+
},
|
|
49
|
+
close() {
|
|
50
|
+
state = { ...state, open: false };
|
|
51
|
+
emit();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function countItems(lines: CartLines) {
|
|
56
|
+
return Object.values(lines).reduce((s, q) => s + q, 0);
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Live shelf totals, shared between client islands. The product grid
|
|
2
|
+
// (app/(marketing)/shop-client.tsx) owns the live Product query and publishes
|
|
3
|
+
// the totals here; the hero's shelf count (components/shelf-count.tsx) reads
|
|
4
|
+
// them. One query and one guest session serve both islands.
|
|
5
|
+
|
|
6
|
+
import { useSyncExternalStore } from "react";
|
|
7
|
+
|
|
8
|
+
export type ShelfTotals = { units: number; low: number; soldOut: number } | null;
|
|
9
|
+
|
|
10
|
+
let totals: ShelfTotals = null;
|
|
11
|
+
const listeners = new Set<() => void>();
|
|
12
|
+
|
|
13
|
+
function subscribe(listener: () => void) {
|
|
14
|
+
listeners.add(listener);
|
|
15
|
+
return () => listeners.delete(listener);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function publishShelf(stocks: number[], lowAt: number) {
|
|
19
|
+
const next = {
|
|
20
|
+
units: stocks.reduce((s, n) => s + Math.max(0, n), 0),
|
|
21
|
+
low: stocks.filter((n) => n > 0 && n <= lowAt).length,
|
|
22
|
+
soldOut: stocks.filter((n) => n <= 0).length,
|
|
23
|
+
};
|
|
24
|
+
if (totals && totals.units === next.units && totals.low === next.low && totals.soldOut === next.soldOut) return;
|
|
25
|
+
totals = next;
|
|
26
|
+
listeners.forEach((l) => l());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useShelf(): ShelfTotals {
|
|
30
|
+
return useSyncExternalStore(
|
|
31
|
+
subscribe,
|
|
32
|
+
() => totals,
|
|
33
|
+
() => null,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -43,3 +43,30 @@ export type CheckoutResult =
|
|
|
43
43
|
export function formatPrice(cents: number): string {
|
|
44
44
|
return `$${(cents / 100).toFixed(cents % 100 === 0 ? 0 : 2)}`;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
// How a product's stock reads on the storefront. "low" at 3 or fewer.
|
|
48
|
+
export const LOW_STOCK_AT = 3;
|
|
49
|
+
|
|
50
|
+
export type StockStatus =
|
|
51
|
+
| { kind: "sold_out"; label: string }
|
|
52
|
+
| { kind: "low"; label: string }
|
|
53
|
+
| { kind: "in_stock"; label: string };
|
|
54
|
+
|
|
55
|
+
export function stockStatus(stock: number): StockStatus {
|
|
56
|
+
if (stock <= 0) return { kind: "sold_out", label: "Sold out" };
|
|
57
|
+
if (stock <= LOW_STOCK_AT) return { kind: "low", label: stock === 1 ? "Last one" : `Only ${stock} left` };
|
|
58
|
+
return { kind: "in_stock", label: `${stock} in stock` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Shipping terms: a flat rate, free at or above a subtotal threshold. The cart
|
|
62
|
+
// shows these numbers and `checkout` charges them on the Stripe session.
|
|
63
|
+
export type ShippingTerms = { flatCents: number; freeOverCents: number };
|
|
64
|
+
|
|
65
|
+
// Cents still needed to reach free shipping; 0 once the subtotal qualifies.
|
|
66
|
+
export function freeShippingRemaining(subtotalCents: number, terms: ShippingTerms): number {
|
|
67
|
+
return Math.max(0, terms.freeOverCents - subtotalCents);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function shippingCents(subtotalCents: number, terms: ShippingTerms): number {
|
|
71
|
+
return freeShippingRemaining(subtotalCents, terms) === 0 ? 0 : terms.flatCents;
|
|
72
|
+
}
|