@pylonsync/create-pylon 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +30 -7
- package/package.json +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/README.md +28 -3
- package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
- package/templates/agency/app/(marketing)/page.tsx +3 -3
- package/templates/agency/app/(marketing)/work/page.tsx +3 -3
- package/templates/agency/app/auth-form.tsx +4 -4
- package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
- package/templates/agency/app/dashboard/page.tsx +14 -16
- package/templates/agency/app/icon.svg +3 -0
- package/templates/agency/app/login/page.tsx +12 -14
- package/templates/agency/components/marketing.tsx +12 -5
- package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
- package/templates/agency/lib/agency.ts +22 -0
- package/templates/agency/lib/demo.ts +118 -0
- package/templates/agency/lib/site.config.ts +19 -13
- package/templates/agency/public/favicon.ico +0 -0
- package/templates/agency/tests/agency.test.ts +57 -0
- package/templates/ai-chat/.env.example +13 -27
- package/templates/ai-chat/README.md +53 -60
- package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
- package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
- package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
- package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
- package/templates/ai-chat/app/(chat)/page.tsx +7 -11
- package/templates/ai-chat/app/auth-form.tsx +45 -38
- package/templates/ai-chat/app/error.tsx +9 -12
- package/templates/ai-chat/app/globals.css +464 -102
- package/templates/ai-chat/app/layout.tsx +11 -19
- package/templates/ai-chat/app/login/page.tsx +39 -24
- package/templates/ai-chat/app.ts +118 -43
- package/templates/ai-chat/components/chat/composer.tsx +193 -0
- package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
- package/templates/ai-chat/components/chat/icons.tsx +198 -0
- package/templates/ai-chat/components/chat/markdown.tsx +164 -0
- package/templates/ai-chat/components/chat/message.tsx +183 -0
- package/templates/ai-chat/components/chat/setup.tsx +71 -0
- package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
- package/templates/ai-chat/functions/_beginTurn.ts +145 -0
- package/templates/ai-chat/functions/_finishTurn.ts +55 -0
- package/templates/ai-chat/functions/assistantStatus.ts +13 -0
- package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
- package/templates/ai-chat/functions/createConversation.ts +28 -0
- package/templates/ai-chat/functions/deleteConversation.ts +15 -20
- package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
- package/templates/ai-chat/functions/renameConversation.ts +19 -0
- package/templates/ai-chat/functions/respond.ts +88 -0
- package/templates/ai-chat/functions/seedExamples.ts +46 -0
- package/templates/ai-chat/functions/stopResponse.ts +30 -0
- package/templates/ai-chat/lib/brand-icon.ts +7 -0
- package/templates/ai-chat/lib/chat.ts +277 -0
- package/templates/ai-chat/lib/claim.ts +15 -0
- package/templates/ai-chat/lib/examples.ts +179 -0
- package/templates/ai-chat/lib/highlight.ts +107 -0
- package/templates/ai-chat/lib/markdown.ts +317 -0
- package/templates/ai-chat/lib/provider.ts +38 -0
- package/templates/ai-chat/lib/site.config.ts +55 -39
- package/templates/ai-chat/tests/chat.test.ts +174 -0
- package/templates/ai-chat/tests/components.test.tsx +57 -0
- package/templates/ai-chat/tests/markdown.test.ts +88 -0
- package/templates/ai-chat/tests/provider.test.ts +32 -0
- package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
- package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
- package/templates/backend/consumer/apps/api/app.ts +166 -0
- package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
- package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
- package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
- package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
- package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
- package/templates/backend/consumer/apps/api/package.json +8 -2
- package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
- package/templates/chat/README.md +43 -21
- package/templates/chat/app/chat-client.tsx +495 -85
- package/templates/chat/app/error.tsx +1 -1
- package/templates/chat/app/globals.css +170 -54
- package/templates/chat/app/layout.tsx +12 -9
- package/templates/chat/app/not-found.tsx +4 -2
- package/templates/chat/app/page.tsx +9 -19
- package/templates/chat/app.ts +96 -17
- package/templates/chat/components/chat/avatar.tsx +45 -0
- package/templates/chat/components/chat/composer.tsx +110 -0
- package/templates/chat/components/chat/message-list.tsx +285 -0
- package/templates/chat/components/chat/sidebar.tsx +190 -0
- package/templates/chat/functions/joinChat.ts +83 -0
- package/templates/chat/functions/renameMember.ts +25 -0
- package/templates/chat/lib/chat.ts +188 -0
- package/templates/chat/lib/seed.ts +78 -0
- package/templates/chat/tests/chat.test.ts +159 -0
- package/templates/chat/tests/composer.test.tsx +54 -0
- package/templates/consumer/README.md +3 -3
- package/templates/consumer/app/globals.css +10 -3
- package/templates/consumer/app/icon.svg +1 -0
- package/templates/consumer/components/social/avatar.tsx +25 -11
- package/templates/consumer/functions/seedFeed.ts +3 -3
- package/templates/consumer/lib/seed.ts +43 -85
- package/templates/consumer/public/favicon.ico +0 -0
- package/templates/consumer/public/images/posts/arch.jpg +0 -0
- package/templates/consumer/public/images/posts/coffee.jpg +0 -0
- package/templates/consumer/public/images/posts/dog.jpg +0 -0
- package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
- package/templates/consumer/public/images/posts/ramen.jpg +0 -0
- package/templates/consumer/public/images/posts/run.jpg +0 -0
- package/templates/consumer/public/images/posts/sunset.jpg +0 -0
- package/templates/consumer/tests/seed.test.ts +39 -0
- package/templates/creator/.env.example +7 -0
- package/templates/creator/README.md +24 -5
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
- package/templates/creator/app/auth-form.tsx +7 -7
- package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
- package/templates/creator/app/dashboard/page.tsx +18 -18
- package/templates/creator/app/icon.svg +2 -0
- package/templates/creator/app/login/page.tsx +14 -14
- package/templates/creator/app.ts +4 -4
- package/templates/creator/functions/seedDemo.ts +32 -0
- package/templates/creator/functions/subscribe.ts +4 -15
- package/templates/creator/lib/demo.ts +94 -0
- package/templates/creator/lib/site.config.ts +15 -10
- package/templates/creator/lib/stats.ts +35 -0
- package/templates/creator/public/favicon.ico +0 -0
- package/templates/creator/tests/counter.test.ts +92 -0
- package/templates/creator/tests/live-count.test.tsx +45 -0
- package/templates/crm/README.md +22 -1
- package/templates/crm/app/apple-icon.png +0 -0
- package/templates/crm/app/companies/companies-view.tsx +32 -14
- package/templates/crm/app/companies/page.tsx +2 -1
- package/templates/crm/app/contacts/contacts-view.tsx +34 -10
- package/templates/crm/app/contacts/page.tsx +2 -1
- package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
- package/templates/crm/app/deals/[id]/page.tsx +2 -1
- package/templates/crm/app/globals.css +29 -14
- package/templates/crm/app/icon.svg +1 -0
- package/templates/crm/app/layout.tsx +7 -5
- package/templates/crm/app/page.tsx +2 -1
- package/templates/crm/app/pipeline-view.tsx +5 -5
- package/templates/crm/app/workspace.tsx +29 -17
- package/templates/crm/app.ts +5 -5
- package/templates/crm/components/activity-timeline.tsx +14 -4
- package/templates/crm/components/app-shell.tsx +94 -0
- package/templates/crm/components/auth-form.tsx +35 -5
- package/templates/crm/components/avatar.tsx +5 -1
- package/templates/crm/components/board-skeleton.tsx +5 -5
- package/templates/crm/components/brand-mark.tsx +25 -0
- package/templates/crm/components/command-palette.tsx +17 -17
- package/templates/crm/components/data-table.tsx +97 -36
- package/templates/crm/components/deal-card.tsx +23 -27
- package/templates/crm/components/filter-tabs.tsx +101 -0
- package/templates/crm/components/metrics-bar.tsx +6 -6
- package/templates/crm/components/nav.tsx +15 -0
- package/templates/crm/components/page-header.tsx +43 -7
- package/templates/crm/components/pipeline-board.tsx +16 -10
- package/templates/crm/components/require-auth.tsx +24 -26
- package/templates/crm/components/sidebar.tsx +54 -49
- package/templates/crm/components/stage-badge.tsx +4 -2
- package/templates/crm/components/ui/button.tsx +5 -4
- package/templates/crm/components/ui/dialog.tsx +1 -1
- package/templates/crm/components/ui/input.tsx +1 -1
- package/templates/crm/components/ui/select.tsx +1 -1
- package/templates/crm/components/ui/textarea.tsx +1 -1
- package/templates/crm/functions/seedWorkspace.ts +32 -17
- package/templates/crm/lib/brand.ts +9 -0
- package/templates/crm/lib/demo-team.ts +51 -0
- package/templates/crm/lib/pipeline.ts +37 -0
- package/templates/crm/lib/search.ts +4 -3
- package/templates/crm/lib/seed.ts +53 -8
- package/templates/crm/public/favicon.ico +0 -0
- package/templates/crm/tests/components.test.tsx +5 -5
- package/templates/crm/tests/demo-team.test.ts +18 -0
- package/templates/crm/tests/pipeline.test.ts +59 -3
- package/templates/directory/.env.example +7 -0
- package/templates/directory/README.md +22 -3
- package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
- package/templates/directory/app/(marketing)/page.tsx +6 -4
- package/templates/directory/app/auth-form.tsx +4 -4
- package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
- package/templates/directory/app/dashboard/page.tsx +16 -14
- package/templates/directory/app/icon.svg +4 -0
- package/templates/directory/app/login/page.tsx +16 -15
- package/templates/directory/functions/seedDemo.ts +43 -0
- package/templates/directory/lib/demo.ts +145 -0
- package/templates/directory/lib/directory.ts +27 -0
- package/templates/directory/lib/site.config.ts +21 -16
- package/templates/directory/public/favicon.ico +0 -0
- package/templates/directory/tests/directory.test.ts +46 -0
- package/templates/expo/consumer/apps/expo/App.tsx +360 -288
- package/templates/expo/consumer/apps/expo/app.json +9 -1
- package/templates/expo/consumer/apps/expo/package.json +2 -1
- package/templates/helpdesk/README.md +21 -0
- package/templates/helpdesk/app/apple-icon.png +0 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
- package/templates/helpdesk/app/customers/page.tsx +2 -1
- package/templates/helpdesk/app/globals.css +29 -14
- package/templates/helpdesk/app/icon.svg +1 -0
- package/templates/helpdesk/app/inbox-view.tsx +66 -47
- package/templates/helpdesk/app/layout.tsx +7 -5
- package/templates/helpdesk/app/page.tsx +4 -7
- package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
- package/templates/helpdesk/app/workspace.tsx +33 -22
- package/templates/helpdesk/app.ts +2 -2
- package/templates/helpdesk/components/app-shell.tsx +94 -0
- package/templates/helpdesk/components/auth-form.tsx +35 -5
- package/templates/helpdesk/components/avatar.tsx +5 -1
- package/templates/helpdesk/components/brand-mark.tsx +28 -0
- package/templates/helpdesk/components/command-palette.tsx +17 -17
- package/templates/helpdesk/components/data-table.tsx +97 -36
- package/templates/helpdesk/components/filter-tabs.tsx +101 -0
- package/templates/helpdesk/components/nav.tsx +14 -0
- package/templates/helpdesk/components/page-header.tsx +43 -7
- package/templates/helpdesk/components/priority-badge.tsx +49 -6
- package/templates/helpdesk/components/require-auth.tsx +24 -26
- package/templates/helpdesk/components/sidebar.tsx +54 -48
- package/templates/helpdesk/components/sla-indicator.tsx +5 -8
- package/templates/helpdesk/components/thread.tsx +7 -7
- package/templates/helpdesk/components/ticket-list.tsx +79 -19
- package/templates/helpdesk/components/ui/button.tsx +5 -4
- package/templates/helpdesk/components/ui/dialog.tsx +1 -1
- package/templates/helpdesk/components/ui/input.tsx +1 -1
- package/templates/helpdesk/components/ui/select.tsx +1 -1
- package/templates/helpdesk/components/ui/textarea.tsx +1 -1
- package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
- package/templates/helpdesk/lib/brand.ts +9 -0
- package/templates/helpdesk/lib/demo-team.ts +51 -0
- package/templates/helpdesk/lib/search.ts +4 -3
- package/templates/helpdesk/lib/seed.ts +43 -2
- package/templates/helpdesk/lib/tickets.ts +68 -20
- package/templates/helpdesk/public/favicon.ico +0 -0
- package/templates/helpdesk/tests/components.test.tsx +10 -0
- package/templates/helpdesk/tests/demo-team.test.ts +18 -0
- package/templates/helpdesk/tests/tickets.test.ts +70 -0
- package/templates/imessage/.env.example +49 -0
- package/templates/imessage/AGENTS.md +153 -0
- package/templates/imessage/CLAUDE.md +7 -0
- package/templates/imessage/README.md +187 -0
- package/templates/imessage/app/console.tsx +382 -0
- package/templates/imessage/app/contacts/page.tsx +13 -0
- package/templates/imessage/app/error.tsx +16 -0
- package/templates/imessage/app/gate.tsx +31 -0
- package/templates/imessage/app/globals.css +183 -0
- package/templates/imessage/app/layout.tsx +17 -0
- package/templates/imessage/app/login/login-form.tsx +115 -0
- package/templates/imessage/app/login/page.tsx +30 -0
- package/templates/imessage/app/not-found.tsx +13 -0
- package/templates/imessage/app/page.tsx +14 -0
- package/templates/imessage/app/robots.ts +6 -0
- package/templates/imessage/app/setup/page.tsx +13 -0
- package/templates/imessage/app/sign-out.tsx +20 -0
- package/templates/imessage/app.ts +299 -0
- package/templates/imessage/bunfig +4 -0
- package/templates/imessage/components/avatar.tsx +46 -0
- package/templates/imessage/components/contact-panel.tsx +263 -0
- package/templates/imessage/components/contacts-view.tsx +172 -0
- package/templates/imessage/components/conversation-list.tsx +129 -0
- package/templates/imessage/components/nav-rail.tsx +67 -0
- package/templates/imessage/components/setup-view.tsx +408 -0
- package/templates/imessage/components/thread.tsx +229 -0
- package/templates/imessage/components/ui/badge.tsx +37 -0
- package/templates/imessage/components/ui/button.tsx +56 -0
- package/templates/imessage/components/ui/card.tsx +90 -0
- package/templates/imessage/components/ui/dialog.tsx +110 -0
- package/templates/imessage/components/ui/input.tsx +30 -0
- package/templates/imessage/components/ui/label.tsx +21 -0
- package/templates/imessage/components/ui/select.tsx +39 -0
- package/templates/imessage/components/ui/table.tsx +124 -0
- package/templates/imessage/components/ui/textarea.tsx +24 -0
- package/templates/imessage/components.json +20 -0
- package/templates/imessage/functions/answerNow.ts +34 -0
- package/templates/imessage/functions/assistant.ts +96 -0
- package/templates/imessage/functions/beginTurn.ts +101 -0
- package/templates/imessage/functions/cancelReminder.ts +13 -0
- package/templates/imessage/functions/claimOutbound.ts +44 -0
- package/templates/imessage/functions/clearDemo.ts +18 -0
- package/templates/imessage/functions/deleteNote.ts +11 -0
- package/templates/imessage/functions/deliverOutbound.ts +55 -0
- package/templates/imessage/functions/finishTurn.ts +84 -0
- package/templates/imessage/functions/fireReminder.ts +42 -0
- package/templates/imessage/functions/ingestInbound.ts +22 -0
- package/templates/imessage/functions/ownerAccount.ts +13 -0
- package/templates/imessage/functions/processTurn.ts +74 -0
- package/templates/imessage/functions/relayApply.ts +102 -0
- package/templates/imessage/functions/relaySync.ts +63 -0
- package/templates/imessage/functions/releaseOutbound.ts +18 -0
- package/templates/imessage/functions/resolveTurn.ts +25 -0
- package/templates/imessage/functions/saveContact.ts +27 -0
- package/templates/imessage/functions/seedDemo.ts +70 -0
- package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
- package/templates/imessage/functions/sendblueWebhook.ts +49 -0
- package/templates/imessage/functions/setContactStatus.ts +17 -0
- package/templates/imessage/functions/settleOutbound.ts +35 -0
- package/templates/imessage/functions/setupStatus.ts +33 -0
- package/templates/imessage/functions/sweep.ts +58 -0
- package/templates/imessage/functions/toolCancelReminder.ts +17 -0
- package/templates/imessage/functions/toolForgetNote.ts +15 -0
- package/templates/imessage/functions/toolListNotes.ts +16 -0
- package/templates/imessage/functions/toolListReminders.ts +17 -0
- package/templates/imessage/functions/toolSaveNote.ts +22 -0
- package/templates/imessage/functions/toolSetReminder.ts +55 -0
- package/templates/imessage/functions/updateSettings.ts +48 -0
- package/templates/imessage/gitignore +11 -0
- package/templates/imessage/lib/agent-call.ts +251 -0
- package/templates/imessage/lib/assistant.ts +74 -0
- package/templates/imessage/lib/chunk.ts +135 -0
- package/templates/imessage/lib/demo.ts +68 -0
- package/templates/imessage/lib/format.ts +170 -0
- package/templates/imessage/lib/gate.ts +104 -0
- package/templates/imessage/lib/handles.ts +67 -0
- package/templates/imessage/lib/provider.ts +38 -0
- package/templates/imessage/lib/relay-protocol.ts +95 -0
- package/templates/imessage/lib/reminders.ts +41 -0
- package/templates/imessage/lib/secrets.ts +59 -0
- package/templates/imessage/lib/sendblue.ts +173 -0
- package/templates/imessage/lib/store.ts +407 -0
- package/templates/imessage/lib/transport.ts +135 -0
- package/templates/imessage/lib/utils.ts +10 -0
- package/templates/imessage/package.json +40 -0
- package/templates/imessage/relay/.env.example +14 -0
- package/templates/imessage/relay/applescript.ts +70 -0
- package/templates/imessage/relay/chatdb.ts +165 -0
- package/templates/imessage/relay/client.ts +41 -0
- package/templates/imessage/relay/main.ts +137 -0
- package/templates/imessage/relay/state.ts +35 -0
- package/templates/imessage/tests/agent-call.test.ts +112 -0
- package/templates/imessage/tests/applescript.test.ts +88 -0
- package/templates/imessage/tests/chatdb.test.ts +139 -0
- package/templates/imessage/tests/chunk.test.ts +61 -0
- package/templates/imessage/tests/fake-ctx.ts +88 -0
- package/templates/imessage/tests/ingest.test.ts +141 -0
- package/templates/imessage/tests/lib.test.ts +99 -0
- package/templates/imessage/tests/review-fixes.test.ts +138 -0
- package/templates/imessage/tests/secrets.test.ts +36 -0
- package/templates/imessage/tests/sendblue.test.ts +143 -0
- package/templates/imessage/tests/setup.ts +5 -0
- package/templates/imessage/tsconfig.json +18 -0
- package/templates/inventory/README.md +21 -0
- package/templates/inventory/app/apple-icon.png +0 -0
- package/templates/inventory/app/globals.css +29 -14
- package/templates/inventory/app/icon.svg +1 -0
- package/templates/inventory/app/layout.tsx +7 -5
- package/templates/inventory/app/movements/movements-view.tsx +32 -10
- package/templates/inventory/app/movements/page.tsx +2 -1
- package/templates/inventory/app/page.tsx +4 -6
- package/templates/inventory/app/products/[id]/page.tsx +2 -1
- package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
- package/templates/inventory/app/products-view.tsx +74 -33
- package/templates/inventory/app/workspace.tsx +34 -16
- package/templates/inventory/app.ts +10 -2
- package/templates/inventory/components/app-shell.tsx +94 -0
- package/templates/inventory/components/auth-form.tsx +35 -5
- package/templates/inventory/components/avatar.tsx +5 -1
- package/templates/inventory/components/brand-mark.tsx +25 -0
- package/templates/inventory/components/category-tile.tsx +49 -0
- package/templates/inventory/components/command-palette.tsx +17 -17
- package/templates/inventory/components/data-table.tsx +97 -36
- package/templates/inventory/components/filter-tabs.tsx +101 -0
- package/templates/inventory/components/nav.tsx +14 -0
- package/templates/inventory/components/page-header.tsx +43 -7
- package/templates/inventory/components/require-auth.tsx +24 -26
- package/templates/inventory/components/sidebar.tsx +54 -48
- package/templates/inventory/components/stock-level.tsx +30 -14
- package/templates/inventory/components/summary-bar.tsx +8 -15
- package/templates/inventory/components/ui/button.tsx +5 -4
- package/templates/inventory/components/ui/dialog.tsx +1 -1
- package/templates/inventory/components/ui/input.tsx +1 -1
- package/templates/inventory/components/ui/select.tsx +1 -1
- package/templates/inventory/components/ui/textarea.tsx +1 -1
- package/templates/inventory/functions/seedWorkspace.ts +18 -8
- package/templates/inventory/lib/brand.ts +9 -0
- package/templates/inventory/lib/demo-team.ts +51 -0
- package/templates/inventory/lib/search.ts +4 -3
- package/templates/inventory/lib/seed.ts +29 -2
- package/templates/inventory/lib/stock.ts +32 -0
- package/templates/inventory/public/favicon.ico +0 -0
- package/templates/inventory/tests/demo-team.test.ts +18 -0
- package/templates/inventory/tests/stock.test.ts +29 -0
- package/templates/invoices/README.md +16 -0
- package/templates/invoices/app/apple-icon.png +0 -0
- package/templates/invoices/app/clients/clients-view.tsx +18 -6
- package/templates/invoices/app/clients/page.tsx +2 -1
- package/templates/invoices/app/globals.css +29 -14
- package/templates/invoices/app/icon.svg +1 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
- package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
- package/templates/invoices/app/invoices-view.tsx +81 -32
- package/templates/invoices/app/layout.tsx +7 -5
- package/templates/invoices/app/page.tsx +4 -6
- package/templates/invoices/app/workspace.tsx +26 -16
- package/templates/invoices/app.ts +2 -2
- package/templates/invoices/components/app-shell.tsx +94 -0
- package/templates/invoices/components/auth-form.tsx +35 -5
- package/templates/invoices/components/avatar.tsx +5 -1
- package/templates/invoices/components/brand-mark.tsx +26 -0
- package/templates/invoices/components/command-palette.tsx +17 -17
- package/templates/invoices/components/data-table.tsx +97 -36
- package/templates/invoices/components/filter-tabs.tsx +101 -0
- package/templates/invoices/components/line-items.tsx +19 -14
- package/templates/invoices/components/nav.tsx +14 -0
- package/templates/invoices/components/page-header.tsx +43 -7
- package/templates/invoices/components/require-auth.tsx +24 -26
- package/templates/invoices/components/sidebar.tsx +54 -48
- package/templates/invoices/components/status-badge.tsx +7 -7
- package/templates/invoices/components/summary-bar.tsx +5 -5
- package/templates/invoices/components/ui/button.tsx +5 -4
- package/templates/invoices/components/ui/dialog.tsx +1 -1
- package/templates/invoices/components/ui/input.tsx +1 -1
- package/templates/invoices/components/ui/select.tsx +1 -1
- package/templates/invoices/components/ui/textarea.tsx +1 -1
- package/templates/invoices/functions/recordPayment.ts +3 -0
- package/templates/invoices/lib/billing.ts +40 -0
- package/templates/invoices/lib/brand.ts +9 -0
- package/templates/invoices/lib/search.ts +4 -3
- package/templates/invoices/public/favicon.ico +0 -0
- package/templates/invoices/tests/billing.test.ts +27 -0
- package/templates/ios/consumer/apps/ios/Package.swift +4 -3
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
- package/templates/ios/consumer/apps/ios/project.yml +13 -9
- package/templates/local-service/.env.example +7 -0
- package/templates/local-service/README.md +26 -2
- package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
- package/templates/local-service/app/(marketing)/page.tsx +66 -42
- package/templates/local-service/app/auth-form.tsx +8 -8
- package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
- package/templates/local-service/app/dashboard/page.tsx +14 -14
- package/templates/local-service/app/icon.svg +2 -0
- package/templates/local-service/app/login/page.tsx +15 -15
- package/templates/local-service/functions/seedDemo.ts +49 -0
- package/templates/local-service/lib/demo.ts +135 -0
- package/templates/local-service/lib/hours.ts +44 -0
- package/templates/local-service/lib/site.config.ts +8 -3
- package/templates/local-service/lib/slots.ts +3 -0
- package/templates/local-service/public/favicon.ico +0 -0
- package/templates/local-service/tests/booking.test.ts +104 -0
- package/templates/marketplace/README.md +23 -15
- package/templates/marketplace/app/globals.css +172 -114
- package/templates/marketplace/app/layout.tsx +75 -30
- package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
- package/templates/marketplace/app/me/page.tsx +4 -4
- package/templates/marketplace/app/page.tsx +221 -298
- package/templates/marketplace/app/sell/page.tsx +8 -11
- package/templates/marketplace/app.ts +32 -7
- package/templates/marketplace/client/ActivityFeed.tsx +311 -0
- package/templates/marketplace/client/AuthNav.tsx +24 -16
- package/templates/marketplace/client/ListingCard.tsx +118 -0
- package/templates/marketplace/client/LiveGrid.tsx +92 -0
- package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
- package/templates/marketplace/client/LoginCard.tsx +114 -136
- package/templates/marketplace/client/MarketProvider.tsx +3 -5
- package/templates/marketplace/client/MyMarket.tsx +371 -185
- package/templates/marketplace/client/OfferPanel.tsx +421 -287
- package/templates/marketplace/client/SellForm.tsx +271 -192
- package/templates/marketplace/client/ThemeToggle.tsx +1 -0
- package/templates/marketplace/client/WatchButton.tsx +16 -16
- package/templates/marketplace/client/market.ts +61 -14
- package/templates/marketplace/functions/seedMarket.ts +143 -36
- package/templates/marketplace/functions/seedOffers.ts +101 -0
- package/templates/marketplace/lib/catalog.ts +72 -1
- package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
- package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
- package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
- package/templates/marketplace/public/images/listings/watch.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +64 -2
- package/templates/marketplace/tests/listing-card.test.tsx +61 -0
- package/templates/projects/README.md +24 -3
- package/templates/projects/app/apple-icon.png +0 -0
- package/templates/projects/app/clients/clients-view.tsx +20 -6
- package/templates/projects/app/clients/page.tsx +2 -1
- package/templates/projects/app/globals.css +29 -14
- package/templates/projects/app/icon.svg +1 -0
- package/templates/projects/app/layout.tsx +7 -5
- package/templates/projects/app/page.tsx +3 -5
- package/templates/projects/app/projects/[id]/page.tsx +2 -1
- package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
- package/templates/projects/app/projects-view.tsx +105 -52
- package/templates/projects/app/workspace.tsx +41 -20
- package/templates/projects/app.ts +2 -2
- package/templates/projects/components/app-shell.tsx +94 -0
- package/templates/projects/components/auth-form.tsx +35 -5
- package/templates/projects/components/avatar.tsx +5 -1
- package/templates/projects/components/board-skeleton.tsx +3 -3
- package/templates/projects/components/brand-mark.tsx +25 -0
- package/templates/projects/components/command-palette.tsx +17 -17
- package/templates/projects/components/data-table.tsx +97 -36
- package/templates/projects/components/filter-tabs.tsx +101 -0
- package/templates/projects/components/meter.tsx +48 -0
- package/templates/projects/components/nav.tsx +14 -0
- package/templates/projects/components/page-header.tsx +43 -7
- package/templates/projects/components/require-auth.tsx +24 -26
- package/templates/projects/components/sidebar.tsx +54 -48
- package/templates/projects/components/summary-bar.tsx +49 -0
- package/templates/projects/components/task-board.tsx +10 -9
- package/templates/projects/components/time-dialog.tsx +29 -5
- package/templates/projects/components/ui/button.tsx +5 -4
- package/templates/projects/components/ui/dialog.tsx +1 -1
- package/templates/projects/components/ui/input.tsx +1 -1
- package/templates/projects/components/ui/select.tsx +1 -1
- package/templates/projects/components/ui/textarea.tsx +1 -1
- package/templates/projects/functions/seedWorkspace.ts +26 -12
- package/templates/projects/lib/brand.ts +9 -0
- package/templates/projects/lib/demo-team.ts +51 -0
- package/templates/projects/lib/search.ts +4 -3
- package/templates/projects/lib/seed.ts +35 -1
- package/templates/projects/lib/work.ts +63 -0
- package/templates/projects/public/favicon.ico +0 -0
- package/templates/projects/tests/demo-team.test.ts +18 -0
- package/templates/projects/tests/work.test.ts +65 -0
- package/templates/restaurant/.env.example +7 -0
- package/templates/restaurant/README.md +29 -3
- package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
- package/templates/restaurant/app/(marketing)/page.tsx +47 -23
- package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
- package/templates/restaurant/app/auth-form.tsx +8 -8
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
- package/templates/restaurant/app/dashboard/page.tsx +14 -14
- package/templates/restaurant/app/icon.svg +4 -0
- package/templates/restaurant/app/login/page.tsx +15 -15
- package/templates/restaurant/functions/seedDemo.ts +44 -0
- package/templates/restaurant/lib/demo.ts +144 -0
- package/templates/restaurant/lib/hours.ts +45 -0
- package/templates/restaurant/lib/site.config.ts +3 -3
- package/templates/restaurant/public/favicon.ico +0 -0
- package/templates/restaurant/tests/reservations.test.ts +67 -0
- package/templates/saas/README.md +54 -35
- package/templates/saas/app/(auth)/auth-form.tsx +25 -23
- package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/layout.tsx +27 -33
- package/templates/saas/app/(auth)/login/page.tsx +1 -1
- package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/signup/page.tsx +1 -1
- package/templates/saas/app/(marketing)/layout.tsx +127 -253
- package/templates/saas/app/(marketing)/page.tsx +133 -372
- package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
- package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
- package/templates/saas/app/dashboard/board-client.tsx +416 -0
- package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
- package/templates/saas/app/dashboard/layout.tsx +27 -13
- package/templates/saas/app/dashboard/overview-client.tsx +332 -0
- package/templates/saas/app/dashboard/page.tsx +29 -35
- package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
- package/templates/saas/app/dashboard/projects/page.tsx +11 -7
- package/templates/saas/app/dashboard/projects-client.tsx +398 -0
- package/templates/saas/app/globals.css +5 -4
- package/templates/saas/app/icon.svg +1 -0
- package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
- package/templates/saas/app/onboarding/page.tsx +6 -2
- package/templates/saas/app.ts +68 -13
- package/templates/saas/components/dashboard-shell.tsx +212 -132
- package/templates/saas/components/marketing.tsx +137 -274
- package/templates/saas/components/pricing-table.tsx +12 -11
- package/templates/saas/components/setup-checklist.tsx +31 -29
- package/templates/saas/components/task-ui.tsx +199 -0
- package/templates/saas/functions/createProject.ts +7 -13
- package/templates/saas/functions/createTask.ts +67 -0
- package/templates/saas/functions/deleteProject.ts +17 -0
- package/templates/saas/functions/deleteTask.ts +14 -0
- package/templates/saas/functions/seedWorkspace.ts +46 -0
- package/templates/saas/functions/setProjectStatus.ts +41 -0
- package/templates/saas/functions/updateProfile.ts +13 -0
- package/templates/saas/functions/updateProject.ts +4 -5
- package/templates/saas/functions/updateTask.ts +88 -0
- package/templates/saas/lib/members.ts +53 -0
- package/templates/saas/lib/names.ts +53 -0
- package/templates/saas/lib/plans.ts +11 -0
- package/templates/saas/lib/sample-workspace.ts +117 -0
- package/templates/saas/lib/site.config.ts +200 -463
- package/templates/saas/lib/tasks.ts +101 -0
- package/templates/saas/lib/use-live.ts +25 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/public/screenshots/board.png +0 -0
- package/templates/saas/public/screenshots/members.png +0 -0
- package/templates/saas/public/screenshots/overview.png +0 -0
- package/templates/saas/public/screenshots/projects.png +0 -0
- package/templates/saas/scripts/capture-screenshots.mjs +156 -0
- package/templates/saas/tests/names.test.ts +35 -0
- package/templates/saas/tests/plans.test.ts +12 -0
- package/templates/saas/tests/sample-workspace.test.ts +46 -0
- package/templates/saas/tests/tasks.test.ts +81 -0
- package/templates/shop/README.md +25 -13
- package/templates/shop/app/(marketing)/layout.tsx +94 -67
- package/templates/shop/app/(marketing)/not-found.tsx +13 -12
- package/templates/shop/app/(marketing)/page.tsx +120 -59
- package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
- package/templates/shop/app/(marketing)/success/page.tsx +75 -22
- package/templates/shop/app/globals.css +94 -1
- package/templates/shop/app/layout.tsx +4 -4
- package/templates/shop/app/login/page.tsx +4 -4
- package/templates/shop/app.ts +18 -6
- package/templates/shop/components/cart-button.tsx +33 -0
- package/templates/shop/components/marketing.tsx +17 -87
- package/templates/shop/components/shelf-count.tsx +36 -0
- package/templates/shop/functions/checkout.ts +17 -1
- package/templates/shop/lib/cart-store.ts +57 -0
- package/templates/shop/lib/shelf-store.ts +35 -0
- package/templates/shop/lib/shop.ts +27 -0
- package/templates/shop/lib/site.config.ts +108 -98
- package/templates/shop/public/images/hero.jpg +0 -0
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/shop/public/images/products/sea-salt.jpg +0 -0
- package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
- package/templates/shop/tests/shop.test.ts +64 -0
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/README.md +29 -13
- package/templates/waitlist/app/(marketing)/page.tsx +9 -7
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
- package/templates/waitlist/app/auth-form.tsx +8 -8
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
- package/templates/waitlist/app/dashboard/page.tsx +17 -16
- package/templates/waitlist/app/icon.svg +2 -0
- package/templates/waitlist/app/login/page.tsx +14 -14
- package/templates/waitlist/app.ts +6 -6
- package/templates/waitlist/functions/joinWaitlist.ts +6 -18
- package/templates/waitlist/functions/seedDemo.ts +32 -0
- package/templates/waitlist/lib/demo.ts +94 -0
- package/templates/waitlist/lib/launch.ts +19 -0
- package/templates/waitlist/lib/site.config.ts +14 -9
- package/templates/waitlist/lib/stats.ts +35 -0
- package/templates/waitlist/public/favicon.ico +0 -0
- package/templates/waitlist/tests/counter.test.ts +107 -0
- package/templates/waitlist/tests/live-count.test.tsx +48 -0
- package/templates/ai-chat/tests/example.test.ts +0 -12
- package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
- package/templates/backend/consumer/apps/api/schema.ts +0 -143
- package/templates/chat/functions/_keep.ts +0 -13
- package/templates/chat/tests/example.test.ts +0 -12
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
- package/templates/marketplace/client/LiveTicker.tsx +0 -116
- package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
- package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mutation } from "@pylonsync/functions";
|
|
2
|
+
import { demoDataEnabled, demoSignups } from "../lib/demo";
|
|
3
|
+
import { syncWaitlistStat } from "../lib/stats";
|
|
4
|
+
|
|
5
|
+
// seedDemo — fill an empty waitlist with fictional signups in development.
|
|
6
|
+
//
|
|
7
|
+
// The landing page counter calls this when it finds no signups, and the
|
|
8
|
+
// dashboard calls it once on load. It writes nothing unless demo data is on
|
|
9
|
+
// (lib/demo.ts: PYLON_DEMO_DATA, else a `pylon dev` process), so `pylon start`,
|
|
10
|
+
// Docker, and Pylon Cloud deploys do not get demo rows. It also writes nothing
|
|
11
|
+
// once any Signup exists, so it cannot mix demo rows into a real list. Clearing
|
|
12
|
+
// every signup in development seeds the list again on the next load. The
|
|
13
|
+
// advisory lock stops two first visits from seeding twice.
|
|
14
|
+
//
|
|
15
|
+
// Public because the first visitor in `pylon dev` is anonymous. It returns only
|
|
16
|
+
// a flag, never a row.
|
|
17
|
+
export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
18
|
+
auth: "public",
|
|
19
|
+
async handler(ctx) {
|
|
20
|
+
if (!demoDataEnabled(ctx.env)) return { seeded: false };
|
|
21
|
+
|
|
22
|
+
await ctx.db.advisoryLock("waitlist_seed_demo");
|
|
23
|
+
const existing = await ctx.db.unsafe.list("Signup");
|
|
24
|
+
if (existing.length > 0) return { seeded: false };
|
|
25
|
+
|
|
26
|
+
for (const row of demoSignups(Date.now())) {
|
|
27
|
+
await ctx.db.unsafe.insert("Signup", { email: row.email, createdAt: row.createdAt });
|
|
28
|
+
}
|
|
29
|
+
await syncWaitlistStat(ctx.db.unsafe);
|
|
30
|
+
return { seeded: true };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Demo signups for local development.
|
|
2
|
+
//
|
|
3
|
+
// `pylon dev` fills an empty waitlist with about 180 fictional signups 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 DemoSignup {
|
|
61
|
+
email: string;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* About 180 signups over the 30 days before `nowMs`, rising toward today.
|
|
67
|
+
* Emails are unique. Every timestamp is in the past.
|
|
68
|
+
*/
|
|
69
|
+
export function demoSignups(nowMs: number, seed = 7): DemoSignup[] {
|
|
70
|
+
const rand = rng(seed);
|
|
71
|
+
const out: DemoSignup[] = [];
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The launch line under the hero form, from `siteConfig.hero.launchDate`.
|
|
2
|
+
//
|
|
3
|
+
// Before the date it names the month ("Public launch: March 2027"). From the
|
|
4
|
+
// date on it says invites are going out, so the page never shows a launch date
|
|
5
|
+
// that has already passed. The date is read and formatted in UTC, so the
|
|
6
|
+
// server render and the browser produce the same text.
|
|
7
|
+
|
|
8
|
+
/** `launchDate` is "YYYY-MM-DD" (UTC). Returns null for a malformed date. */
|
|
9
|
+
export function launchLabel(launchDate: string, nowMs: number): string | null {
|
|
10
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(launchDate.trim());
|
|
11
|
+
if (!m) return null;
|
|
12
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
13
|
+
const atMs = Date.UTC(y, mo - 1, d);
|
|
14
|
+
const at = new Date(atMs);
|
|
15
|
+
if (at.getUTCFullYear() !== y || at.getUTCMonth() !== mo - 1 || at.getUTCDate() !== d) return null;
|
|
16
|
+
if (nowMs >= atMs) return "Invites are going out now";
|
|
17
|
+
const month = at.toLocaleDateString("en-US", { month: "long", year: "numeric", timeZone: "UTC" });
|
|
18
|
+
return `Public launch: ${month}`;
|
|
19
|
+
}
|
|
@@ -36,7 +36,9 @@ export type MockRow = { title: string; tag: string; done?: boolean };
|
|
|
36
36
|
|
|
37
37
|
export type WaitlistConfig = BaseConfig & {
|
|
38
38
|
hero: {
|
|
39
|
-
|
|
39
|
+
// "YYYY-MM-DD" (UTC). The page shows the month until this date, then
|
|
40
|
+
// "Invites are going out now". See lib/launch.ts.
|
|
41
|
+
launchDate: string;
|
|
40
42
|
headline: string;
|
|
41
43
|
subcopy: string;
|
|
42
44
|
emailPlaceholder: string;
|
|
@@ -47,9 +49,11 @@ export type WaitlistConfig = BaseConfig & {
|
|
|
47
49
|
counter: {
|
|
48
50
|
enabled: boolean;
|
|
49
51
|
label: string; // e.g. "people on the list"
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
52
|
+
labelOne: string; // the same label for a count of 1, e.g. "person on the list"
|
|
53
|
+
// Signups collected before this site existed (an old form, a spreadsheet),
|
|
54
|
+
// added to the Signup table's count. Keep 0 unless those people are real;
|
|
55
|
+
// the page shows the sum as one number.
|
|
56
|
+
importedCount: number;
|
|
53
57
|
};
|
|
54
58
|
// The HTML/CSS product window on the right of the hero. No screenshot.
|
|
55
59
|
mock: {
|
|
@@ -93,11 +97,11 @@ export const siteConfig: WaitlistConfig = {
|
|
|
93
97
|
seo: {
|
|
94
98
|
title: "Lumo — projects, notes, and tasks in one workspace. Coming soon.",
|
|
95
99
|
description:
|
|
96
|
-
"Lumo puts your projects, notes, and tasks in one workspace.
|
|
100
|
+
"Lumo puts your projects, notes, and tasks in one workspace. Join the waitlist for an early invite.",
|
|
97
101
|
},
|
|
98
102
|
|
|
99
103
|
hero: {
|
|
100
|
-
|
|
104
|
+
launchDate: "2027-03-02",
|
|
101
105
|
headline: "Projects, notes, and tasks in one workspace.",
|
|
102
106
|
subcopy:
|
|
103
107
|
"Lumo is one app for the plan, the notes behind it, and the tasks that come out of it. Join the list and we send an invite before the public launch.",
|
|
@@ -110,7 +114,8 @@ export const siteConfig: WaitlistConfig = {
|
|
|
110
114
|
counter: {
|
|
111
115
|
enabled: true,
|
|
112
116
|
label: "people on the list",
|
|
113
|
-
|
|
117
|
+
labelOne: "person on the list",
|
|
118
|
+
importedCount: 0,
|
|
114
119
|
},
|
|
115
120
|
|
|
116
121
|
mock: {
|
|
@@ -148,8 +153,8 @@ export const siteConfig: WaitlistConfig = {
|
|
|
148
153
|
headline: "Questions",
|
|
149
154
|
items: [
|
|
150
155
|
{
|
|
151
|
-
q: "
|
|
152
|
-
a: "
|
|
156
|
+
q: "Who gets invited first?",
|
|
157
|
+
a: "People get invites in the order they joined. Each invite covers one workspace for up to five people.",
|
|
153
158
|
},
|
|
154
159
|
{
|
|
155
160
|
q: "What do I get for joining?",
|
|
@@ -28,3 +28,38 @@ export interface WaitlistStatsData {
|
|
|
28
28
|
export type WaitlistStatsResult =
|
|
29
29
|
| ({ authorized: true } & WaitlistStatsData)
|
|
30
30
|
| { authorized: false };
|
|
31
|
+
|
|
32
|
+
/* ------------------------- public live counter ------------------------- */
|
|
33
|
+
|
|
34
|
+
// The public counter shows the Signup table's row count, read from the
|
|
35
|
+
// PII-free WaitlistStat row. `importedCount` is `siteConfig.counter.importedCount`:
|
|
36
|
+
// signups collected before this site existed (an old form, a spreadsheet). It is
|
|
37
|
+
// 0 by default, so the page shows only rows in the table.
|
|
38
|
+
export function publicCount(
|
|
39
|
+
statRows: ReadonlyArray<{ count: number }>,
|
|
40
|
+
importedCount = 0,
|
|
41
|
+
): number {
|
|
42
|
+
const real = statRows.length > 0 ? Math.max(0, Math.floor(statRows[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 StatDb {
|
|
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 Signup row count into the single WaitlistStat row, creating it if
|
|
56
|
+
// missing. Every writer of Signup calls this in the same transaction, so the
|
|
57
|
+
// public number is always the table's count.
|
|
58
|
+
export async function syncWaitlistStat(db: StatDb): Promise<number> {
|
|
59
|
+
const total = (await db.list("Signup")).length;
|
|
60
|
+
const stat = (await db.list("WaitlistStat"))[0] as { id: string } | undefined;
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
if (stat) await db.update("WaitlistStat", stat.id, { count: total, updatedAt: now });
|
|
63
|
+
else await db.insert("WaitlistStat", { count: total, updatedAt: now });
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { publicCount, syncWaitlistStat, type StatDb } from "../lib/stats";
|
|
3
|
+
import { demoDataEnabled, demoSignups } from "../lib/demo";
|
|
4
|
+
import { launchLabel } from "../lib/launch";
|
|
5
|
+
import { siteConfig } from "../lib/site.config";
|
|
6
|
+
|
|
7
|
+
describe("publicCount", () => {
|
|
8
|
+
test("is the table's count when nothing is imported", () => {
|
|
9
|
+
expect(publicCount([{ count: 3 }])).toBe(3);
|
|
10
|
+
expect(publicCount([])).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
test("adds only an explicit, non-negative imported count", () => {
|
|
13
|
+
expect(publicCount([{ count: 3 }], 40)).toBe(43);
|
|
14
|
+
expect(publicCount([{ count: 3 }], -5)).toBe(3);
|
|
15
|
+
expect(publicCount([{ count: 3 }], Number.NaN)).toBe(3);
|
|
16
|
+
});
|
|
17
|
+
test("the shipped config adds nothing to the real count", () => {
|
|
18
|
+
expect(siteConfig.counter.importedCount).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// An in-memory stand-in for ctx.db.unsafe.
|
|
23
|
+
function fakeDb(signups: number): StatDb & { rows: Record<string, Record<string, unknown>[]> } {
|
|
24
|
+
const rows: Record<string, Record<string, unknown>[]> = {
|
|
25
|
+
Signup: Array.from({ length: signups }, (_, i) => ({ id: `s${i}` })),
|
|
26
|
+
WaitlistStat: [],
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
rows,
|
|
30
|
+
async list(entity) {
|
|
31
|
+
return rows[entity] ?? [];
|
|
32
|
+
},
|
|
33
|
+
async insert(entity, data) {
|
|
34
|
+
const id = `${entity}-${rows[entity].length}`;
|
|
35
|
+
rows[entity].push({ id, ...data });
|
|
36
|
+
return id;
|
|
37
|
+
},
|
|
38
|
+
async update(entity, id, data) {
|
|
39
|
+
const row = rows[entity].find((r) => r.id === id);
|
|
40
|
+
if (row) Object.assign(row, data);
|
|
41
|
+
return id;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("syncWaitlistStat", () => {
|
|
47
|
+
test("creates the stat row with the Signup count", async () => {
|
|
48
|
+
const db = fakeDb(5);
|
|
49
|
+
expect(await syncWaitlistStat(db)).toBe(5);
|
|
50
|
+
expect(db.rows.WaitlistStat).toHaveLength(1);
|
|
51
|
+
expect(db.rows.WaitlistStat[0].count).toBe(5);
|
|
52
|
+
});
|
|
53
|
+
test("recounts into the existing row instead of adding one", async () => {
|
|
54
|
+
const db = fakeDb(5);
|
|
55
|
+
await syncWaitlistStat(db);
|
|
56
|
+
db.rows.Signup.push({ id: "late" });
|
|
57
|
+
await syncWaitlistStat(db);
|
|
58
|
+
expect(db.rows.WaitlistStat).toHaveLength(1);
|
|
59
|
+
expect(db.rows.WaitlistStat[0].count).toBe(6);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("demoDataEnabled", () => {
|
|
64
|
+
test("needs a `pylon dev` process when PYLON_DEMO_DATA is unset", () => {
|
|
65
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "1", PYLON_DEV_WATCH_DIR: "/src/app" })).toBe(true);
|
|
66
|
+
// The Docker base image turns dev mode on by default; without `pylon dev` it must not seed.
|
|
67
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "true" })).toBe(false);
|
|
68
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "true", PYLON_DEV_WATCH_DIR: "/src/app" })).toBe(true);
|
|
69
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "false" })).toBe(false);
|
|
70
|
+
expect(demoDataEnabled({})).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
test("an explicit PYLON_DEMO_DATA wins", () => {
|
|
73
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "1", PYLON_DEV_WATCH_DIR: "/src/app", PYLON_DEMO_DATA: "0" })).toBe(false);
|
|
74
|
+
expect(demoDataEnabled({ PYLON_DEV_MODE: "false", PYLON_DEMO_DATA: "1" })).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("demoSignups", () => {
|
|
79
|
+
const now = Date.parse("2026-09-26T15:00:00Z");
|
|
80
|
+
const rows = demoSignups(now);
|
|
81
|
+
test("unique emails, all in the past 30 days", () => {
|
|
82
|
+
expect(new Set(rows.map((r) => r.email)).size).toBe(rows.length);
|
|
83
|
+
for (const r of rows) {
|
|
84
|
+
const t = Date.parse(r.createdAt);
|
|
85
|
+
expect(t).toBeLessThan(now);
|
|
86
|
+
expect(t).toBeGreaterThan(now - 31 * 86_400_000);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
test("enough rows to fill the chart, stable for a seed", () => {
|
|
90
|
+
expect(rows.length).toBeGreaterThan(120);
|
|
91
|
+
expect(demoSignups(now)).toEqual(rows);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("launchLabel", () => {
|
|
96
|
+
const now = Date.parse("2026-09-26T12:00:00Z");
|
|
97
|
+
test("names the month before the date", () => {
|
|
98
|
+
expect(launchLabel("2027-03-02", now)).toBe("Public launch: March 2027");
|
|
99
|
+
});
|
|
100
|
+
test("never shows a date that has passed", () => {
|
|
101
|
+
expect(launchLabel("2026-09-01", now)).toBe("Invites are going out now");
|
|
102
|
+
});
|
|
103
|
+
test("rejects a malformed date", () => {
|
|
104
|
+
expect(launchLabel("fall 2026", now)).toBeNull();
|
|
105
|
+
expect(launchLabel("2026-02-31", now)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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 WaitlistStat row. Mock the data boundary so the test
|
|
6
|
+
// controls what the "server" says.
|
|
7
|
+
let statRows: { id: string; count: number; updatedAt: string }[] = [];
|
|
8
|
+
let loading = false;
|
|
9
|
+
mock.module("@pylonsync/react", () => ({
|
|
10
|
+
db: { useQuery: () => ({ data: statRows, loading }) },
|
|
11
|
+
callFn: async () => ({ seeded: false }),
|
|
12
|
+
}));
|
|
13
|
+
mock.module("@pylonsync/client", () => ({
|
|
14
|
+
EnsureGuest: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
15
|
+
}));
|
|
16
|
+
const { LiveCount } = await import("../app/(marketing)/waitlist-hero");
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
cleanup();
|
|
20
|
+
statRows = [];
|
|
21
|
+
loading = false;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("shows the table's count, with no vanity baseline", () => {
|
|
25
|
+
statRows = [{ id: "w", count: 42, updatedAt: "" }];
|
|
26
|
+
render(<LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />);
|
|
27
|
+
expect(screen.getByTestId("live-count").textContent).toBe("42");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("shows nothing, not a placeholder number, before the first sync", () => {
|
|
31
|
+
loading = true;
|
|
32
|
+
render(<LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />);
|
|
33
|
+
expect(screen.queryByTestId("live-count")).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("uses the singular label for one signup", () => {
|
|
37
|
+
statRows = [{ id: "w", count: 1, updatedAt: "" }];
|
|
38
|
+
const { container } = render(
|
|
39
|
+
<LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />,
|
|
40
|
+
);
|
|
41
|
+
expect(container.textContent).toContain("1person on the list");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("shows nothing while the list is empty", () => {
|
|
45
|
+
statRows = [{ id: "w", count: 0, updatedAt: "" }];
|
|
46
|
+
render(<LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />);
|
|
47
|
+
expect(screen.queryByTestId("live-count")).toBeNull();
|
|
48
|
+
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
// Your first test. Run the suite with: pylon test (or: npm test)
|
|
4
|
-
//
|
|
5
|
-
// `pylon test` discovers every *.test.ts file under tests/ (or functions/) and
|
|
6
|
-
// runs it with Bun's test runner against an in-memory Pylon. Replace this
|
|
7
|
-
// placeholder with tests for your functions and helpers — see AGENTS.md →
|
|
8
|
-
// Testing for the pure-logic and HTTP-against-`pylon dev` patterns.
|
|
9
|
-
|
|
10
|
-
test("placeholder — replace me with a real test", () => {
|
|
11
|
-
expect(true).toBe(true);
|
|
12
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { query } from "@pylonsync/functions";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Public feed — every Post, newest first. Joins author Profile + the
|
|
5
|
-
* caller's like state into the response so the client can render a
|
|
6
|
-
* row without N+1 follow-up queries.
|
|
7
|
-
*
|
|
8
|
-
* For a real consumer app, paginate this (limit + cursor by createdAt)
|
|
9
|
-
* — the scaffold returns at most 100 rows, which is plenty for a
|
|
10
|
-
* Hello-World feed but unsustainable past a few thousand posts.
|
|
11
|
-
*/
|
|
12
|
-
export default query({
|
|
13
|
-
args: {},
|
|
14
|
-
async handler(ctx) {
|
|
15
|
-
const posts = (await ctx.db.query("Post", {})) as any[];
|
|
16
|
-
const sorted = [...posts]
|
|
17
|
-
.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
|
|
18
|
-
.slice(0, 100);
|
|
19
|
-
|
|
20
|
-
const callerProfile = ctx.auth.userId
|
|
21
|
-
? ((await ctx.db.query("Profile", {
|
|
22
|
-
userId: ctx.auth.userId,
|
|
23
|
-
})) as any[])[0]
|
|
24
|
-
: null;
|
|
25
|
-
|
|
26
|
-
const out: Array<{
|
|
27
|
-
id: string;
|
|
28
|
-
body: string;
|
|
29
|
-
createdAt: string;
|
|
30
|
-
author: { id: string; handle: string; displayName: string } | null;
|
|
31
|
-
likeCount: number;
|
|
32
|
-
likedByMe: boolean;
|
|
33
|
-
}> = [];
|
|
34
|
-
for (const p of sorted) {
|
|
35
|
-
const author = (await ctx.db.get("Profile", p.authorId)) as any;
|
|
36
|
-
const likes = (await ctx.db.query("Like", { postId: p.id })) as any[];
|
|
37
|
-
const likedByMe = callerProfile
|
|
38
|
-
? likes.some((l) => l.profileId === callerProfile.id)
|
|
39
|
-
: false;
|
|
40
|
-
out.push({
|
|
41
|
-
id: p.id,
|
|
42
|
-
body: p.body,
|
|
43
|
-
createdAt: p.createdAt,
|
|
44
|
-
author: author
|
|
45
|
-
? {
|
|
46
|
-
id: author.id,
|
|
47
|
-
handle: author.handle,
|
|
48
|
-
displayName: author.displayName,
|
|
49
|
-
}
|
|
50
|
-
: null,
|
|
51
|
-
likeCount: likes.length,
|
|
52
|
-
likedByMe,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
},
|
|
57
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { query, v } from "@pylonsync/functions";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Every Post by a single Profile, newest first. Used by the profile
|
|
5
|
-
* page on each platform.
|
|
6
|
-
*/
|
|
7
|
-
export default query({
|
|
8
|
-
args: { profileId: v.id("Profile") },
|
|
9
|
-
async handler(ctx, args: { profileId: string }) {
|
|
10
|
-
const rows = (await ctx.db.query("Post", {
|
|
11
|
-
authorId: args.profileId,
|
|
12
|
-
})) as any[];
|
|
13
|
-
return rows.sort(
|
|
14
|
-
(a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt),
|
|
15
|
-
);
|
|
16
|
-
},
|
|
17
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Toggle a like on a Post. Idempotent — calling twice from the same
|
|
5
|
-
* user lands at the original state. Returns { liked: boolean,
|
|
6
|
-
* likeCount: number } so the client can update its UI without a
|
|
7
|
-
* follow-up read.
|
|
8
|
-
*/
|
|
9
|
-
export default mutation({
|
|
10
|
-
args: { postId: v.id("Post") },
|
|
11
|
-
async handler(ctx, args: { postId: string }) {
|
|
12
|
-
if (!ctx.auth.userId) {
|
|
13
|
-
throw ctx.error("UNAUTHENTICATED", "log in first");
|
|
14
|
-
}
|
|
15
|
-
const profiles = (await ctx.db.query("Profile", {
|
|
16
|
-
userId: ctx.auth.userId,
|
|
17
|
-
})) as any[];
|
|
18
|
-
if (profiles.length === 0) {
|
|
19
|
-
throw ctx.error("NO_PROFILE", "create a profile first");
|
|
20
|
-
}
|
|
21
|
-
const profile = profiles[0];
|
|
22
|
-
|
|
23
|
-
const existing = (await ctx.db.query("Like", {
|
|
24
|
-
postId: args.postId,
|
|
25
|
-
profileId: profile.id,
|
|
26
|
-
})) as any[];
|
|
27
|
-
|
|
28
|
-
if (existing.length === 0) {
|
|
29
|
-
await ctx.db.insert("Like", {
|
|
30
|
-
postId: args.postId,
|
|
31
|
-
profileId: profile.id,
|
|
32
|
-
createdAt: new Date().toISOString(),
|
|
33
|
-
});
|
|
34
|
-
} else {
|
|
35
|
-
for (const like of existing) {
|
|
36
|
-
await ctx.db.delete("Like", like.id);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const allLikes = (await ctx.db.query("Like", {
|
|
41
|
-
postId: args.postId,
|
|
42
|
-
})) as any[];
|
|
43
|
-
return {
|
|
44
|
-
liked: existing.length === 0,
|
|
45
|
-
likeCount: allLikes.length,
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
});
|