@pylonsync/create-pylon 0.19.1 → 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,18 +1,33 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import { db, callFn } from "@pylonsync/react";
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { db, callFn, useRouter } from "@pylonsync/react";
|
|
5
5
|
import { EnsureGuest } from "@pylonsync/client";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
import { Minus, Plus, X } from "lucide-react";
|
|
7
|
+
import { siteConfig, productBySlug } from "@/lib/site.config";
|
|
8
|
+
import {
|
|
9
|
+
formatPrice,
|
|
10
|
+
freeShippingRemaining,
|
|
11
|
+
shippingCents,
|
|
12
|
+
stockStatus,
|
|
13
|
+
LOW_STOCK_AT,
|
|
14
|
+
type CheckoutResult,
|
|
15
|
+
} from "@/lib/shop";
|
|
16
|
+
import { cart, countItems, useCart, type CartLines } from "@/lib/cart-store";
|
|
17
|
+
import { publishShelf } from "@/lib/shelf-store";
|
|
18
|
+
|
|
19
|
+
// The storefront, and the realtime part of this template. The grid subscribes
|
|
20
|
+
// to the public Product entity with `db.useQuery`, so every card's stock
|
|
21
|
+
// ("Only 2 left") ticks down, and flips to "Sold out", for everyone with the
|
|
22
|
+
// page open when someone checks out. Shopping is a normal cart: add items, then
|
|
23
|
+
// check out (name + email). The `checkout` action holds stock under a
|
|
24
|
+
// per-product lock (so the cart can't oversell) and, when Stripe is configured,
|
|
25
|
+
// hands the browser off to a hosted Stripe Checkout page; otherwise it records
|
|
26
|
+
// a reserved order and the shopper lands on /success.
|
|
27
|
+
//
|
|
28
|
+
// Cart contents live in lib/cart-store.ts so the nav's cart button (a separate
|
|
29
|
+
// client island) shows the same count and can open the panel rendered here.
|
|
30
|
+
// Shelf totals go to lib/shelf-store.ts for the hero's live count.
|
|
16
31
|
//
|
|
17
32
|
// Wrapped in <EnsureGuest> so the sync connection is established for anonymous
|
|
18
33
|
// visitors. The guest session holds no PII and can't read the Order table; the
|
|
@@ -28,6 +43,8 @@ interface ProductRow {
|
|
|
28
43
|
stock: number;
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
type CartEntry = { product: ProductRow; qty: number };
|
|
47
|
+
|
|
31
48
|
export function ShopGrid() {
|
|
32
49
|
return (
|
|
33
50
|
<EnsureGuest fallback={<GridSkeleton />}>
|
|
@@ -38,15 +55,17 @@ export function ShopGrid() {
|
|
|
38
55
|
|
|
39
56
|
function Shop() {
|
|
40
57
|
const { data: products, loading } = db.useQuery<ProductRow>("Product");
|
|
41
|
-
const
|
|
42
|
-
const [checkingOut, setCheckingOut] = useState(false);
|
|
43
|
-
const [placed, setPlaced] = useState(false);
|
|
58
|
+
const { lines, open } = useCart();
|
|
44
59
|
|
|
45
60
|
// Seed the catalog from config on first visit (idempotent server-side).
|
|
46
61
|
useEffect(() => {
|
|
47
62
|
void callFn("seedProducts", {});
|
|
48
63
|
}, []);
|
|
49
64
|
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (products.length > 0) publishShelf(products.map((p) => p.stock), LOW_STOCK_AT);
|
|
67
|
+
}, [products]);
|
|
68
|
+
|
|
50
69
|
const bySlug = useMemo(() => new Map(products.map((p) => [p.slug, p])), [products]);
|
|
51
70
|
const ordered = useMemo(() => {
|
|
52
71
|
const idx = new Map(siteConfig.products.items.map((p, i) => [p.slug, i]));
|
|
@@ -56,7 +75,7 @@ function Shop() {
|
|
|
56
75
|
function setQty(slug: string, qty: number) {
|
|
57
76
|
const stock = bySlug.get(slug)?.stock ?? 0;
|
|
58
77
|
const clamped = Math.max(0, Math.min(qty, stock));
|
|
59
|
-
|
|
78
|
+
cart.setLines((c: CartLines) => {
|
|
60
79
|
const next = { ...c };
|
|
61
80
|
if (clamped <= 0) delete next[slug];
|
|
62
81
|
else next[slug] = clamped;
|
|
@@ -65,228 +84,253 @@ function Shop() {
|
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
// Cart entries that still reference a real product, clamped to live stock.
|
|
68
|
-
const entries = Object.entries(
|
|
87
|
+
const entries = Object.entries(lines)
|
|
69
88
|
.map(([slug, qty]) => {
|
|
70
89
|
const p = bySlug.get(slug);
|
|
71
90
|
return p ? { product: p, qty: Math.min(qty, p.stock) } : null;
|
|
72
91
|
})
|
|
73
|
-
.filter((x): x is
|
|
74
|
-
const itemCount = entries.reduce((s, e) => s + e.qty, 0);
|
|
75
|
-
const total = entries.reduce((s, e) => s + e.product.priceCents * e.qty, 0);
|
|
92
|
+
.filter((x): x is CartEntry => !!x && x.qty > 0);
|
|
76
93
|
|
|
77
94
|
if (loading && products.length === 0) return <GridSkeleton />;
|
|
78
95
|
|
|
79
96
|
return (
|
|
80
97
|
<>
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
{/* The first product is featured: two columns wide on tablet, and two
|
|
99
|
+
columns by two rows on desktop, beside a 2x2 block of the rest. */}
|
|
100
|
+
<div className={GRID}>
|
|
101
|
+
{ordered.map((p, i) => (
|
|
83
102
|
<ProductCard
|
|
84
103
|
key={p.id}
|
|
104
|
+
featured={i === 0}
|
|
85
105
|
product={p}
|
|
86
|
-
qty={
|
|
87
|
-
onAdd={() => setQty(p.slug, (
|
|
106
|
+
qty={lines[p.slug] ?? 0}
|
|
107
|
+
onAdd={() => setQty(p.slug, (lines[p.slug] ?? 0) + 1)}
|
|
88
108
|
onSet={(q) => setQty(p.slug, q)}
|
|
89
109
|
/>
|
|
90
110
|
))}
|
|
91
111
|
</div>
|
|
92
112
|
|
|
93
|
-
{
|
|
94
|
-
{itemCount > 0 ? (
|
|
95
|
-
<div className="pointer-events-none fixed inset-x-0 bottom-0 z-40 px-4 pb-4">
|
|
96
|
-
<div className="pointer-events-auto mx-auto flex max-w-md items-center justify-between gap-4 rounded-full border border-zinc-200 bg-white px-5 py-3 shadow-[0_16px_48px_-16px_rgba(0,0,0,0.3)]">
|
|
97
|
-
<span className="text-[14px] font-medium text-zinc-900">
|
|
98
|
-
{itemCount} {itemCount === 1 ? "item" : "items"}
|
|
99
|
-
<span className="ml-2 text-zinc-400">·</span>
|
|
100
|
-
<span className="ml-2 tabular-nums">{formatPrice(total)}</span>
|
|
101
|
-
</span>
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
onClick={() => {
|
|
105
|
-
setPlaced(false);
|
|
106
|
-
setCheckingOut(true);
|
|
107
|
-
}}
|
|
108
|
-
className="inline-flex h-9 items-center rounded-full bg-brand px-5 text-[14px] font-medium text-white transition-opacity hover:opacity-90"
|
|
109
|
-
>
|
|
110
|
-
Checkout →
|
|
111
|
-
</button>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
) : null}
|
|
115
|
-
|
|
116
|
-
{/* Checkout modal */}
|
|
117
|
-
{checkingOut ? (
|
|
118
|
-
<CheckoutModal
|
|
119
|
-
entries={entries}
|
|
120
|
-
total={total}
|
|
121
|
-
placed={placed}
|
|
122
|
-
onClose={() => setCheckingOut(false)}
|
|
123
|
-
onSetQty={setQty}
|
|
124
|
-
onPlaced={(succeededSlugs) => {
|
|
125
|
-
setCart((c) => {
|
|
126
|
-
const next = { ...c };
|
|
127
|
-
succeededSlugs.forEach((s) => delete next[s]);
|
|
128
|
-
return next;
|
|
129
|
-
});
|
|
130
|
-
setPlaced(true);
|
|
131
|
-
}}
|
|
132
|
-
/>
|
|
133
|
-
) : null}
|
|
113
|
+
{open ? <CartPanel entries={entries} onClose={cart.close} onSetQty={setQty} /> : null}
|
|
134
114
|
</>
|
|
135
115
|
);
|
|
136
116
|
}
|
|
137
117
|
|
|
118
|
+
const GRID = "grid gap-x-6 gap-y-14 sm:grid-cols-2 lg:grid-cols-4 lg:gap-x-8";
|
|
119
|
+
const FEATURED_CELL = "sm:col-span-2 lg:row-span-2";
|
|
120
|
+
|
|
138
121
|
function ProductCard({
|
|
122
|
+
featured,
|
|
139
123
|
product,
|
|
140
124
|
qty,
|
|
141
125
|
onAdd,
|
|
142
126
|
onSet,
|
|
143
127
|
}: {
|
|
128
|
+
featured?: boolean;
|
|
144
129
|
product: ProductRow;
|
|
145
130
|
qty: number;
|
|
146
131
|
onAdd: () => void;
|
|
147
132
|
onSet: (q: number) => void;
|
|
148
133
|
}) {
|
|
149
134
|
const soldOut = product.stock <= 0;
|
|
135
|
+
const size = productBySlug(product.slug)?.size;
|
|
150
136
|
return (
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
137
|
+
<article className={"group flex flex-col " + (featured ? FEATURED_CELL : "")}>
|
|
138
|
+
<div
|
|
139
|
+
className={
|
|
140
|
+
"relative overflow-hidden bg-stone " +
|
|
141
|
+
(featured ? "aspect-[4/5] sm:aspect-[5/4] lg:aspect-auto lg:min-h-[480px] lg:flex-1" : "aspect-[4/5]")
|
|
142
|
+
}
|
|
143
|
+
>
|
|
144
|
+
<ProductPhoto
|
|
145
|
+
image={product.image}
|
|
146
|
+
name={product.name}
|
|
147
|
+
className={
|
|
148
|
+
"size-full object-cover transition-[transform,opacity,filter] duration-[1400ms] ease-out-quint group-hover:scale-[1.035] " +
|
|
149
|
+
(soldOut ? "opacity-60 grayscale-[0.6]" : "")
|
|
150
|
+
}
|
|
151
|
+
/>
|
|
152
|
+
{soldOut ? (
|
|
153
|
+
<span className="absolute top-4 left-4 rounded-full bg-warm-white/90 px-3 py-1 text-[12px] font-medium text-ink">
|
|
154
|
+
Sold out
|
|
158
155
|
</span>
|
|
159
|
-
</div>
|
|
160
|
-
{product.description ? (
|
|
161
|
-
<p className="mt-1.5 text-[13px] leading-relaxed text-zinc-500">{product.description}</p>
|
|
162
156
|
) : null}
|
|
157
|
+
</div>
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
<div className="mt-5 flex items-baseline justify-between gap-4">
|
|
160
|
+
<h3
|
|
161
|
+
className={
|
|
162
|
+
"font-display leading-tight tracking-[-0.01em] text-ink " +
|
|
163
|
+
(featured ? "text-[2rem] sm:text-[2.25rem]" : "text-[1.375rem]")
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
{product.name}
|
|
167
|
+
</h3>
|
|
168
|
+
<span className="shrink-0 text-[15px] font-medium tabular-nums text-ink">
|
|
169
|
+
{formatPrice(product.priceCents)}
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
{product.description ? (
|
|
173
|
+
<p className="mt-1.5 max-w-[38ch] text-[14px] leading-relaxed text-ink-2">{product.description}</p>
|
|
174
|
+
) : null}
|
|
175
|
+
{size ? <p className="mt-1 text-[13px] text-ink-2/75">{size}</p> : null}
|
|
167
176
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
</button>
|
|
189
|
-
</div>
|
|
190
|
-
) : (
|
|
191
|
-
<button
|
|
192
|
-
type="button"
|
|
193
|
-
disabled={soldOut}
|
|
194
|
-
onClick={onAdd}
|
|
195
|
-
className={
|
|
196
|
-
"inline-flex h-10 w-full items-center justify-center rounded-lg text-sm font-medium transition-colors " +
|
|
197
|
-
(soldOut
|
|
198
|
-
? "cursor-not-allowed bg-zinc-100 text-zinc-400"
|
|
199
|
-
: "bg-brand text-white hover:opacity-90")
|
|
200
|
-
}
|
|
201
|
-
>
|
|
202
|
-
{soldOut ? "Sold out" : "Add to cart"}
|
|
203
|
-
</button>
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
177
|
+
<div className="mt-auto flex min-h-15 items-center justify-between gap-4 pt-5">
|
|
178
|
+
<StockLine stock={product.stock} />
|
|
179
|
+
{qty > 0 ? (
|
|
180
|
+
<Stepper
|
|
181
|
+
qty={qty}
|
|
182
|
+
max={product.stock}
|
|
183
|
+
onDec={() => onSet(qty - 1)}
|
|
184
|
+
onInc={onAdd}
|
|
185
|
+
label={product.name}
|
|
186
|
+
/>
|
|
187
|
+
) : soldOut ? null : (
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={onAdd}
|
|
191
|
+
className="inline-flex h-10 shrink-0 items-center justify-center gap-1.5 rounded-full border border-ink/25 px-5 text-[13.5px] font-medium text-ink transition-[background-color,color,border-color,transform] duration-300 ease-out-quint hover:border-ink hover:bg-ink hover:text-warm-white active:scale-[0.97]"
|
|
192
|
+
>
|
|
193
|
+
<Plus className="size-3.5" strokeWidth={1.5} />
|
|
194
|
+
Add to cart
|
|
195
|
+
</button>
|
|
196
|
+
)}
|
|
206
197
|
</div>
|
|
207
|
-
</
|
|
198
|
+
</article>
|
|
208
199
|
);
|
|
209
200
|
}
|
|
210
201
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const isPhoto = /^(https?:\/\/|\/)/.test(image);
|
|
217
|
-
if (isPhoto) {
|
|
202
|
+
// A product photo from lib/site.config.ts. If the file is missing, the frame
|
|
203
|
+
// shows the product name on the stone background instead of a broken image.
|
|
204
|
+
function ProductPhoto({ image, name, className }: { image: string; name: string; className?: string }) {
|
|
205
|
+
const [failed, setFailed] = useState(false);
|
|
206
|
+
if (failed || !image) {
|
|
218
207
|
return (
|
|
219
|
-
<div className="
|
|
220
|
-
|
|
221
|
-
<img
|
|
222
|
-
src={image}
|
|
223
|
-
alt=""
|
|
224
|
-
className={"size-full object-cover " + (soldOut ? "opacity-40 grayscale" : "")}
|
|
225
|
-
/>
|
|
208
|
+
<div className="grid size-full place-items-center p-6 text-center">
|
|
209
|
+
<span className="font-display text-[1.75rem] font-light text-ink/40">{name}</span>
|
|
226
210
|
</div>
|
|
227
211
|
);
|
|
228
212
|
}
|
|
213
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
214
|
+
return <img src={image} alt={name} loading="lazy" onError={() => setFailed(true)} className={className} />;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Stock status with a colored dot. When the live count changes, the line
|
|
218
|
+
// flashes once so the change is visible.
|
|
219
|
+
function StockLine({ stock }: { stock: number }) {
|
|
220
|
+
const status = stockStatus(stock);
|
|
221
|
+
const flash = useChangeCount(stock);
|
|
222
|
+
const dot =
|
|
223
|
+
status.kind === "sold_out" ? "bg-ink/25" : status.kind === "low" ? "bg-brand" : "bg-[#5d7a4a]";
|
|
229
224
|
return (
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
225
|
+
<p
|
|
226
|
+
key={flash}
|
|
227
|
+
className={
|
|
228
|
+
"flex items-center gap-2 rounded-full px-1 -mx-1 text-[13px] tabular-nums " +
|
|
229
|
+
(flash > 0 ? "stock-tick " : "") +
|
|
230
|
+
(status.kind === "low" ? "font-medium text-brand" : "text-ink-2")
|
|
231
|
+
}
|
|
232
|
+
aria-live="polite"
|
|
233
|
+
>
|
|
234
|
+
<span className="relative flex size-1.5">
|
|
235
|
+
{status.kind === "low" ? <span className={`ping-soft absolute inset-0 rounded-full ${dot}`} /> : null}
|
|
236
|
+
<span className={`relative size-1.5 rounded-full ${dot}`} />
|
|
234
237
|
</span>
|
|
235
|
-
|
|
238
|
+
{status.label}
|
|
239
|
+
</p>
|
|
236
240
|
);
|
|
237
241
|
}
|
|
238
242
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
// Counts how many times `value` has changed since mount (0 on first render).
|
|
244
|
+
function useChangeCount(value: number) {
|
|
245
|
+
const prev = useRef(value);
|
|
246
|
+
const [count, setCount] = useState(0);
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (prev.current !== value) {
|
|
249
|
+
prev.current = value;
|
|
250
|
+
setCount((c) => c + 1);
|
|
251
|
+
}
|
|
252
|
+
}, [value]);
|
|
253
|
+
return count;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function Stepper({
|
|
257
|
+
qty,
|
|
258
|
+
max,
|
|
259
|
+
onDec,
|
|
260
|
+
onInc,
|
|
261
|
+
label,
|
|
262
|
+
small,
|
|
263
|
+
}: {
|
|
264
|
+
qty: number;
|
|
265
|
+
max: number;
|
|
266
|
+
onDec: () => void;
|
|
267
|
+
onInc: () => void;
|
|
268
|
+
label: string;
|
|
269
|
+
small?: boolean;
|
|
270
|
+
}) {
|
|
271
|
+
const btn =
|
|
272
|
+
"flex items-center justify-center rounded-full text-ink/70 transition-colors duration-200 hover:bg-ink/[0.06] hover:text-ink disabled:opacity-30 disabled:hover:bg-transparent " +
|
|
273
|
+
(small ? "size-7" : "size-8");
|
|
248
274
|
return (
|
|
249
|
-
<
|
|
275
|
+
<div
|
|
250
276
|
className={
|
|
251
|
-
"inline-flex items-center
|
|
252
|
-
(low ? "bg-red-50 text-red-600" : "bg-green-50 text-green-700")
|
|
277
|
+
"inline-flex shrink-0 items-center rounded-full border border-ink/20 " + (small ? "h-9 px-0.5" : "h-10 px-1")
|
|
253
278
|
}
|
|
254
279
|
>
|
|
255
|
-
<
|
|
256
|
-
<
|
|
257
|
-
|
|
280
|
+
<button type="button" onClick={onDec} aria-label={`Remove one ${label}`} className={btn}>
|
|
281
|
+
<Minus className="size-3.5" strokeWidth={1.5} />
|
|
282
|
+
</button>
|
|
283
|
+
<span className="min-w-7 text-center text-[13.5px] font-medium tabular-nums text-ink" aria-label={`${qty} in cart`}>
|
|
284
|
+
{qty}
|
|
258
285
|
</span>
|
|
259
|
-
{
|
|
260
|
-
|
|
286
|
+
<button type="button" onClick={onInc} disabled={qty >= max} aria-label={`Add one ${label}`} className={btn}>
|
|
287
|
+
<Plus className="size-3.5" strokeWidth={1.5} />
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
261
290
|
);
|
|
262
291
|
}
|
|
263
292
|
|
|
264
|
-
|
|
293
|
+
// The cart, as a panel sliding in from the right. Opened from the nav cart
|
|
294
|
+
// button (components/cart-button.tsx) through the shared store.
|
|
295
|
+
function CartPanel({
|
|
265
296
|
entries,
|
|
266
|
-
total,
|
|
267
|
-
placed,
|
|
268
297
|
onClose,
|
|
269
298
|
onSetQty,
|
|
270
|
-
onPlaced,
|
|
271
299
|
}: {
|
|
272
|
-
entries:
|
|
273
|
-
total: number;
|
|
274
|
-
placed: boolean;
|
|
300
|
+
entries: CartEntry[];
|
|
275
301
|
onClose: () => void;
|
|
276
302
|
onSetQty: (slug: string, qty: number) => void;
|
|
277
|
-
onPlaced: (succeededSlugs: string[]) => void;
|
|
278
303
|
}) {
|
|
304
|
+
const router = useRouter();
|
|
279
305
|
const [name, setName] = useState("");
|
|
280
306
|
const [email, setEmail] = useState("");
|
|
281
307
|
const [status, setStatus] = useState<"idle" | "placing">("idle");
|
|
282
308
|
const [error, setError] = useState<string | null>(null);
|
|
283
|
-
|
|
309
|
+
|
|
310
|
+
const terms = siteConfig.checkout.shipping;
|
|
311
|
+
const subtotal = entries.reduce((s, e) => s + e.product.priceCents * e.qty, 0);
|
|
312
|
+
const shipping = shippingCents(subtotal, terms);
|
|
313
|
+
const remaining = freeShippingRemaining(subtotal, terms);
|
|
314
|
+
const count = countItems(Object.fromEntries(entries.map((e) => [e.product.slug, e.qty])));
|
|
315
|
+
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
function onKey(e: KeyboardEvent) {
|
|
318
|
+
if (e.key === "Escape") onClose();
|
|
319
|
+
}
|
|
320
|
+
window.addEventListener("keydown", onKey);
|
|
321
|
+
const overflow = document.body.style.overflow;
|
|
322
|
+
document.body.style.overflow = "hidden";
|
|
323
|
+
return () => {
|
|
324
|
+
window.removeEventListener("keydown", onKey);
|
|
325
|
+
document.body.style.overflow = overflow;
|
|
326
|
+
};
|
|
327
|
+
}, [onClose]);
|
|
284
328
|
|
|
285
329
|
async function submitCheckout(e: React.FormEvent) {
|
|
286
330
|
e.preventDefault();
|
|
287
331
|
if (status === "placing") return;
|
|
288
332
|
if (!name.trim() || !email.trim()) {
|
|
289
|
-
setError("
|
|
333
|
+
setError("Enter your name and email.");
|
|
290
334
|
return;
|
|
291
335
|
}
|
|
292
336
|
setStatus("placing");
|
|
@@ -304,130 +348,213 @@ function CheckoutModal({
|
|
|
304
348
|
});
|
|
305
349
|
} catch (err) {
|
|
306
350
|
setStatus("idle");
|
|
307
|
-
setError(err instanceof Error ? err.message : "Checkout failed.
|
|
351
|
+
setError(err instanceof Error ? err.message : "Checkout failed. Try again.");
|
|
308
352
|
return;
|
|
309
353
|
}
|
|
310
354
|
|
|
311
355
|
if (res.ok && res.mode === "stripe") {
|
|
312
|
-
// Hand off to Stripe's hosted checkout
|
|
356
|
+
// Hand off to Stripe's hosted checkout. The button stays busy while the
|
|
313
357
|
// browser navigates away. Stock is already held; the webhook settles it.
|
|
314
358
|
window.location.href = res.checkoutUrl;
|
|
315
359
|
return;
|
|
316
360
|
}
|
|
317
361
|
|
|
318
|
-
setStatus("idle");
|
|
319
362
|
if (res.ok && res.mode === "reserved") {
|
|
320
|
-
// No payment processor configured
|
|
321
|
-
|
|
322
|
-
|
|
363
|
+
// No payment processor configured: the order is held for the owner.
|
|
364
|
+
// Empty the cart and show the confirmation page.
|
|
365
|
+
const sold = res.soldOut;
|
|
366
|
+
cart.setLines(() => ({}));
|
|
367
|
+
cart.close();
|
|
368
|
+
const qs = new URLSearchParams({ order: "reserved" });
|
|
369
|
+
if (sold.length > 0) qs.set("sold", sold.join(","));
|
|
370
|
+
router.push(`/success?${qs.toString()}`);
|
|
323
371
|
return;
|
|
324
372
|
}
|
|
325
|
-
|
|
373
|
+
|
|
374
|
+
setStatus("idle");
|
|
326
375
|
if (res.reason === "sold_out") {
|
|
327
376
|
setError(`Sold out before checkout: ${res.soldOut.join(", ") || "those items"}. Nothing was ordered.`);
|
|
328
377
|
} else if (res.reason === "empty") {
|
|
329
378
|
setError("Your cart is empty.");
|
|
330
379
|
} else {
|
|
331
|
-
setError("Could not place the order.
|
|
380
|
+
setError("Could not place the order. Check your details.");
|
|
332
381
|
}
|
|
333
382
|
}
|
|
334
383
|
|
|
335
384
|
return (
|
|
336
|
-
<div className="fixed inset-0 z-50 flex
|
|
337
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
385
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
aria-label="Close cart"
|
|
389
|
+
onClick={onClose}
|
|
390
|
+
className="animate-in fade-in absolute inset-0 bg-espresso/35 duration-500"
|
|
391
|
+
/>
|
|
392
|
+
<aside
|
|
393
|
+
role="dialog"
|
|
394
|
+
aria-modal="true"
|
|
395
|
+
aria-label="Cart"
|
|
396
|
+
className="animate-in slide-in-from-right relative flex h-full w-full max-w-[460px] flex-col bg-warm-white shadow-[-40px_0_80px_-40px_rgba(34,29,24,0.35)] duration-500 ease-out-quint"
|
|
397
|
+
>
|
|
398
|
+
<div className="flex h-20 shrink-0 items-center justify-between px-6 sm:px-8">
|
|
399
|
+
<h2 className="font-display text-[1.75rem] font-light tracking-[-0.01em] text-ink">
|
|
400
|
+
Cart <span className="text-ink/40 tabular-nums">{count}</span>
|
|
401
|
+
</h2>
|
|
402
|
+
<button
|
|
403
|
+
type="button"
|
|
404
|
+
onClick={onClose}
|
|
405
|
+
aria-label="Close"
|
|
406
|
+
className="flex size-10 items-center justify-center rounded-full text-ink/70 transition-colors hover:bg-ink/[0.06] hover:text-ink"
|
|
407
|
+
>
|
|
408
|
+
<X className="size-5" strokeWidth={1.25} />
|
|
409
|
+
</button>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
{entries.length === 0 ? (
|
|
413
|
+
<div className="flex flex-1 flex-col items-center justify-center px-8 text-center">
|
|
414
|
+
<p className="font-display text-[1.5rem] font-light text-ink">Your cart is empty.</p>
|
|
349
415
|
<button
|
|
350
416
|
type="button"
|
|
351
417
|
onClick={onClose}
|
|
352
|
-
className="mt-
|
|
418
|
+
className="mt-6 inline-flex h-11 items-center rounded-full border border-ink/25 px-6 text-[14px] font-medium text-ink transition-colors hover:border-ink"
|
|
353
419
|
>
|
|
354
|
-
|
|
420
|
+
Back to the shelf
|
|
355
421
|
</button>
|
|
356
422
|
</div>
|
|
357
423
|
) : (
|
|
358
424
|
<>
|
|
359
|
-
<div className="
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
425
|
+
<div className="px-6 pb-5 sm:px-8">
|
|
426
|
+
<p className="text-[13px] text-ink-2">
|
|
427
|
+
{remaining > 0 ? (
|
|
428
|
+
<>
|
|
429
|
+
Add <span className="font-medium tabular-nums text-ink">{formatPrice(remaining)}</span> for free
|
|
430
|
+
shipping.
|
|
431
|
+
</>
|
|
432
|
+
) : (
|
|
433
|
+
"Your order ships free."
|
|
434
|
+
)}
|
|
435
|
+
</p>
|
|
436
|
+
<div className="mt-2.5 h-[2px] overflow-hidden rounded-full bg-ink/10">
|
|
437
|
+
<div
|
|
438
|
+
className="h-full rounded-full bg-ink transition-[width] duration-700 ease-out-quint"
|
|
439
|
+
style={{ width: `${Math.min(100, (subtotal / terms.freeOverCents) * 100)}%` }}
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
364
442
|
</div>
|
|
365
443
|
|
|
366
|
-
<ul className="
|
|
444
|
+
<ul className="flex-1 divide-y divide-ink/10 overflow-y-auto border-t border-ink/10 px-6 sm:px-8">
|
|
367
445
|
{entries.map(({ product, qty }) => (
|
|
368
|
-
<li key={product.slug} className="flex
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
<div className="truncate text-[14px] font-medium text-zinc-900">{product.name}</div>
|
|
372
|
-
<div className="text-[12.5px] text-zinc-500">{formatPrice(product.priceCents)} each</div>
|
|
446
|
+
<li key={product.slug} className="flex gap-4 py-5">
|
|
447
|
+
<div className="h-[100px] w-20 shrink-0 overflow-hidden bg-stone">
|
|
448
|
+
<ProductPhoto image={product.image} name="" className="size-full object-cover" />
|
|
373
449
|
</div>
|
|
374
|
-
<div className="flex
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
450
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
451
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
452
|
+
<span className="font-display truncate text-[1.1875rem] text-ink">{product.name}</span>
|
|
453
|
+
<span className="shrink-0 text-[14px] font-medium tabular-nums text-ink">
|
|
454
|
+
{formatPrice(product.priceCents * qty)}
|
|
455
|
+
</span>
|
|
456
|
+
</div>
|
|
457
|
+
<span className="mt-0.5 text-[12.5px] text-ink-2">
|
|
458
|
+
{formatPrice(product.priceCents)} each
|
|
459
|
+
</span>
|
|
460
|
+
<div className="mt-auto flex items-center justify-between pt-3">
|
|
461
|
+
<Stepper
|
|
462
|
+
small
|
|
463
|
+
qty={qty}
|
|
464
|
+
max={product.stock}
|
|
465
|
+
onDec={() => onSetQty(product.slug, qty - 1)}
|
|
466
|
+
onInc={() => onSetQty(product.slug, qty + 1)}
|
|
467
|
+
label={product.name}
|
|
468
|
+
/>
|
|
469
|
+
<button
|
|
470
|
+
type="button"
|
|
471
|
+
onClick={() => onSetQty(product.slug, 0)}
|
|
472
|
+
className="text-[12.5px] text-ink-2 underline decoration-ink/20 underline-offset-4 transition-colors hover:text-ink hover:decoration-ink"
|
|
473
|
+
>
|
|
474
|
+
Remove
|
|
475
|
+
</button>
|
|
476
|
+
</div>
|
|
378
477
|
</div>
|
|
379
|
-
<span className="w-16 shrink-0 text-right text-[14px] font-medium tabular-nums">
|
|
380
|
-
{formatPrice(product.priceCents * qty)}
|
|
381
|
-
</span>
|
|
382
478
|
</li>
|
|
383
479
|
))}
|
|
384
480
|
</ul>
|
|
385
481
|
|
|
386
|
-
{
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
482
|
+
<form onSubmit={submitCheckout} className="shrink-0 border-t border-ink/10 bg-stone/40 px-6 py-6 sm:px-8">
|
|
483
|
+
<dl className="space-y-1.5 text-[14px]">
|
|
484
|
+
<div className="flex justify-between text-ink-2">
|
|
485
|
+
<dt>Subtotal</dt>
|
|
486
|
+
<dd className="tabular-nums">{formatPrice(subtotal)}</dd>
|
|
487
|
+
</div>
|
|
488
|
+
<div className="flex justify-between text-ink-2">
|
|
489
|
+
<dt>Shipping</dt>
|
|
490
|
+
<dd className="tabular-nums">{shipping === 0 ? "Free" : formatPrice(shipping)}</dd>
|
|
393
491
|
</div>
|
|
394
|
-
<div className="
|
|
395
|
-
<
|
|
396
|
-
<
|
|
492
|
+
<div className="flex justify-between pt-1.5 text-[16px] font-medium text-ink">
|
|
493
|
+
<dt>Total</dt>
|
|
494
|
+
<dd className="tabular-nums">{formatPrice(subtotal + shipping)}</dd>
|
|
397
495
|
</div>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
496
|
+
</dl>
|
|
497
|
+
<div className="mt-5 grid gap-2.5 sm:grid-cols-2">
|
|
498
|
+
<label className="block">
|
|
499
|
+
<span className="sr-only">Name</span>
|
|
500
|
+
<input
|
|
501
|
+
value={name}
|
|
502
|
+
onChange={(e) => setName(e.target.value)}
|
|
503
|
+
placeholder="Name"
|
|
504
|
+
autoComplete="name"
|
|
505
|
+
className={inputCls}
|
|
506
|
+
/>
|
|
507
|
+
</label>
|
|
508
|
+
<label className="block">
|
|
509
|
+
<span className="sr-only">Email</span>
|
|
510
|
+
<input
|
|
511
|
+
type="email"
|
|
512
|
+
value={email}
|
|
513
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
514
|
+
placeholder="Email"
|
|
515
|
+
autoComplete="email"
|
|
516
|
+
className={inputCls}
|
|
517
|
+
/>
|
|
518
|
+
</label>
|
|
519
|
+
</div>
|
|
520
|
+
{error ? <p className="mt-3 text-[13px] text-[#a1301f]">{error}</p> : null}
|
|
521
|
+
<button
|
|
522
|
+
type="submit"
|
|
523
|
+
disabled={status === "placing"}
|
|
524
|
+
className="mt-4 inline-flex h-12 w-full items-center justify-center rounded-full bg-ink text-[14.5px] font-medium text-warm-white transition-[background-color,transform] duration-300 ease-out-quint hover:bg-brand active:scale-[0.99] disabled:opacity-60"
|
|
525
|
+
>
|
|
526
|
+
{status === "placing" ? "Placing order…" : `Check out · ${formatPrice(subtotal + shipping)}`}
|
|
527
|
+
</button>
|
|
528
|
+
</form>
|
|
411
529
|
</>
|
|
412
530
|
)}
|
|
413
|
-
</
|
|
531
|
+
</aside>
|
|
414
532
|
</div>
|
|
415
533
|
);
|
|
416
534
|
}
|
|
417
535
|
|
|
418
536
|
const inputCls =
|
|
419
|
-
"h-
|
|
537
|
+
"h-11 w-full rounded-full border border-ink/15 bg-warm-white px-4 text-[14px] text-ink outline-none transition-[border-color,box-shadow] duration-300 placeholder:text-ink-2/70 focus:border-ink/50 focus:ring-4 focus:ring-ink/[0.06]";
|
|
420
538
|
|
|
421
539
|
function GridSkeleton() {
|
|
422
540
|
return (
|
|
423
|
-
<div className=
|
|
424
|
-
{Array.from({ length:
|
|
425
|
-
<div key={i} className="
|
|
426
|
-
<div
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
541
|
+
<div className={GRID}>
|
|
542
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
543
|
+
<div key={i} className={"flex flex-col " + (i === 0 ? FEATURED_CELL : "")}>
|
|
544
|
+
<div
|
|
545
|
+
className={
|
|
546
|
+
"animate-pulse bg-stone " +
|
|
547
|
+
(i === 0 ? "aspect-[4/5] sm:aspect-[5/4] lg:aspect-auto lg:min-h-[480px] lg:flex-1" : "aspect-[4/5]")
|
|
548
|
+
}
|
|
549
|
+
/>
|
|
550
|
+
<div className="mt-5 flex justify-between">
|
|
551
|
+
<div className="h-6 w-1/2 animate-pulse rounded bg-ink/[0.06]" />
|
|
552
|
+
<div className="h-5 w-12 animate-pulse rounded bg-ink/[0.06]" />
|
|
553
|
+
</div>
|
|
554
|
+
<div className="mt-3 h-4 w-3/4 animate-pulse rounded bg-ink/[0.06]" />
|
|
555
|
+
<div className="mt-6 flex justify-between">
|
|
556
|
+
<div className="h-4 w-20 animate-pulse rounded bg-ink/[0.06]" />
|
|
557
|
+
<div className="h-10 w-32 animate-pulse rounded-full bg-ink/[0.06]" />
|
|
431
558
|
</div>
|
|
432
559
|
</div>
|
|
433
560
|
))}
|