@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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { db, callFn } from "@pylonsync/react";
|
|
5
5
|
import { useAuth } from "@pylonsync/client";
|
|
6
|
+
import { Check, LogOut, MapPin, X } from "lucide-react";
|
|
6
7
|
import { siteConfig } from "@/lib/site.config";
|
|
7
8
|
import type { BookingRow, OwnerBookingsResult } from "@/lib/booking";
|
|
8
9
|
|
|
@@ -36,6 +37,12 @@ export function BookingsDashboard({ userEmail }: { userEmail: string }) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// In `pylon dev` an empty booking book gets demo appointments
|
|
41
|
+
// (functions/seedDemo.ts). On a deploy the call returns without writing.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
void callFn("seedDemo", {}).catch(() => {});
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
39
46
|
useEffect(() => {
|
|
40
47
|
void load();
|
|
41
48
|
// Re-fetch whenever the live booked-slot set changes.
|
|
@@ -54,11 +61,7 @@ export function BookingsDashboard({ userEmail }: { userEmail: string }) {
|
|
|
54
61
|
|
|
55
62
|
if (denied) return <OwnerOnly email={userEmail} />;
|
|
56
63
|
if (error) {
|
|
57
|
-
return
|
|
58
|
-
<div className="rounded-xl border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700">
|
|
59
|
-
{error}
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
64
|
+
return <div className="border border-brand bg-brand-soft px-5 py-4 text-sm text-brand">{error}</div>;
|
|
62
65
|
}
|
|
63
66
|
if (!bookings) return <Skeleton />;
|
|
64
67
|
|
|
@@ -71,35 +74,59 @@ export function BookingsDashboard({ userEmail }: { userEmail: string }) {
|
|
|
71
74
|
(b) => new Date(b.startsAt).toLocaleDateString() === todayKey,
|
|
72
75
|
).length;
|
|
73
76
|
const pendingCount = upcoming.filter((b) => b.status === "pending").length;
|
|
77
|
+
const weekEnd = nowMs + 7 * 86_400_000;
|
|
78
|
+
const weekRevenue = upcoming
|
|
79
|
+
.filter((b) => Date.parse(b.startsAt) < weekEnd)
|
|
80
|
+
.reduce((sum, b) => sum + priceOf(b.serviceSlug), 0);
|
|
74
81
|
|
|
75
82
|
return (
|
|
76
|
-
<div className="space-y-
|
|
77
|
-
<div>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
<div className="space-y-10">
|
|
84
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
|
85
|
+
<div>
|
|
86
|
+
<h1 className="font-display text-[3.5rem] leading-none sm:text-[4.5rem]">Bookings</h1>
|
|
87
|
+
<p className="mt-3 max-w-lg text-[15px] leading-relaxed text-ink/70">
|
|
88
|
+
New bookings appear here as they are made. Cancelling a booking frees the time on
|
|
89
|
+
the site right away.
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
<p className="flex items-center gap-2 text-[13px] font-medium">
|
|
93
|
+
<span className="relative flex size-2">
|
|
94
|
+
<span className="absolute inline-flex size-2 animate-ping rounded-full bg-green-500/60" />
|
|
95
|
+
<span className="relative inline-flex size-2 rounded-full bg-green-600" />
|
|
96
|
+
</span>
|
|
97
|
+
Live
|
|
82
98
|
</p>
|
|
83
99
|
</div>
|
|
84
100
|
|
|
85
|
-
<
|
|
86
|
-
<Stat label="Upcoming" value={upcoming.length} />
|
|
87
|
-
<Stat label="Today" value={todayCount} />
|
|
88
|
-
<Stat label="
|
|
89
|
-
|
|
101
|
+
<dl className="grid grid-cols-2 gap-px border border-ink bg-ink sm:grid-cols-4">
|
|
102
|
+
<Stat label="Upcoming" value={String(upcoming.length)} />
|
|
103
|
+
<Stat label="Today" value={String(todayCount)} />
|
|
104
|
+
<Stat label="To confirm" value={String(pendingCount)} accent={pendingCount > 0} />
|
|
105
|
+
<Stat label="Booked, next 7 days" value={`$${weekRevenue.toLocaleString("en-US")}`} />
|
|
106
|
+
</dl>
|
|
90
107
|
|
|
91
108
|
<UpcomingList bookings={upcoming} busyId={busyId} onAct={act} />
|
|
109
|
+
|
|
110
|
+
{!siteConfig.location.mapEmbedUrl ? <MapHint /> : null}
|
|
92
111
|
</div>
|
|
93
112
|
);
|
|
94
113
|
}
|
|
95
114
|
|
|
96
|
-
function
|
|
115
|
+
function priceOf(slug: string): number {
|
|
116
|
+
return siteConfig.services.items.find((s) => s.slug === slug)?.priceUsd ?? 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Stat({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
|
|
97
120
|
return (
|
|
98
|
-
<div className="
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
<div className="bg-cream px-4 py-4">
|
|
122
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">{label}</dt>
|
|
123
|
+
<dd
|
|
124
|
+
className={
|
|
125
|
+
"mt-1 font-display text-[2.75rem] leading-none tabular-nums " + (accent ? "text-brand" : "")
|
|
126
|
+
}
|
|
127
|
+
>
|
|
128
|
+
{value}
|
|
129
|
+
</dd>
|
|
103
130
|
</div>
|
|
104
131
|
);
|
|
105
132
|
}
|
|
@@ -117,7 +144,7 @@ function UpcomingList({
|
|
|
117
144
|
const groups = useMemo(() => {
|
|
118
145
|
const m = new Map<string, BookingRow[]>();
|
|
119
146
|
for (const b of bookings) {
|
|
120
|
-
const key = new Date(b.startsAt).toLocaleDateString(
|
|
147
|
+
const key = new Date(b.startsAt).toLocaleDateString("en-US", {
|
|
121
148
|
weekday: "long",
|
|
122
149
|
month: "short",
|
|
123
150
|
day: "numeric",
|
|
@@ -131,29 +158,28 @@ function UpcomingList({
|
|
|
131
158
|
|
|
132
159
|
if (bookings.length === 0) {
|
|
133
160
|
return (
|
|
134
|
-
<div className="
|
|
135
|
-
No upcoming bookings
|
|
161
|
+
<div className="border border-dashed border-ink/50 p-10 text-center text-[15px] text-ink/70">
|
|
162
|
+
No upcoming bookings. New bookings appear here as soon as someone books a chair.
|
|
136
163
|
</div>
|
|
137
164
|
);
|
|
138
165
|
}
|
|
139
166
|
|
|
140
167
|
return (
|
|
141
|
-
<div className="space-y-
|
|
168
|
+
<div className="space-y-8">
|
|
142
169
|
{groups.map(([day, items]) => (
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
booking={b}
|
|
150
|
-
busy={busyId === b.id}
|
|
151
|
-
onAct={onAct}
|
|
152
|
-
last={i === items.length - 1}
|
|
153
|
-
/>
|
|
154
|
-
))}
|
|
170
|
+
<section key={day}>
|
|
171
|
+
<div className="flex items-baseline justify-between border-b border-ink pb-2">
|
|
172
|
+
<h2 className="font-display text-[2rem] leading-none">{day}</h2>
|
|
173
|
+
<span className="text-[13px] text-ink/60">
|
|
174
|
+
{items.length} {items.length === 1 ? "booking" : "bookings"}
|
|
175
|
+
</span>
|
|
155
176
|
</div>
|
|
156
|
-
|
|
177
|
+
<ul className="divide-y divide-ink/20">
|
|
178
|
+
{items.map((b) => (
|
|
179
|
+
<BookingItem key={b.id} booking={b} busy={busyId === b.id} onAct={onAct} />
|
|
180
|
+
))}
|
|
181
|
+
</ul>
|
|
182
|
+
</section>
|
|
157
183
|
))}
|
|
158
184
|
</div>
|
|
159
185
|
);
|
|
@@ -163,37 +189,29 @@ function BookingItem({
|
|
|
163
189
|
booking,
|
|
164
190
|
busy,
|
|
165
191
|
onAct,
|
|
166
|
-
last,
|
|
167
192
|
}: {
|
|
168
193
|
booking: BookingRow;
|
|
169
194
|
busy: boolean;
|
|
170
195
|
onAct: (id: string, fn: "confirmBooking" | "cancelBooking") => void;
|
|
171
|
-
last: boolean;
|
|
172
196
|
}) {
|
|
173
197
|
const service = siteConfig.services.items.find((s) => s.slug === booking.serviceSlug);
|
|
174
198
|
return (
|
|
175
|
-
<
|
|
176
|
-
className=
|
|
177
|
-
"
|
|
178
|
-
(last ? "" : "border-b border-zinc-100")
|
|
179
|
-
}
|
|
180
|
-
>
|
|
181
|
-
<div className="w-20 shrink-0 text-[14px] font-semibold tabular-nums text-zinc-900">
|
|
182
|
-
{new Date(booking.startsAt).toLocaleTimeString(undefined, {
|
|
199
|
+
<li className="flex flex-wrap items-center gap-x-5 gap-y-2 py-4">
|
|
200
|
+
<div className="w-24 shrink-0 font-display text-[1.75rem] leading-none tabular-nums">
|
|
201
|
+
{new Date(booking.startsAt).toLocaleTimeString("en-US", {
|
|
183
202
|
hour: "numeric",
|
|
184
203
|
minute: "2-digit",
|
|
185
204
|
})}
|
|
186
205
|
</div>
|
|
187
206
|
<div className="min-w-0 flex-1">
|
|
188
|
-
<div className="flex items-center gap-2">
|
|
189
|
-
<span className="text-[
|
|
190
|
-
|
|
191
|
-
</span>
|
|
192
|
-
<StatusBadge status={booking.status} />
|
|
207
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
208
|
+
<span className="text-[15px] font-semibold">{booking.customerName}</span>
|
|
209
|
+
<StatusTag status={booking.status} />
|
|
193
210
|
</div>
|
|
194
|
-
<div className="truncate text-[
|
|
195
|
-
{service
|
|
196
|
-
{
|
|
211
|
+
<div className="mt-0.5 truncate text-[13px] text-ink/65">
|
|
212
|
+
{service ? `${service.name} · ${service.durationMin} min · ${service.price}` : booking.serviceSlug}
|
|
213
|
+
{" · "}
|
|
214
|
+
{booking.customerPhone ?? booking.customerEmail}
|
|
197
215
|
</div>
|
|
198
216
|
</div>
|
|
199
217
|
<div className="flex items-center gap-2">
|
|
@@ -202,35 +220,48 @@ function BookingItem({
|
|
|
202
220
|
type="button"
|
|
203
221
|
disabled={busy}
|
|
204
222
|
onClick={() => onAct(booking.id, "confirmBooking")}
|
|
205
|
-
className="
|
|
223
|
+
className="inline-flex h-9 items-center gap-1.5 bg-ink px-3.5 text-[13px] font-medium text-cream transition-opacity hover:opacity-85 disabled:opacity-50"
|
|
206
224
|
>
|
|
207
|
-
|
|
225
|
+
<Check aria-hidden className="size-3.5" />
|
|
226
|
+
Confirm
|
|
208
227
|
</button>
|
|
209
228
|
) : null}
|
|
210
229
|
<button
|
|
211
230
|
type="button"
|
|
212
231
|
disabled={busy}
|
|
213
232
|
onClick={() => onAct(booking.id, "cancelBooking")}
|
|
214
|
-
className="
|
|
233
|
+
className="inline-flex h-9 items-center gap-1.5 border border-ink/40 px-3.5 text-[13px] font-medium text-ink/75 transition-colors hover:border-brand hover:text-brand disabled:opacity-50"
|
|
215
234
|
>
|
|
235
|
+
<X aria-hidden className="size-3.5" />
|
|
216
236
|
Cancel
|
|
217
237
|
</button>
|
|
218
238
|
</div>
|
|
219
|
-
</
|
|
239
|
+
</li>
|
|
220
240
|
);
|
|
221
241
|
}
|
|
222
242
|
|
|
223
|
-
function
|
|
243
|
+
function StatusTag({ status }: { status: string }) {
|
|
224
244
|
const tone =
|
|
225
245
|
status === "confirmed"
|
|
226
|
-
? "
|
|
246
|
+
? "border-ink/30 text-ink/70"
|
|
227
247
|
: status === "cancelled"
|
|
228
|
-
? "
|
|
229
|
-
: "bg-
|
|
248
|
+
? "border-ink/20 text-ink/40"
|
|
249
|
+
: "border-brand bg-brand-soft text-brand";
|
|
250
|
+
const label = status === "pending" ? "To confirm" : status === "confirmed" ? "Confirmed" : "Cancelled";
|
|
251
|
+
return <span className={"border px-1.5 py-px text-[11px] font-medium " + tone}>{label}</span>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Setup notes for the owner live here, not on the public site.
|
|
255
|
+
function MapHint() {
|
|
230
256
|
return (
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
<aside className="flex gap-3 border border-ink/30 bg-paper/60 p-4 text-[13.5px] leading-relaxed text-ink/75">
|
|
258
|
+
<MapPin aria-hidden className="mt-0.5 size-4 shrink-0 text-ink" />
|
|
259
|
+
<p>
|
|
260
|
+
Your site shows the weekly hours next to the address. To show a map there instead, paste a
|
|
261
|
+
Google Maps embed URL into <code className="bg-cream px-1 text-[12.5px]">location.mapEmbedUrl</code> in{" "}
|
|
262
|
+
<code className="bg-cream px-1 text-[12.5px]">lib/site.config.ts</code>.
|
|
263
|
+
</p>
|
|
264
|
+
</aside>
|
|
234
265
|
);
|
|
235
266
|
}
|
|
236
267
|
|
|
@@ -238,18 +269,16 @@ function StatusBadge({ status }: { status: string }) {
|
|
|
238
269
|
|
|
239
270
|
function OwnerOnly({ email }: { email: string }) {
|
|
240
271
|
return (
|
|
241
|
-
<div className="
|
|
242
|
-
<h1 className="text-
|
|
243
|
-
<p className="mx-auto mt-
|
|
244
|
-
You
|
|
245
|
-
<span className="font-medium text-zinc-700">{email || "this account"}</span>.
|
|
272
|
+
<div className="border border-dashed border-ink/50 px-6 py-12 text-center">
|
|
273
|
+
<h1 className="font-display text-[2.5rem] leading-none">This dashboard is owner-only</h1>
|
|
274
|
+
<p className="mx-auto mt-4 max-w-md text-[14px] leading-relaxed text-ink/70">
|
|
275
|
+
You are signed in as <span className="font-medium text-ink">{email || "this account"}</span>.
|
|
246
276
|
Only the shop owner can see bookings. Set{" "}
|
|
247
|
-
<code className="
|
|
277
|
+
<code className="bg-paper px-1.5 py-0.5 text-[12px]">
|
|
248
278
|
PYLON_OWNER_EMAIL={email || "you@yourshop.com"}
|
|
249
279
|
</code>{" "}
|
|
250
|
-
in
|
|
251
|
-
|
|
252
|
-
restart, and reload — or sign in with the owner account.
|
|
280
|
+
in <code className="bg-paper px-1.5 py-0.5 text-[12px]">.env</code>, restart, and reload. Or
|
|
281
|
+
sign in with the owner account.
|
|
253
282
|
</p>
|
|
254
283
|
</div>
|
|
255
284
|
);
|
|
@@ -266,20 +295,22 @@ export function UserMenu({ email }: { email: string }) {
|
|
|
266
295
|
}
|
|
267
296
|
return (
|
|
268
297
|
<details className="group relative">
|
|
269
|
-
<summary
|
|
298
|
+
<summary
|
|
299
|
+
aria-label="Account"
|
|
300
|
+
className="flex size-9 cursor-pointer select-none list-none items-center justify-center bg-brand font-display text-[20px] leading-none text-cream marker:hidden [&::-webkit-details-marker]:hidden"
|
|
301
|
+
>
|
|
270
302
|
{initial}
|
|
271
303
|
</summary>
|
|
272
|
-
<div className="absolute right-0 top-full z-40 mt-2 w-
|
|
273
|
-
<div className="border-b border-
|
|
274
|
-
|
|
275
|
-
{email || "Signed in"}
|
|
276
|
-
</div>
|
|
304
|
+
<div className="absolute right-0 top-full z-40 mt-2 w-60 border border-ink bg-cream py-1">
|
|
305
|
+
<div className="truncate border-b border-ink/20 px-3 py-2.5 text-[13px] font-medium">
|
|
306
|
+
{email || "Signed in"}
|
|
277
307
|
</div>
|
|
278
308
|
<button
|
|
279
309
|
type="button"
|
|
280
310
|
onClick={onSignOut}
|
|
281
|
-
className="flex w-full items-center px-3 py-2 text-left text-[13px] text-
|
|
311
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-[13px] text-ink/75 transition-colors hover:bg-paper hover:text-ink"
|
|
282
312
|
>
|
|
313
|
+
<LogOut aria-hidden className="size-3.5" />
|
|
283
314
|
Sign out
|
|
284
315
|
</button>
|
|
285
316
|
</div>
|
|
@@ -291,14 +322,10 @@ export function UserMenu({ email }: { email: string }) {
|
|
|
291
322
|
|
|
292
323
|
function Skeleton() {
|
|
293
324
|
return (
|
|
294
|
-
<div className="space-y-
|
|
295
|
-
<div className="h-
|
|
296
|
-
<div className="
|
|
297
|
-
|
|
298
|
-
<div key={i} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
|
|
299
|
-
))}
|
|
300
|
-
</div>
|
|
301
|
-
<div className="h-48 animate-pulse rounded-xl bg-zinc-100" />
|
|
325
|
+
<div className="space-y-10">
|
|
326
|
+
<div className="h-16 w-64 animate-pulse bg-ink/10" />
|
|
327
|
+
<div className="h-24 animate-pulse bg-ink/10" />
|
|
328
|
+
<div className="h-72 animate-pulse bg-ink/10" />
|
|
302
329
|
</div>
|
|
303
330
|
);
|
|
304
331
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { Link, type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
+
import { ArrowUpRight } from "lucide-react";
|
|
3
4
|
import { siteConfig } from "@/lib/site.config";
|
|
4
5
|
import { UserMenu, BookingsDashboard } from "./dashboard-client";
|
|
5
6
|
|
|
@@ -30,33 +31,32 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
|
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// Dashboard chrome in the site's own look: the cream page, the ruled header,
|
|
35
|
+
// the shop name in the display face, a link back to the site, and the account
|
|
36
|
+
// menu (a client island for sign-out).
|
|
33
37
|
function Shell({ email, children }: { email: string; children: React.ReactNode }) {
|
|
34
38
|
const { brand } = siteConfig;
|
|
35
39
|
return (
|
|
36
|
-
<div className="flex min-h-screen flex-col bg-
|
|
37
|
-
<header className="border-b border-
|
|
38
|
-
<div className="mx-auto flex h-14 max-w-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</span>
|
|
43
|
-
<span className="text-[15px] font-semibold tracking-tight">
|
|
44
|
-
{brand.name} <span className="text-zinc-400">/ bookings</span>
|
|
45
|
-
</span>
|
|
46
|
-
</div>
|
|
40
|
+
<div className="flex min-h-screen flex-col bg-cream text-ink">
|
|
41
|
+
<header className="sticky top-0 z-30 border-b border-ink bg-cream">
|
|
42
|
+
<div className="mx-auto flex h-14 w-full max-w-5xl items-center justify-between px-6">
|
|
43
|
+
<Link href="/" className="font-display text-[26px] leading-none tracking-[0.02em]">
|
|
44
|
+
{brand.name}
|
|
45
|
+
</Link>
|
|
47
46
|
<div className="flex items-center gap-4">
|
|
48
47
|
<Link
|
|
49
48
|
href="/"
|
|
50
|
-
className="text-[13px] text-
|
|
49
|
+
className="hidden items-center gap-1 text-[13px] font-medium text-ink/70 transition-colors hover:text-ink sm:inline-flex"
|
|
51
50
|
>
|
|
52
|
-
View site
|
|
51
|
+
View site
|
|
52
|
+
<ArrowUpRight aria-hidden className="size-3.5" />
|
|
53
53
|
</Link>
|
|
54
54
|
<UserMenu email={email} />
|
|
55
55
|
</div>
|
|
56
56
|
</div>
|
|
57
57
|
</header>
|
|
58
58
|
<main className="flex-1">
|
|
59
|
-
<div className="mx-auto max-w-
|
|
59
|
+
<div className="mx-auto w-full max-w-5xl px-6 py-10 sm:py-12">{children}</div>
|
|
60
60
|
</main>
|
|
61
61
|
</div>
|
|
62
62
|
);
|
|
@@ -18,21 +18,21 @@ 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
|
|
22
|
-
<div className="
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
{brand.
|
|
26
|
-
</
|
|
27
|
-
</
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
<div className="flex min-h-screen flex-col bg-cream text-ink">
|
|
22
|
+
<div className="border-b border-ink">
|
|
23
|
+
<div className="mx-auto flex h-14 w-full max-w-5xl items-center px-6">
|
|
24
|
+
<Link href="/" className="font-display text-[26px] leading-none tracking-[0.02em]">
|
|
25
|
+
{brand.name}
|
|
26
|
+
</Link>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="flex flex-1 items-center justify-center px-6 py-16">
|
|
30
|
+
<div className="w-full max-w-[400px]">
|
|
31
|
+
<h1 className="font-display text-[3.5rem] leading-none">Owner sign in</h1>
|
|
32
|
+
<p className="mt-3 text-[15px] text-ink/70">Bookings, confirmations, and cancellations.</p>
|
|
33
|
+
<div className="mt-8 border border-ink bg-paper/60 p-6">
|
|
34
|
+
<AuthForm />
|
|
35
|
+
</div>
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mutation } from "@pylonsync/functions";
|
|
2
|
+
import { demoBookings, demoDataEnabled } from "../lib/demo";
|
|
3
|
+
import { siteConfig } from "../lib/site.config";
|
|
4
|
+
|
|
5
|
+
// seedDemo — fill an empty booking book with fictional appointments in
|
|
6
|
+
// development, so the public grid and the owner dashboard show the same
|
|
7
|
+
// bookings.
|
|
8
|
+
//
|
|
9
|
+
// The booking widget calls this when it finds no booked slots, and the
|
|
10
|
+
// dashboard calls it once on load. It writes nothing unless demo data is on
|
|
11
|
+
// (lib/demo.ts: PYLON_DEMO_DATA, else a `pylon dev` process), so `pylon start`,
|
|
12
|
+
// Docker, and Pylon Cloud deploys do not get demo rows. It also writes nothing
|
|
13
|
+
// once any Booking exists, so it cannot mix demo rows into a real book. The
|
|
14
|
+
// advisory lock stops two first visits from seeding twice.
|
|
15
|
+
//
|
|
16
|
+
// Each active booking gets its BookedSlot, the same pair createBooking writes.
|
|
17
|
+
// Public because the first visitor in `pylon dev` is anonymous. It returns only
|
|
18
|
+
// a flag, never a row.
|
|
19
|
+
export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
20
|
+
auth: "public",
|
|
21
|
+
async handler(ctx) {
|
|
22
|
+
if (!demoDataEnabled(ctx.env)) return { seeded: false };
|
|
23
|
+
|
|
24
|
+
await ctx.db.advisoryLock("local_service_seed_demo");
|
|
25
|
+
const existing = await ctx.db.unsafe.list("Booking");
|
|
26
|
+
if (existing.length > 0) return { seeded: false };
|
|
27
|
+
|
|
28
|
+
for (const b of demoBookings(Date.now(), siteConfig)) {
|
|
29
|
+
const bookingId = await ctx.db.unsafe.insert("Booking", {
|
|
30
|
+
serviceSlug: b.serviceSlug,
|
|
31
|
+
startsAt: b.startsAt,
|
|
32
|
+
endsAt: b.endsAt,
|
|
33
|
+
customerName: b.customerName,
|
|
34
|
+
customerEmail: b.customerEmail,
|
|
35
|
+
customerPhone: b.customerPhone,
|
|
36
|
+
status: b.status,
|
|
37
|
+
createdAt: b.createdAt,
|
|
38
|
+
});
|
|
39
|
+
if (b.status === "cancelled") continue;
|
|
40
|
+
await ctx.db.unsafe.insert("BookedSlot", {
|
|
41
|
+
serviceSlug: b.serviceSlug,
|
|
42
|
+
startsAt: b.startsAt,
|
|
43
|
+
endsAt: b.endsAt,
|
|
44
|
+
bookingId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return { seeded: true };
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Demo bookings for local development.
|
|
2
|
+
//
|
|
3
|
+
// `pylon dev` fills an empty booking book with fictional appointments from the
|
|
4
|
+
// past week and the next ten days, so the booking grid shows taken times and
|
|
5
|
+
// the owner dashboard has a schedule. Both read the same rows: every demo
|
|
6
|
+
// Booking gets its BookedSlot, exactly like createBooking writes.
|
|
7
|
+
//
|
|
8
|
+
// A deploy does not seed: `demoDataEnabled` is true only when PYLON_DEMO_DATA is
|
|
9
|
+
// on, or when it is unset and the process runs under `pylon dev`. Set PYLON_DEMO_DATA=0 to
|
|
10
|
+
// turn it off in development too.
|
|
11
|
+
//
|
|
12
|
+
// Pure data + pure shaping, so functions/seedDemo.ts stays a thin wrapper and
|
|
13
|
+
// this file is testable without a server.
|
|
14
|
+
|
|
15
|
+
import { localDateKey, rangesOverlap, slotsForDay, weekdayOf } from "./slots";
|
|
16
|
+
import type { LocalServiceConfig } from "./site.config";
|
|
17
|
+
|
|
18
|
+
const ON = new Set(["1", "true", "yes", "on"]);
|
|
19
|
+
const OFF = new Set(["0", "false", "no", "off"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* True when demo rows may be written. An explicit PYLON_DEMO_DATA wins.
|
|
23
|
+
* Otherwise it needs both PYLON_DEV_MODE on and PYLON_DEV_WATCH_DIR set. Only
|
|
24
|
+
* `pylon dev` sets PYLON_DEV_WATCH_DIR; `pylon start` and the Docker image do
|
|
25
|
+
* not, and the image turns PYLON_DEV_MODE on by default, so dev mode alone is
|
|
26
|
+
* not proof of a local machine.
|
|
27
|
+
*/
|
|
28
|
+
export function demoDataEnabled(env: Record<string, string | undefined>): boolean {
|
|
29
|
+
const explicit = env.PYLON_DEMO_DATA?.trim().toLowerCase();
|
|
30
|
+
if (explicit && ON.has(explicit)) return true;
|
|
31
|
+
if (explicit && OFF.has(explicit)) return false;
|
|
32
|
+
const dev = env.PYLON_DEV_MODE?.trim().toLowerCase();
|
|
33
|
+
const devMode = dev === "1" || dev === "true";
|
|
34
|
+
return devMode && Boolean(env.PYLON_DEV_WATCH_DIR?.trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CUSTOMERS = [
|
|
38
|
+
"Marcus Bell", "Daniel Reyes", "Hannah Kim", "Luis Ortega", "Tyler Brooks", "Andre Wallace",
|
|
39
|
+
"Sam Okafor", "Kevin Tran", "Jordan Miles", "Eli Navarro", "Chris Delgado", "Owen Price",
|
|
40
|
+
"Ravi Shah", "Nate Coleman", "Isaac Moreno", "Ben Hollis", "Victor Lam", "Grant Ellison",
|
|
41
|
+
"Mateo Vargas", "Dev Patel", "Caleb Ford", "Jamal Hayes", "Theo Lindqvist", "Omar Haddad",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Small deterministic PRNG (mulberry32) so the demo set is stable per seed. */
|
|
45
|
+
function rng(seed: number): () => number {
|
|
46
|
+
let a = seed >>> 0;
|
|
47
|
+
return () => {
|
|
48
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
49
|
+
let t = a;
|
|
50
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
51
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
52
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DemoBooking {
|
|
57
|
+
serviceSlug: string;
|
|
58
|
+
startsAt: string;
|
|
59
|
+
endsAt: string;
|
|
60
|
+
customerName: string;
|
|
61
|
+
customerEmail: string;
|
|
62
|
+
customerPhone: string | null;
|
|
63
|
+
status: "pending" | "confirmed" | "cancelled";
|
|
64
|
+
createdAt: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Cfg = Pick<LocalServiceConfig, "booking" | "services">;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Appointments on open days from 7 days ago to 9 days ahead. Each fits inside
|
|
71
|
+
* opening hours on the slot grid, and no two active bookings overlap. Past
|
|
72
|
+
* appointments are confirmed; upcoming ones are mostly confirmed, some
|
|
73
|
+
* pending, a few cancelled.
|
|
74
|
+
*/
|
|
75
|
+
export function demoBookings(nowMs: number, cfg: Cfg, seed = 11): DemoBooking[] {
|
|
76
|
+
const rand = rng(seed);
|
|
77
|
+
const out: DemoBooking[] = [];
|
|
78
|
+
const services = cfg.services.items;
|
|
79
|
+
if (services.length === 0) return out;
|
|
80
|
+
|
|
81
|
+
for (let offset = -7; offset <= 9; offset++) {
|
|
82
|
+
const day = localDateKey(offset, nowMs);
|
|
83
|
+
const hours = cfg.booking.hours[weekdayOf(day)];
|
|
84
|
+
if (!hours) continue;
|
|
85
|
+
// The full grid for the day, ignoring "now": seeding also fills the past.
|
|
86
|
+
const grid = slotsForDay({
|
|
87
|
+
dayISODate: day,
|
|
88
|
+
open: hours.open,
|
|
89
|
+
close: hours.close,
|
|
90
|
+
slotMinutes: cfg.booking.slotMinutes,
|
|
91
|
+
durationMin: 0,
|
|
92
|
+
leadTimeHours: 0,
|
|
93
|
+
busy: [],
|
|
94
|
+
nowMs: 0,
|
|
95
|
+
});
|
|
96
|
+
// With a zero duration the last grid start is closing time itself.
|
|
97
|
+
const close = Date.parse(grid[grid.length - 1].startsAt);
|
|
98
|
+
const busy: [number, number][] = [];
|
|
99
|
+
// Busier near the weekend and in the days just ahead.
|
|
100
|
+
const fill = offset >= 0 && offset <= 3 ? 0.5 : 0.35;
|
|
101
|
+
for (const slot of grid) {
|
|
102
|
+
if (rand() > fill) continue;
|
|
103
|
+
const svc = services[Math.floor(rand() * services.length)];
|
|
104
|
+
const start = Date.parse(slot.startsAt);
|
|
105
|
+
const end = start + svc.durationMin * 60_000;
|
|
106
|
+
if (end > close) continue;
|
|
107
|
+
if (busy.some(([s, e]) => rangesOverlap(start, end, s, e))) continue;
|
|
108
|
+
|
|
109
|
+
const name = CUSTOMERS[Math.floor(rand() * CUSTOMERS.length)];
|
|
110
|
+
const [first, last] = name.toLowerCase().split(" ");
|
|
111
|
+
const past = end <= nowMs;
|
|
112
|
+
const roll = rand();
|
|
113
|
+
const status: DemoBooking["status"] = past
|
|
114
|
+
? "confirmed"
|
|
115
|
+
: roll < 0.62
|
|
116
|
+
? "confirmed"
|
|
117
|
+
: roll < 0.92
|
|
118
|
+
? "pending"
|
|
119
|
+
: "cancelled";
|
|
120
|
+
if (status !== "cancelled") busy.push([start, end]);
|
|
121
|
+
const bookedAt = Math.min(nowMs - 3_600_000, start - (1 + Math.floor(rand() * 6)) * 86_400_000);
|
|
122
|
+
out.push({
|
|
123
|
+
serviceSlug: svc.slug,
|
|
124
|
+
startsAt: new Date(start).toISOString(),
|
|
125
|
+
endsAt: new Date(end).toISOString(),
|
|
126
|
+
customerName: name,
|
|
127
|
+
customerEmail: `${first}.${last[0]}@example.com`,
|
|
128
|
+
customerPhone: rand() < 0.7 ? `(214) 555-01${String(Math.floor(rand() * 100)).padStart(2, "0")}` : null,
|
|
129
|
+
status,
|
|
130
|
+
createdAt: new Date(bookedAt).toISOString(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|