@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Demo subscribers for local development.
|
|
2
|
+
//
|
|
3
|
+
// `pylon dev` fills an empty list with about 180 fictional subscribers spread
|
|
4
|
+
// over the last 30 days, so the counter, chart, and list have data. A deploy
|
|
5
|
+
// does not seed: `demoDataEnabled` is true only when PYLON_DEMO_DATA is on, or
|
|
6
|
+
// when it is unset and the process runs under `pylon dev`. Set
|
|
7
|
+
// PYLON_DEMO_DATA=0 to turn it off in development too.
|
|
8
|
+
//
|
|
9
|
+
// Pure data + pure shaping, so functions/seedDemo.ts stays a thin wrapper and
|
|
10
|
+
// this file is testable without a server.
|
|
11
|
+
|
|
12
|
+
const ON = new Set(["1", "true", "yes", "on"]);
|
|
13
|
+
const OFF = new Set(["0", "false", "no", "off"]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* True when demo rows may be written. An explicit PYLON_DEMO_DATA wins.
|
|
17
|
+
* Otherwise it needs both PYLON_DEV_MODE on and PYLON_DEV_WATCH_DIR set. Only
|
|
18
|
+
* `pylon dev` sets PYLON_DEV_WATCH_DIR; `pylon start` and the Docker image do
|
|
19
|
+
* not, and the image turns PYLON_DEV_MODE on by default, so dev mode alone is
|
|
20
|
+
* not proof of a local machine.
|
|
21
|
+
*/
|
|
22
|
+
export function demoDataEnabled(env: Record<string, string | undefined>): boolean {
|
|
23
|
+
const explicit = env.PYLON_DEMO_DATA?.trim().toLowerCase();
|
|
24
|
+
if (explicit && ON.has(explicit)) return true;
|
|
25
|
+
if (explicit && OFF.has(explicit)) return false;
|
|
26
|
+
const dev = env.PYLON_DEV_MODE?.trim().toLowerCase();
|
|
27
|
+
const devMode = dev === "1" || dev === "true";
|
|
28
|
+
return devMode && Boolean(env.PYLON_DEV_WATCH_DIR?.trim());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FIRST = [
|
|
32
|
+
"priya", "marcus", "lena", "tomas", "aisha", "ben", "hana", "diego", "sofia", "kwame",
|
|
33
|
+
"ingrid", "raj", "noor", "felix", "maya", "owen", "yuki", "carmen", "theo", "amara",
|
|
34
|
+
"jonas", "leila", "sam", "ines", "dev", "clara", "malik", "rosa", "ethan", "zara",
|
|
35
|
+
];
|
|
36
|
+
const LAST = [
|
|
37
|
+
"nair", "bell", "fischer", "alvarez", "khan", "osei", "sato", "ruiz", "marin", "mensah",
|
|
38
|
+
"berg", "patel", "haddad", "wagner", "chen", "reilly", "tanaka", "vega", "park", "okafor",
|
|
39
|
+
];
|
|
40
|
+
// Fictional company domains on the reserved .example TLD, so no demo address
|
|
41
|
+
// can reach a real inbox.
|
|
42
|
+
const DOMAINS = [
|
|
43
|
+
"fieldnote.example", "northwind.example", "quarry.example", "bluefin.example", "wren.example",
|
|
44
|
+
"copperline.example", "tidewater.example", "larkspur.example", "kestrel.example", "orchardlabs.example",
|
|
45
|
+
"sable.example", "ironwood.example", "halcyon.example", "pinecrest.example", "vantage.example",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** Small deterministic PRNG (mulberry32) so the demo set is stable per seed. */
|
|
49
|
+
function rng(seed: number): () => number {
|
|
50
|
+
let a = seed >>> 0;
|
|
51
|
+
return () => {
|
|
52
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
53
|
+
let t = a;
|
|
54
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
55
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
56
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DemoSubscriber {
|
|
61
|
+
email: string;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* About 180 subscribers over the 30 days before `nowMs`, rising toward today.
|
|
67
|
+
* Emails are unique. Every timestamp is in the past.
|
|
68
|
+
*/
|
|
69
|
+
export function demoSubscribers(nowMs: number, seed = 23): DemoSubscriber[] {
|
|
70
|
+
const rand = rng(seed);
|
|
71
|
+
const out: DemoSubscriber[] = [];
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
const DAY = 86_400_000;
|
|
74
|
+
for (let daysAgo = 29; daysAgo >= 0; daysAgo--) {
|
|
75
|
+
// Growth curve: ~2/day a month ago, ~10/day this week, plus noise.
|
|
76
|
+
const base = 2 + Math.round((29 - daysAgo) * 0.28);
|
|
77
|
+
const count = Math.max(0, base + Math.round((rand() - 0.5) * 4));
|
|
78
|
+
for (let i = 0; i < count; i++) {
|
|
79
|
+
const first = FIRST[Math.floor(rand() * FIRST.length)];
|
|
80
|
+
const last = LAST[Math.floor(rand() * LAST.length)];
|
|
81
|
+
const domain = DOMAINS[Math.floor(rand() * DOMAINS.length)];
|
|
82
|
+
const shape = rand();
|
|
83
|
+
const local = shape < 0.5 ? `${first}.${last}` : shape < 0.8 ? `${first}` : `${first[0]}${last}`;
|
|
84
|
+
const email = `${local}@${domain}`;
|
|
85
|
+
if (seen.has(email)) continue;
|
|
86
|
+
seen.add(email);
|
|
87
|
+
// A random minute inside that day, never later than `nowMs`.
|
|
88
|
+
const offset = Math.floor(rand() * DAY);
|
|
89
|
+
const ts = Math.min(nowMs - 60_000, nowMs - daysAgo * DAY - offset);
|
|
90
|
+
out.push({ email, createdAt: new Date(ts).toISOString() });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
|
|
94
|
+
}
|
|
@@ -48,7 +48,11 @@ export type CreatorConfig = BaseConfig & {
|
|
|
48
48
|
ctaLabel: string;
|
|
49
49
|
successMessage: string;
|
|
50
50
|
counterLabel: string; // e.g. "readers subscribed"
|
|
51
|
-
|
|
51
|
+
counterLabelOne: string; // the same label for a count of 1
|
|
52
|
+
// Subscribers you are moving over from another tool, added to the
|
|
53
|
+
// Subscriber table's count. Keep 0 unless those people are real; the page
|
|
54
|
+
// shows the sum as one number.
|
|
55
|
+
importedCount: number;
|
|
52
56
|
};
|
|
53
57
|
links?: { headline: string; body: string; items: LinkItem[] };
|
|
54
58
|
};
|
|
@@ -62,7 +66,7 @@ export const siteConfig: CreatorConfig = {
|
|
|
62
66
|
domain: "mayarivera.co",
|
|
63
67
|
email: "hello@mayarivera.example",
|
|
64
68
|
footerBlurb:
|
|
65
|
-
"Product design coach and writer.
|
|
69
|
+
"Product design coach and writer. 1:1 coaching, portfolio reviews, and a weekly newsletter.",
|
|
66
70
|
copyrightName: "Maya Rivera",
|
|
67
71
|
socials: [
|
|
68
72
|
{
|
|
@@ -83,14 +87,14 @@ export const siteConfig: CreatorConfig = {
|
|
|
83
87
|
seo: {
|
|
84
88
|
title: "Maya Rivera — product design coach & writer",
|
|
85
89
|
description:
|
|
86
|
-
"Product design coaching, portfolio reviews, and a weekly newsletter for designers
|
|
90
|
+
"Product design coaching, portfolio reviews, and a weekly newsletter for product designers.",
|
|
87
91
|
},
|
|
88
92
|
|
|
89
93
|
hero: {
|
|
90
94
|
name: "Maya Rivera",
|
|
91
95
|
tagline: "Product design coach & writer.",
|
|
92
96
|
intro:
|
|
93
|
-
"I
|
|
97
|
+
"I coach product designers on portfolios, critique, and the move to senior roles. I led design teams for fifteen years. Now I work with designers one at a time.",
|
|
94
98
|
},
|
|
95
99
|
|
|
96
100
|
about: {
|
|
@@ -106,7 +110,7 @@ export const siteConfig: CreatorConfig = {
|
|
|
106
110
|
items: [
|
|
107
111
|
{
|
|
108
112
|
title: "1:1 coaching",
|
|
109
|
-
body: "
|
|
113
|
+
body: "Two sessions a month on your work and your next role. We agree on a plan and review it each session.",
|
|
110
114
|
price: "from $400/mo",
|
|
111
115
|
},
|
|
112
116
|
{
|
|
@@ -151,7 +155,7 @@ export const siteConfig: CreatorConfig = {
|
|
|
151
155
|
items: [
|
|
152
156
|
{
|
|
153
157
|
quote:
|
|
154
|
-
"Maya helped me land a senior role in three months. The portfolio review
|
|
158
|
+
"Maya helped me land a senior role in three months. The portfolio review changed which projects I led with.",
|
|
155
159
|
name: "Daniel Reyes",
|
|
156
160
|
role: "Senior Product Designer",
|
|
157
161
|
},
|
|
@@ -163,7 +167,7 @@ export const siteConfig: CreatorConfig = {
|
|
|
163
167
|
},
|
|
164
168
|
{
|
|
165
169
|
quote:
|
|
166
|
-
"I read the newsletter every week.
|
|
170
|
+
"I read the newsletter every week. Each issue has one thing I can use that week.",
|
|
167
171
|
name: "Marcus Bell",
|
|
168
172
|
role: "Product Designer",
|
|
169
173
|
},
|
|
@@ -172,12 +176,13 @@ export const siteConfig: CreatorConfig = {
|
|
|
172
176
|
|
|
173
177
|
newsletter: {
|
|
174
178
|
name: "The Studio Notes",
|
|
175
|
-
subcopy: "One short email every Sunday on
|
|
179
|
+
subcopy: "One short email every Sunday on critique, portfolios, and design careers.",
|
|
176
180
|
emailPlaceholder: "you@email.com",
|
|
177
181
|
ctaLabel: "Subscribe",
|
|
178
182
|
successMessage: "You are in. The next issue arrives on Sunday.",
|
|
179
|
-
counterLabel: "designers
|
|
180
|
-
|
|
183
|
+
counterLabel: "designers subscribed",
|
|
184
|
+
counterLabelOne: "designer subscribed",
|
|
185
|
+
importedCount: 0,
|
|
181
186
|
},
|
|
182
187
|
|
|
183
188
|
links: {
|
|
@@ -28,3 +28,38 @@ export interface SubscriberStatsData {
|
|
|
28
28
|
export type SubscriberStatsResult =
|
|
29
29
|
| ({ authorized: true } & SubscriberStatsData)
|
|
30
30
|
| { authorized: false };
|
|
31
|
+
|
|
32
|
+
/* ------------------------- public live counter ------------------------- */
|
|
33
|
+
|
|
34
|
+
// The public counter shows the Subscriber table's row count, read from the
|
|
35
|
+
// PII-free SubscriberCount row. `importedCount` is
|
|
36
|
+
// `siteConfig.newsletter.importedCount`: subscribers you are moving over from
|
|
37
|
+
// another tool. It is 0 by default, so the page shows only rows in the table.
|
|
38
|
+
export function publicCount(
|
|
39
|
+
countRows: ReadonlyArray<{ count: number }>,
|
|
40
|
+
importedCount = 0,
|
|
41
|
+
): number {
|
|
42
|
+
const real = countRows.length > 0 ? Math.max(0, Math.floor(countRows[0].count)) : 0;
|
|
43
|
+
const imported = Number.isFinite(importedCount) ? Math.max(0, Math.floor(importedCount)) : 0;
|
|
44
|
+
return real + imported;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The subset of `ctx.db.unsafe` the recount needs. Typed structurally so the
|
|
48
|
+
// helper runs against the real handle and a test double.
|
|
49
|
+
export interface CountDb {
|
|
50
|
+
list(entity: string): Promise<unknown[]>;
|
|
51
|
+
insert(entity: string, data: Record<string, unknown>): Promise<unknown>;
|
|
52
|
+
update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Write the Subscriber row count into the single SubscriberCount row, creating
|
|
56
|
+
// it if missing. Every writer of Subscriber calls this in the same transaction,
|
|
57
|
+
// so the public number is always the table's count.
|
|
58
|
+
export async function syncSubscriberCount(db: CountDb): Promise<number> {
|
|
59
|
+
const total = (await db.list("Subscriber")).length;
|
|
60
|
+
const row = (await db.list("SubscriberCount"))[0] as { id: string } | undefined;
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
if (row) await db.update("SubscriberCount", row.id, { count: total, updatedAt: now });
|
|
63
|
+
else await db.insert("SubscriberCount", { count: total, updatedAt: now });
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { publicCount, syncSubscriberCount, type CountDb } from "../lib/stats";
|
|
3
|
+
import { demoDataEnabled, demoSubscribers } from "../lib/demo";
|
|
4
|
+
import { siteConfig } from "../lib/site.config";
|
|
5
|
+
|
|
6
|
+
describe("publicCount", () => {
|
|
7
|
+
test("is the table's count when nothing is imported", () => {
|
|
8
|
+
expect(publicCount([{ count: 3 }])).toBe(3);
|
|
9
|
+
expect(publicCount([])).toBe(0);
|
|
10
|
+
});
|
|
11
|
+
test("adds only an explicit, non-negative imported count", () => {
|
|
12
|
+
expect(publicCount([{ count: 3 }], 40)).toBe(43);
|
|
13
|
+
expect(publicCount([{ count: 3 }], -5)).toBe(3);
|
|
14
|
+
expect(publicCount([{ count: 3 }], Number.NaN)).toBe(3);
|
|
15
|
+
});
|
|
16
|
+
test("the shipped config adds nothing to the real count", () => {
|
|
17
|
+
expect(siteConfig.newsletter.importedCount).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// An in-memory stand-in for ctx.db.unsafe.
|
|
22
|
+
function fakeDb(signups: number): CountDb & { rows: Record<string, Record<string, unknown>[]> } {
|
|
23
|
+
const rows: Record<string, Record<string, unknown>[]> = {
|
|
24
|
+
Subscriber: Array.from({ length: signups }, (_, i) => ({ id: `s${i}` })),
|
|
25
|
+
SubscriberCount: [],
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
rows,
|
|
29
|
+
async list(entity) {
|
|
30
|
+
return rows[entity] ?? [];
|
|
31
|
+
},
|
|
32
|
+
async insert(entity, data) {
|
|
33
|
+
const id = `${entity}-${rows[entity].length}`;
|
|
34
|
+
rows[entity].push({ id, ...data });
|
|
35
|
+
return id;
|
|
36
|
+
},
|
|
37
|
+
async update(entity, id, data) {
|
|
38
|
+
const row = rows[entity].find((r) => r.id === id);
|
|
39
|
+
if (row) Object.assign(row, data);
|
|
40
|
+
return id;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("syncSubscriberCount", () => {
|
|
46
|
+
test("creates the stat row with the Subscriber count", async () => {
|
|
47
|
+
const db = fakeDb(5);
|
|
48
|
+
expect(await syncSubscriberCount(db)).toBe(5);
|
|
49
|
+
expect(db.rows.SubscriberCount).toHaveLength(1);
|
|
50
|
+
expect(db.rows.SubscriberCount[0].count).toBe(5);
|
|
51
|
+
});
|
|
52
|
+
test("recounts into the existing row instead of adding one", async () => {
|
|
53
|
+
const db = fakeDb(5);
|
|
54
|
+
await syncSubscriberCount(db);
|
|
55
|
+
db.rows.Subscriber.push({ id: "late" });
|
|
56
|
+
await syncSubscriberCount(db);
|
|
57
|
+
expect(db.rows.SubscriberCount).toHaveLength(1);
|
|
58
|
+
expect(db.rows.SubscriberCount[0].count).toBe(6);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("demoDataEnabled", () => {
|
|
63
|
+
test("needs a `pylon dev` process when PYLON_DEMO_DATA is unset", () => {
|
|
64
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "1", PYLON_DEV_WATCH_DIR: "/src/app" })).toBe(true);
|
|
65
|
+
// The Docker base image turns dev mode on by default; without `pylon dev` it must not seed.
|
|
66
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "true" })).toBe(false);
|
|
67
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "true", PYLON_DEV_WATCH_DIR: "/src/app" })).toBe(true);
|
|
68
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "false" })).toBe(false);
|
|
69
|
+
expect(demoDataEnabled({})).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
test("an explicit PYLON_DEMO_DATA wins", () => {
|
|
72
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "1", PYLON_DEV_WATCH_DIR: "/src/app", PYLON_DEMO_DATA: "0" })).toBe(false);
|
|
73
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "false", PYLON_DEMO_DATA: "1" })).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("demoSubscribers", () => {
|
|
78
|
+
const now = Date.parse("2026-09-26T15:00:00Z");
|
|
79
|
+
const rows = demoSubscribers(now);
|
|
80
|
+
test("unique emails, all in the past 30 days", () => {
|
|
81
|
+
expect(new Set(rows.map((r) => r.email)).size).toBe(rows.length);
|
|
82
|
+
for (const r of rows) {
|
|
83
|
+
const t = Date.parse(r.createdAt);
|
|
84
|
+
expect(t).toBeLessThan(now);
|
|
85
|
+
expect(t).toBeGreaterThan(now - 31 * 86_400_000);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
test("enough rows to fill the chart, stable for a seed", () => {
|
|
89
|
+
expect(rows.length).toBeGreaterThan(120);
|
|
90
|
+
expect(demoSubscribers(now)).toEqual(rows);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
// The live count reads the SubscriberCount row. Mock the data boundary so the
|
|
6
|
+
// test controls what the "server" says.
|
|
7
|
+
let rows: { id: string; count: number }[] = [];
|
|
8
|
+
let loading = false;
|
|
9
|
+
mock.module("@pylonsync/react", () => ({
|
|
10
|
+
db: { useQuery: () => ({ data: rows, loading }) },
|
|
11
|
+
callFn: async () => ({ seeded: false }),
|
|
12
|
+
}));
|
|
13
|
+
mock.module("@pylonsync/client", () => ({
|
|
14
|
+
EnsureGuest: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
15
|
+
}));
|
|
16
|
+
const { LiveCounter } = await import("../app/(marketing)/newsletter-signup");
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
cleanup();
|
|
20
|
+
rows = [];
|
|
21
|
+
loading = false;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("shows the table's count, with no vanity baseline", () => {
|
|
25
|
+
rows = [{ id: "c", count: 42 }];
|
|
26
|
+
render(<LiveCounter importedCount={0} label="designers subscribed" labelOne="designer subscribed" />);
|
|
27
|
+
expect(screen.getByTestId("live-count").textContent).toBe("42 designers subscribed");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("shows nothing before the first sync or while the list is empty", () => {
|
|
31
|
+
loading = true;
|
|
32
|
+
const { unmount } = render(<LiveCounter importedCount={0} label="designers subscribed" labelOne="designer subscribed" />);
|
|
33
|
+
expect(screen.queryByTestId("live-count")).toBeNull();
|
|
34
|
+
unmount();
|
|
35
|
+
loading = false;
|
|
36
|
+
rows = [{ id: "c", count: 0 }];
|
|
37
|
+
render(<LiveCounter importedCount={0} label="designers subscribed" labelOne="designer subscribed" />);
|
|
38
|
+
expect(screen.queryByTestId("live-count")).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("uses the singular label for one subscriber", () => {
|
|
42
|
+
rows = [{ id: "c", count: 1 }];
|
|
43
|
+
render(<LiveCounter importedCount={0} label="designers subscribed" labelOne="designer subscribed" />);
|
|
44
|
+
expect(screen.getByTestId("live-count").textContent).toBe("1 designer subscribed");
|
|
45
|
+
});
|
package/templates/crm/README.md
CHANGED
|
@@ -14,6 +14,11 @@ Open http://localhost:4321 and create an account. The first sign-in seeds a demo
|
|
|
14
14
|
pipeline so the board isn't empty; delete `functions/seedWorkspace.ts` and
|
|
15
15
|
`lib/seed.ts` once you have real customers.
|
|
16
16
|
|
|
17
|
+
The deals are spread across you and four demo teammates. Each is a User
|
|
18
|
+
row with a `@demo.invalid` address and no password, so outside dev mode nobody
|
|
19
|
+
can sign in as one (see `lib/demo-team.ts`). Delete those users along with the
|
|
20
|
+
seed.
|
|
21
|
+
|
|
17
22
|
## What's interesting here
|
|
18
23
|
|
|
19
24
|
**The board is live.** Drag a deal to another column and it moves on every
|
|
@@ -31,6 +36,22 @@ win rate are computed from the deals themselves in `lib/pipeline.ts` — pure
|
|
|
31
36
|
functions with no React and no `db`, so the arithmetic is unit-tested directly
|
|
32
37
|
(`tests/pipeline.test.ts`).
|
|
33
38
|
|
|
39
|
+
## Brand and layout
|
|
40
|
+
|
|
41
|
+
The product ships as **Dealbook**. The name lives in `lib/brand.ts`, and the
|
|
42
|
+
sidebar, sign-in screen, page titles, and phone drawer all read it. The mark is
|
|
43
|
+
`components/brand-mark.tsx`; `app/icon.svg`, `app/apple-icon.png`, and
|
|
44
|
+
`public/favicon.ico` draw the same shape for browsers and home screens. The
|
|
45
|
+
accent colour is `--brand` at the top of `app/globals.css`, and the typeface
|
|
46
|
+
(Instrument Sans) is set in `fonts` in `app.ts`.
|
|
47
|
+
|
|
48
|
+
The frame is `components/app-shell.tsx`. From 768px up the sidebar is a fixed
|
|
49
|
+
rail; below that it opens as a drawer from the menu button in each page header,
|
|
50
|
+
tables drop their lower-priority columns (`hideBelow` in
|
|
51
|
+
`components/data-table.tsx`), and boards scroll one column at a time. The nav
|
|
52
|
+
links are in `components/nav.tsx`. Every nav count comes from `navCounts` in
|
|
53
|
+
`app/workspace.tsx`, and page headers and tabs read the same numbers.
|
|
54
|
+
|
|
34
55
|
## Layout
|
|
35
56
|
|
|
36
57
|
```
|
|
@@ -84,7 +105,7 @@ already themed.
|
|
|
84
105
|
- **Add a stage:** `PIPELINE` in `lib/pipeline.ts`. The board, the forecast, and
|
|
85
106
|
the stage picker all read from it.
|
|
86
107
|
- **Add a view:** drop `app/reports/page.tsx` and add it to `NAV` in
|
|
87
|
-
`components/
|
|
108
|
+
`components/nav.tsx`.
|
|
88
109
|
|
|
89
110
|
## Deploy
|
|
90
111
|
|
|
Binary file
|
|
@@ -9,7 +9,7 @@ import { DataTable, type ColumnDef } from "@/components/data-table";
|
|
|
9
9
|
import { EmptyState } from "@/components/empty-state";
|
|
10
10
|
import { RecordDialog } from "@/components/record-dialog";
|
|
11
11
|
import { Avatar } from "@/components/avatar";
|
|
12
|
-
import { money, relativeTime } from "@/lib/pipeline";
|
|
12
|
+
import { isOpen, money, relativeTime } from "@/lib/pipeline";
|
|
13
13
|
import { RequireAuth } from "@/components/require-auth";
|
|
14
14
|
import { Workspace, type CompanyRow, type DealRow } from "../workspace";
|
|
15
15
|
|
|
@@ -17,30 +17,37 @@ export function CompaniesView() {
|
|
|
17
17
|
const [open, setOpen] = useState(false);
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<RequireAuth
|
|
20
|
+
<RequireAuth>
|
|
21
21
|
<Workspace pathname="/companies">
|
|
22
22
|
{(data) => {
|
|
23
|
-
//
|
|
24
|
-
//
|
|
23
|
+
// Open pipeline per company, computed from the deals rather than
|
|
24
|
+
// stored, so a moved deal can't leave it stale.
|
|
25
25
|
const openValue = (companyId: string) =>
|
|
26
26
|
data.deals
|
|
27
|
-
.filter((deal: DealRow) => deal.companyId === companyId)
|
|
27
|
+
.filter((deal: DealRow) => deal.companyId === companyId && isOpen(deal))
|
|
28
28
|
.reduce((sum, deal) => sum + (Number(deal.value) || 0), 0);
|
|
29
29
|
|
|
30
30
|
const columns: ColumnDef<CompanyRow>[] = [
|
|
31
31
|
{
|
|
32
32
|
key: "name",
|
|
33
33
|
header: "Company",
|
|
34
|
+
className: "w-[46%] md:w-[26%]",
|
|
34
35
|
cell: (row) => (
|
|
35
|
-
<span className="flex items-center gap-2">
|
|
36
|
-
<Avatar name={row.name}
|
|
37
|
-
<span className="
|
|
36
|
+
<span className="flex min-w-0 items-center gap-2.5">
|
|
37
|
+
<Avatar name={row.name} shape="square" />
|
|
38
|
+
<span className="min-w-0">
|
|
39
|
+
<span className="block truncate font-medium">{row.name}</span>
|
|
40
|
+
<span className="block truncate text-[12px] text-muted-foreground md:hidden">
|
|
41
|
+
{row.industry ?? row.domain ?? ""}
|
|
42
|
+
</span>
|
|
43
|
+
</span>
|
|
38
44
|
</span>
|
|
39
45
|
),
|
|
40
46
|
},
|
|
41
47
|
{
|
|
42
48
|
key: "domain",
|
|
43
49
|
header: "Domain",
|
|
50
|
+
hideBelow: "md",
|
|
44
51
|
cell: (row) => (
|
|
45
52
|
<span className="text-muted-foreground">{row.domain ?? "—"}</span>
|
|
46
53
|
),
|
|
@@ -48,6 +55,7 @@ export function CompaniesView() {
|
|
|
48
55
|
{
|
|
49
56
|
key: "industry",
|
|
50
57
|
header: "Industry",
|
|
58
|
+
hideBelow: "md",
|
|
51
59
|
cell: (row) => (
|
|
52
60
|
<span className="text-muted-foreground">{row.industry ?? "—"}</span>
|
|
53
61
|
),
|
|
@@ -55,19 +63,27 @@ export function CompaniesView() {
|
|
|
55
63
|
{
|
|
56
64
|
key: "size",
|
|
57
65
|
header: "Size",
|
|
66
|
+
hideBelow: "lg",
|
|
67
|
+
className: "w-[110px]",
|
|
58
68
|
cell: (row) => (
|
|
59
69
|
<span className="text-muted-foreground">{row.size ?? "—"}</span>
|
|
60
70
|
),
|
|
61
71
|
},
|
|
62
72
|
{
|
|
63
73
|
key: "pipeline",
|
|
64
|
-
header: "
|
|
74
|
+
header: "Open pipeline",
|
|
65
75
|
numeric: true,
|
|
66
|
-
|
|
76
|
+
className: "w-[130px]",
|
|
77
|
+
cell: (row) => {
|
|
78
|
+
const value = openValue(row.id);
|
|
79
|
+
return value > 0 ? money(value) : <span className="text-muted-foreground">—</span>;
|
|
80
|
+
},
|
|
67
81
|
},
|
|
68
82
|
{
|
|
69
83
|
key: "created",
|
|
70
84
|
header: "Added",
|
|
85
|
+
hideBelow: "sm",
|
|
86
|
+
className: "w-[110px]",
|
|
71
87
|
cell: (row) => (
|
|
72
88
|
<span className="text-muted-foreground">
|
|
73
89
|
{relativeTime(row.createdAt)}
|
|
@@ -80,16 +96,18 @@ export function CompaniesView() {
|
|
|
80
96
|
|
|
81
97
|
return (
|
|
82
98
|
<>
|
|
83
|
-
<PageHeader title="Companies" count={
|
|
99
|
+
<PageHeader title="Companies" count={data.navCounts["/companies"]}>
|
|
84
100
|
<Button size="sm" onClick={() => setOpen(true)}>
|
|
85
101
|
<Plus />
|
|
86
|
-
New company
|
|
102
|
+
<span className="max-sm:sr-only">New company</span>
|
|
87
103
|
</Button>
|
|
88
104
|
</PageHeader>
|
|
89
105
|
|
|
90
106
|
<DataTable
|
|
91
107
|
rows={rows}
|
|
92
108
|
columns={columns}
|
|
109
|
+
loading={data.loading}
|
|
110
|
+
label="Companies"
|
|
93
111
|
empty={
|
|
94
112
|
<EmptyState
|
|
95
113
|
icon={<Building2 />}
|
|
@@ -111,8 +129,8 @@ export function CompaniesView() {
|
|
|
111
129
|
submitLabel="Create company"
|
|
112
130
|
onOpenChange={setOpen}
|
|
113
131
|
fields={[
|
|
114
|
-
{ name: "name", label: "Name", required: true, placeholder: "
|
|
115
|
-
{ name: "domain", label: "Domain", placeholder: "
|
|
132
|
+
{ name: "name", label: "Name", required: true, placeholder: "Northwind Logistics" },
|
|
133
|
+
{ name: "domain", label: "Domain", placeholder: "northwind.co" },
|
|
116
134
|
{ name: "industry", label: "Industry", placeholder: "Logistics" },
|
|
117
135
|
{ name: "size", label: "Size", placeholder: "50-200" },
|
|
118
136
|
]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { BRAND } from "@/lib/brand";
|
|
3
4
|
import { CompaniesView } from "./companies-view";
|
|
4
5
|
|
|
5
6
|
export const metadata: Metadata = {
|
|
6
|
-
title:
|
|
7
|
+
title: `Companies · ${BRAND.name}`,
|
|
7
8
|
robots: "noindex",
|
|
8
9
|
};
|
|
9
10
|
|
|
@@ -17,23 +17,30 @@ export function ContactsView() {
|
|
|
17
17
|
const [open, setOpen] = useState(false);
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<RequireAuth
|
|
20
|
+
<RequireAuth>
|
|
21
21
|
<Workspace pathname="/contacts">
|
|
22
22
|
{(data) => {
|
|
23
23
|
const columns: ColumnDef<ContactRow>[] = [
|
|
24
24
|
{
|
|
25
25
|
key: "name",
|
|
26
26
|
header: "Name",
|
|
27
|
+
className: "w-[55%] md:w-[22%]",
|
|
27
28
|
cell: (row) => (
|
|
28
|
-
<span className="flex items-center gap-2">
|
|
29
|
-
<Avatar name={row.name}
|
|
30
|
-
<span className="
|
|
29
|
+
<span className="flex min-w-0 items-center gap-2.5">
|
|
30
|
+
<Avatar name={row.name} />
|
|
31
|
+
<span className="min-w-0">
|
|
32
|
+
<span className="block truncate font-medium">{row.name}</span>
|
|
33
|
+
<span className="block truncate text-[12px] text-muted-foreground md:hidden">
|
|
34
|
+
{[row.title, data.companyName(row.companyId)].filter(Boolean).join(", ")}
|
|
35
|
+
</span>
|
|
36
|
+
</span>
|
|
31
37
|
</span>
|
|
32
38
|
),
|
|
33
39
|
},
|
|
34
40
|
{
|
|
35
41
|
key: "title",
|
|
36
42
|
header: "Title",
|
|
43
|
+
hideBelow: "md",
|
|
37
44
|
cell: (row) => (
|
|
38
45
|
<span className="text-muted-foreground">{row.title ?? "—"}</span>
|
|
39
46
|
),
|
|
@@ -41,6 +48,7 @@ export function ContactsView() {
|
|
|
41
48
|
{
|
|
42
49
|
key: "company",
|
|
43
50
|
header: "Company",
|
|
51
|
+
hideBelow: "md",
|
|
44
52
|
cell: (row) => (
|
|
45
53
|
<span className="text-muted-foreground">
|
|
46
54
|
{data.companyName(row.companyId) ?? "—"}
|
|
@@ -50,6 +58,8 @@ export function ContactsView() {
|
|
|
50
58
|
{
|
|
51
59
|
key: "email",
|
|
52
60
|
header: "Email",
|
|
61
|
+
hideBelow: "lg",
|
|
62
|
+
className: "w-[24%]",
|
|
53
63
|
cell: (row) =>
|
|
54
64
|
row.email ? (
|
|
55
65
|
<a
|
|
@@ -66,13 +76,25 @@ export function ContactsView() {
|
|
|
66
76
|
{
|
|
67
77
|
key: "phone",
|
|
68
78
|
header: "Phone",
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
className: "w-[140px]",
|
|
80
|
+
cell: (row) =>
|
|
81
|
+
row.phone ? (
|
|
82
|
+
<a
|
|
83
|
+
href={`tel:${row.phone.replace(/[^+\d]/g, "")}`}
|
|
84
|
+
onClick={(event) => event.stopPropagation()}
|
|
85
|
+
className="tabular text-muted-foreground hover:text-foreground"
|
|
86
|
+
>
|
|
87
|
+
{row.phone}
|
|
88
|
+
</a>
|
|
89
|
+
) : (
|
|
90
|
+
<span className="text-muted-foreground">—</span>
|
|
91
|
+
),
|
|
72
92
|
},
|
|
73
93
|
{
|
|
74
94
|
key: "created",
|
|
75
95
|
header: "Added",
|
|
96
|
+
hideBelow: "xl",
|
|
97
|
+
className: "w-[100px]",
|
|
76
98
|
cell: (row) => (
|
|
77
99
|
<span className="text-muted-foreground">
|
|
78
100
|
{relativeTime(row.createdAt)}
|
|
@@ -85,16 +107,18 @@ export function ContactsView() {
|
|
|
85
107
|
|
|
86
108
|
return (
|
|
87
109
|
<>
|
|
88
|
-
<PageHeader title="Contacts" count={
|
|
110
|
+
<PageHeader title="Contacts" count={data.navCounts["/contacts"]}>
|
|
89
111
|
<Button size="sm" onClick={() => setOpen(true)}>
|
|
90
112
|
<Plus />
|
|
91
|
-
New contact
|
|
113
|
+
<span className="max-sm:sr-only">New contact</span>
|
|
92
114
|
</Button>
|
|
93
115
|
</PageHeader>
|
|
94
116
|
|
|
95
117
|
<DataTable
|
|
96
118
|
rows={rows}
|
|
97
119
|
columns={columns}
|
|
120
|
+
loading={data.loading}
|
|
121
|
+
label="Contacts"
|
|
98
122
|
empty={
|
|
99
123
|
<EmptyState
|
|
100
124
|
icon={<Users />}
|
|
@@ -126,7 +150,7 @@ export function ContactsView() {
|
|
|
126
150
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
127
151
|
.map((company) => ({ value: company.id, label: company.name })),
|
|
128
152
|
},
|
|
129
|
-
{ name: "email", label: "Email", type: "email", placeholder: "dana@
|
|
153
|
+
{ name: "email", label: "Email", type: "email", placeholder: "dana@northwind.co" },
|
|
130
154
|
{ name: "phone", label: "Phone", type: "tel", placeholder: "+1 555 0100" },
|
|
131
155
|
]}
|
|
132
156
|
onCreate={async (values) => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { BRAND } from "@/lib/brand";
|
|
3
4
|
import { ContactsView } from "./contacts-view";
|
|
4
5
|
|
|
5
6
|
export const metadata: Metadata = {
|
|
6
|
-
title:
|
|
7
|
+
title: `Contacts · ${BRAND.name}`,
|
|
7
8
|
robots: "noindex",
|
|
8
9
|
};
|
|
9
10
|
|