@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
|
@@ -51,7 +51,7 @@ export function AuthForm() {
|
|
|
51
51
|
<div className="space-y-5">
|
|
52
52
|
<form onSubmit={onSubmit} className="space-y-4">
|
|
53
53
|
<label className="block">
|
|
54
|
-
<span className="mb-1.5 block text-[13px] font-medium text-
|
|
54
|
+
<span className="mb-1.5 block text-[13px] font-medium text-chalk-2">Email</span>
|
|
55
55
|
<input
|
|
56
56
|
type="email"
|
|
57
57
|
value={email}
|
|
@@ -59,11 +59,11 @@ export function AuthForm() {
|
|
|
59
59
|
required
|
|
60
60
|
autoComplete="email"
|
|
61
61
|
placeholder="you@yourbusiness.com"
|
|
62
|
-
className="h-
|
|
62
|
+
className="h-11 w-full rounded-md border border-line bg-ink px-3.5 text-[15px] text-chalk outline-none transition placeholder:text-chalk-2 focus:border-brand"
|
|
63
63
|
/>
|
|
64
64
|
</label>
|
|
65
65
|
<label className="block">
|
|
66
|
-
<span className="mb-1.5 block text-[13px] font-medium text-
|
|
66
|
+
<span className="mb-1.5 block text-[13px] font-medium text-chalk-2">Password</span>
|
|
67
67
|
<input
|
|
68
68
|
type="password"
|
|
69
69
|
value={password}
|
|
@@ -71,24 +71,24 @@ export function AuthForm() {
|
|
|
71
71
|
required
|
|
72
72
|
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
73
73
|
placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
|
|
74
|
-
className="h-
|
|
74
|
+
className="h-11 w-full rounded-md border border-line bg-ink px-3.5 text-[15px] text-chalk outline-none transition placeholder:text-chalk-2 focus:border-brand"
|
|
75
75
|
/>
|
|
76
76
|
</label>
|
|
77
77
|
{error ? (
|
|
78
|
-
<p className="rounded-
|
|
78
|
+
<p className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-[13px] leading-snug text-red-300">
|
|
79
79
|
{error}
|
|
80
80
|
</p>
|
|
81
81
|
) : null}
|
|
82
82
|
<button
|
|
83
83
|
type="submit"
|
|
84
84
|
disabled={pending}
|
|
85
|
-
className="inline-flex h-
|
|
85
|
+
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-brand text-[15px] font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60"
|
|
86
86
|
>
|
|
87
87
|
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
88
88
|
</button>
|
|
89
89
|
</form>
|
|
90
90
|
|
|
91
|
-
<p className="text-center text-[13px] text-
|
|
91
|
+
<p className="text-center text-[13px] text-chalk-2">
|
|
92
92
|
{mode === "login" ? "First time here?" : "Already have an account?"}{" "}
|
|
93
93
|
<button
|
|
94
94
|
type="button"
|
|
@@ -96,7 +96,7 @@ export function AuthForm() {
|
|
|
96
96
|
setMode(mode === "login" ? "signup" : "login");
|
|
97
97
|
setError(null);
|
|
98
98
|
}}
|
|
99
|
-
className="font-medium text-
|
|
99
|
+
className="font-medium text-chalk underline underline-offset-2"
|
|
100
100
|
>
|
|
101
101
|
{mode === "login" ? "Create the owner account" : "Sign in"}
|
|
102
102
|
</button>
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
import React, { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { db, callFn } from "@pylonsync/react";
|
|
5
5
|
import { useAuth } from "@pylonsync/client";
|
|
6
|
+
import { Download, LogOut, Search } from "lucide-react";
|
|
6
7
|
import type { WaitlistStatsData, WaitlistStatsResult, SignupRow } from "@/lib/stats";
|
|
7
8
|
|
|
8
|
-
// The owner's live dashboard. Liveness rides the
|
|
9
|
+
// The owner's live dashboard. Liveness rides the same public aggregate the
|
|
9
10
|
// landing page uses: `db.useQuery("WaitlistStat")` re-renders the instant the
|
|
10
|
-
// count changes (cross-tab, via the replica). The emails themselves never sync
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// stay live, but PII only ever travels through the gated call.
|
|
11
|
+
// count changes (cross-tab, via the replica). The emails themselves never sync.
|
|
12
|
+
// They come from the owner-gated `waitlistStats` function, fetched on mount and
|
|
13
|
+
// again whenever the live count changes, so PII only travels through that call.
|
|
14
14
|
export function WaitlistDashboard({ userEmail }: { userEmail: string }) {
|
|
15
15
|
const { data: statRows } = db.useQuery<{ id: string; count: number }>("WaitlistStat");
|
|
16
16
|
const liveCount = statRows.length > 0 ? statRows[0].count : 0;
|
|
@@ -19,6 +19,12 @@ export function WaitlistDashboard({ userEmail }: { userEmail: string }) {
|
|
|
19
19
|
const [denied, setDenied] = useState(false);
|
|
20
20
|
const [error, setError] = useState<string | null>(null);
|
|
21
21
|
|
|
22
|
+
// In `pylon dev` an empty waitlist gets demo signups (functions/seedDemo.ts).
|
|
23
|
+
// On a deploy the call returns without writing.
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
void callFn("seedDemo", {}).catch(() => {});
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
22
28
|
useEffect(() => {
|
|
23
29
|
let cancelled = false;
|
|
24
30
|
callFn<WaitlistStatsResult>("waitlistStats", {})
|
|
@@ -46,7 +52,7 @@ export function WaitlistDashboard({ userEmail }: { userEmail: string }) {
|
|
|
46
52
|
if (denied) return <OwnerOnly email={userEmail} />;
|
|
47
53
|
if (error) {
|
|
48
54
|
return (
|
|
49
|
-
<div className="rounded-
|
|
55
|
+
<div className="rounded-lg border border-red-500/30 bg-red-500/10 px-5 py-4 text-sm text-red-300">
|
|
50
56
|
{error}
|
|
51
57
|
</div>
|
|
52
58
|
);
|
|
@@ -54,15 +60,26 @@ export function WaitlistDashboard({ userEmail }: { userEmail: string }) {
|
|
|
54
60
|
if (!data) return <Skeleton />;
|
|
55
61
|
|
|
56
62
|
return (
|
|
57
|
-
<div className="space-y-
|
|
58
|
-
<div>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
<div className="space-y-10">
|
|
64
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
|
65
|
+
<div>
|
|
66
|
+
<h1 className="font-display text-[2rem] font-bold leading-none tracking-[-0.02em] text-chalk">
|
|
67
|
+
Waitlist
|
|
68
|
+
</h1>
|
|
69
|
+
<p className="mt-3 text-[14px] text-chalk-2">
|
|
70
|
+
Signups appear here as people join. The landing page shows the same total.
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<p className="flex items-center gap-2 font-mono-ui text-[12px] text-chalk-2">
|
|
74
|
+
<span className="relative flex size-2">
|
|
75
|
+
<span className="absolute inline-flex size-2 animate-ping rounded-full bg-brand/60" />
|
|
76
|
+
<span className="relative inline-flex size-2 rounded-full bg-brand" />
|
|
77
|
+
</span>
|
|
78
|
+
live
|
|
62
79
|
</p>
|
|
63
80
|
</div>
|
|
64
81
|
|
|
65
|
-
<div className="grid gap-
|
|
82
|
+
<div className="grid gap-px overflow-hidden rounded-lg border border-line bg-line grid-cols-3">
|
|
66
83
|
<Stat label="Total signups" value={data.total} />
|
|
67
84
|
<Stat label="Last 7 days" value={data.last7} />
|
|
68
85
|
<Stat label="Today" value={data.today} />
|
|
@@ -77,12 +94,10 @@ export function WaitlistDashboard({ userEmail }: { userEmail: string }) {
|
|
|
77
94
|
|
|
78
95
|
function Stat({ label, value }: { label: string; value: number }) {
|
|
79
96
|
return (
|
|
80
|
-
<div className="
|
|
81
|
-
<div className="text-[
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<div className="mt-1 text-2xl font-semibold tabular-nums text-zinc-900">
|
|
85
|
-
{value.toLocaleString()}
|
|
97
|
+
<div className="bg-paper px-3 py-4 sm:px-5 sm:py-5">
|
|
98
|
+
<div className="font-mono-ui text-[12px] text-chalk-2">{label}</div>
|
|
99
|
+
<div className="font-display mt-2 text-[1.75rem] sm:text-[2.25rem] font-bold leading-none tabular-nums tracking-[-0.02em] text-chalk">
|
|
100
|
+
{value.toLocaleString("en-US")}
|
|
86
101
|
</div>
|
|
87
102
|
</div>
|
|
88
103
|
);
|
|
@@ -92,121 +107,145 @@ function Stat({ label, value }: { label: string; value: number }) {
|
|
|
92
107
|
|
|
93
108
|
function TrendChart({ daily }: { daily: { date: string; count: number }[] }) {
|
|
94
109
|
const max = Math.max(1, ...daily.map((d) => d.count));
|
|
110
|
+
const total = daily.reduce((n, d) => n + d.count, 0);
|
|
95
111
|
return (
|
|
96
|
-
<
|
|
97
|
-
<div className="flex items-
|
|
98
|
-
<h2 className="text-
|
|
99
|
-
<span className="text-[12px] text-
|
|
100
|
-
|
|
112
|
+
<section className="rounded-lg border border-line bg-paper p-5 sm:p-6">
|
|
113
|
+
<div className="flex items-baseline justify-between gap-4">
|
|
114
|
+
<h2 className="font-display text-[17px] font-medium text-chalk">Last 30 days</h2>
|
|
115
|
+
<span className="font-mono-ui text-[12px] text-chalk-2">
|
|
116
|
+
{total.toLocaleString("en-US")} signups · peak {max}/day
|
|
101
117
|
</span>
|
|
102
118
|
</div>
|
|
103
|
-
{/* Each column is a
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<div
|
|
109
|
-
key={d.date}
|
|
110
|
-
className="group relative flex h-full flex-1 flex-col justify-end"
|
|
111
|
-
>
|
|
119
|
+
{/* Each column is a full-height flex cell that bottom-aligns its bar, so
|
|
120
|
+
the bar's percentage height resolves against the track. */}
|
|
121
|
+
<div className="mt-6 flex h-36 items-end gap-[3px]">
|
|
122
|
+
{daily.map((d, i) => (
|
|
123
|
+
<div key={d.date} className="group relative flex h-full flex-1 flex-col justify-end">
|
|
112
124
|
<div
|
|
113
|
-
className=
|
|
125
|
+
className={
|
|
126
|
+
"w-full rounded-t-[2px] transition-colors " +
|
|
127
|
+
(i === daily.length - 1 ? "bg-brand" : "bg-brand/45 group-hover:bg-brand/80")
|
|
128
|
+
}
|
|
114
129
|
style={{ height: `${Math.max(2, (d.count / max) * 100)}%` }}
|
|
115
130
|
/>
|
|
116
|
-
|
|
117
|
-
<div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 hidden -translate-x-1/2 whitespace-nowrap rounded bg-zinc-900 px-2 py-1 text-[11px] text-white group-hover:block">
|
|
131
|
+
<div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1.5 hidden -translate-x-1/2 whitespace-nowrap rounded border border-line bg-ink px-2 py-1 font-mono-ui text-[11px] text-chalk group-hover:block">
|
|
118
132
|
{fmtDay(d.date)} · {d.count}
|
|
119
133
|
</div>
|
|
120
134
|
</div>
|
|
121
135
|
))}
|
|
122
136
|
</div>
|
|
123
|
-
<div className="mt-
|
|
137
|
+
<div className="mt-3 flex justify-between border-t border-line pt-2 font-mono-ui text-[11px] text-chalk-2">
|
|
124
138
|
<span>{fmtDay(daily[0]?.date)}</span>
|
|
125
|
-
<span>
|
|
139
|
+
<span>Today</span>
|
|
126
140
|
</div>
|
|
127
|
-
</
|
|
141
|
+
</section>
|
|
128
142
|
);
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
/* ----------------------------- signup list ---------------------------- */
|
|
132
146
|
|
|
147
|
+
const PAGE = 50;
|
|
148
|
+
|
|
133
149
|
function SignupTable({ signups }: { signups: SignupRow[] }) {
|
|
134
150
|
const [q, setQ] = useState("");
|
|
151
|
+
const [shown, setShown] = useState(PAGE);
|
|
135
152
|
const filtered = useMemo(() => {
|
|
136
153
|
const needle = q.trim().toLowerCase();
|
|
137
154
|
if (!needle) return signups;
|
|
138
155
|
return signups.filter((s) => s.email.toLowerCase().includes(needle));
|
|
139
156
|
}, [q, signups]);
|
|
157
|
+
// Join order: the oldest signup is No. 1.
|
|
158
|
+
const position = useMemo(() => {
|
|
159
|
+
const m = new Map<string, number>();
|
|
160
|
+
signups.forEach((s, i) => m.set(s.id, signups.length - i));
|
|
161
|
+
return m;
|
|
162
|
+
}, [signups]);
|
|
140
163
|
|
|
141
164
|
return (
|
|
142
|
-
<
|
|
143
|
-
<div className="flex flex-col gap-3 border-b border-
|
|
144
|
-
<h2 className="text-
|
|
165
|
+
<section className="overflow-hidden rounded-lg border border-line bg-paper">
|
|
166
|
+
<div className="flex flex-col gap-3 border-b border-line p-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
|
167
|
+
<h2 className="font-display text-[17px] font-medium text-chalk">
|
|
145
168
|
Signups{" "}
|
|
146
|
-
<span className="font-normal text-
|
|
147
|
-
|
|
169
|
+
<span className="font-mono-ui text-[13px] font-normal text-chalk-2">
|
|
170
|
+
{filtered.length.toLocaleString("en-US")}
|
|
148
171
|
</span>
|
|
149
172
|
</h2>
|
|
150
173
|
<div className="flex items-center gap-2">
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
<label className="relative flex-1 sm:flex-none">
|
|
175
|
+
<Search aria-hidden className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-chalk-2" />
|
|
176
|
+
<input
|
|
177
|
+
value={q}
|
|
178
|
+
onChange={(e) => setQ(e.target.value)}
|
|
179
|
+
placeholder="Search email"
|
|
180
|
+
aria-label="Search signups"
|
|
181
|
+
className="h-9 w-full rounded-md border border-line bg-ink pl-8 pr-3 text-[13px] text-chalk outline-none transition placeholder:text-chalk-2 focus:border-brand sm:w-52"
|
|
182
|
+
/>
|
|
183
|
+
</label>
|
|
158
184
|
<button
|
|
159
185
|
type="button"
|
|
160
186
|
onClick={() => exportCsv(signups)}
|
|
161
187
|
disabled={signups.length === 0}
|
|
162
|
-
className="inline-flex h-9 items-center rounded-md border border-
|
|
188
|
+
className="inline-flex h-9 shrink-0 items-center gap-1.5 rounded-md border border-line px-3 text-[13px] font-medium text-chalk transition-colors hover:border-chalk-2 disabled:opacity-40"
|
|
163
189
|
>
|
|
190
|
+
<Download aria-hidden className="size-3.5" />
|
|
164
191
|
Export CSV
|
|
165
192
|
</button>
|
|
166
193
|
</div>
|
|
167
194
|
</div>
|
|
168
195
|
|
|
169
196
|
{filtered.length === 0 ? (
|
|
170
|
-
<p className="
|
|
197
|
+
<p className="px-5 py-12 text-center text-[14px] text-chalk-2">
|
|
171
198
|
{signups.length === 0
|
|
172
|
-
? "No signups yet.
|
|
199
|
+
? "No signups yet. They appear here as soon as someone joins."
|
|
173
200
|
: "No emails match your search."}
|
|
174
201
|
</p>
|
|
175
202
|
) : (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
<>
|
|
204
|
+
<ul className="divide-y divide-line">
|
|
205
|
+
{filtered.slice(0, shown).map((s) => (
|
|
206
|
+
<li key={s.id} className="flex items-center gap-4 px-4 py-3 sm:px-5">
|
|
207
|
+
<span className="hidden w-12 shrink-0 sm:block font-mono-ui text-[12px] tabular-nums text-chalk-2">
|
|
208
|
+
{position.get(s.id)}
|
|
209
|
+
</span>
|
|
210
|
+
<span className="min-w-0 flex-1 truncate text-[14px] text-chalk">{s.email}</span>
|
|
211
|
+
<span className="shrink-0 font-mono-ui text-[12px] text-chalk-2">
|
|
212
|
+
{fmtDateTime(s.createdAt)}
|
|
213
|
+
</span>
|
|
214
|
+
</li>
|
|
215
|
+
))}
|
|
216
|
+
</ul>
|
|
217
|
+
{filtered.length > shown ? (
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={() => setShown((n) => n + PAGE)}
|
|
221
|
+
className="w-full border-t border-line py-3 text-[13px] text-chalk-2 transition-colors hover:text-chalk"
|
|
222
|
+
>
|
|
223
|
+
Show {Math.min(PAGE, filtered.length - shown)} more
|
|
224
|
+
</button>
|
|
225
|
+
) : null}
|
|
226
|
+
</>
|
|
186
227
|
)}
|
|
187
|
-
</
|
|
228
|
+
</section>
|
|
188
229
|
);
|
|
189
230
|
}
|
|
190
231
|
|
|
191
232
|
/* --------------------------- owner-only gate -------------------------- */
|
|
192
233
|
|
|
193
234
|
// Shown to a signed-in user the `waitlistStats` function refused (not the
|
|
194
|
-
// configured owner, or no PYLON_OWNER_EMAIL set). Fails closed
|
|
235
|
+
// configured owner, or no PYLON_OWNER_EMAIL set). Fails closed: no signup data
|
|
195
236
|
// is fetched or shown.
|
|
196
237
|
function OwnerOnly({ email }: { email: string }) {
|
|
197
238
|
return (
|
|
198
|
-
<div className="rounded-
|
|
199
|
-
<h1 className="text-
|
|
200
|
-
<p className="mx-auto mt-
|
|
201
|
-
You
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">
|
|
239
|
+
<div className="rounded-lg border border-dashed border-line px-6 py-14 text-center">
|
|
240
|
+
<h1 className="font-display text-[1.5rem] font-bold text-chalk">This dashboard is owner-only</h1>
|
|
241
|
+
<p className="mx-auto mt-3 max-w-md text-[14px] leading-relaxed text-chalk-2">
|
|
242
|
+
You are signed in as <span className="text-chalk">{email || "this account"}</span>. Only
|
|
243
|
+
the owner can see signups. Set{" "}
|
|
244
|
+
<code className="rounded bg-ink-2 px-1.5 py-0.5 font-mono-ui text-[12px] text-chalk">
|
|
205
245
|
PYLON_OWNER_EMAIL={email || "you@yourbusiness.com"}
|
|
206
246
|
</code>{" "}
|
|
207
|
-
in
|
|
208
|
-
|
|
209
|
-
restart, and reload — or sign in with the owner account.
|
|
247
|
+
in <code className="rounded bg-ink-2 px-1.5 py-0.5 font-mono-ui text-[12px] text-chalk">.env</code>,
|
|
248
|
+
restart, and reload. Or sign in with the owner account.
|
|
210
249
|
</p>
|
|
211
250
|
</div>
|
|
212
251
|
);
|
|
@@ -223,20 +262,22 @@ export function UserMenu({ email }: { email: string }) {
|
|
|
223
262
|
}
|
|
224
263
|
return (
|
|
225
264
|
<details className="group relative">
|
|
226
|
-
<summary
|
|
265
|
+
<summary
|
|
266
|
+
aria-label="Account"
|
|
267
|
+
className="flex size-8 cursor-pointer select-none list-none items-center justify-center rounded-full bg-brand text-[12px] font-semibold text-white marker:hidden [&::-webkit-details-marker]:hidden"
|
|
268
|
+
>
|
|
227
269
|
{initial}
|
|
228
270
|
</summary>
|
|
229
|
-
<div className="absolute right-0 top-full z-40 mt-2 w-
|
|
230
|
-
<div className="border-b border-
|
|
231
|
-
|
|
232
|
-
{email || "Signed in"}
|
|
233
|
-
</div>
|
|
271
|
+
<div className="absolute right-0 top-full z-40 mt-2 w-60 overflow-hidden rounded-lg border border-line bg-paper py-1 shadow-[0_24px_48px_-24px_rgba(0,0,0,0.8)]">
|
|
272
|
+
<div className="truncate border-b border-line px-3 py-2.5 text-[13px] text-chalk">
|
|
273
|
+
{email || "Signed in"}
|
|
234
274
|
</div>
|
|
235
275
|
<button
|
|
236
276
|
type="button"
|
|
237
277
|
onClick={onSignOut}
|
|
238
|
-
className="flex w-full items-center px-3 py-2 text-left text-[13px] text-
|
|
278
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-[13px] text-chalk-2 transition-colors hover:bg-ink-2 hover:text-chalk"
|
|
239
279
|
>
|
|
280
|
+
<LogOut aria-hidden className="size-3.5" />
|
|
240
281
|
Sign out
|
|
241
282
|
</button>
|
|
242
283
|
</div>
|
|
@@ -248,26 +289,22 @@ export function UserMenu({ email }: { email: string }) {
|
|
|
248
289
|
|
|
249
290
|
function Skeleton() {
|
|
250
291
|
return (
|
|
251
|
-
<div className="space-y-
|
|
252
|
-
<div className="h-
|
|
292
|
+
<div className="space-y-10">
|
|
293
|
+
<div className="h-8 w-40 animate-pulse rounded bg-ink-2" />
|
|
253
294
|
<div className="grid gap-4 sm:grid-cols-3">
|
|
254
295
|
{[0, 1, 2].map((i) => (
|
|
255
|
-
<div key={i} className="h-
|
|
296
|
+
<div key={i} className="h-24 animate-pulse rounded-lg bg-ink-2" />
|
|
256
297
|
))}
|
|
257
298
|
</div>
|
|
258
|
-
<div className="h-
|
|
259
|
-
<div className="h-
|
|
299
|
+
<div className="h-56 animate-pulse rounded-lg bg-ink-2" />
|
|
300
|
+
<div className="h-72 animate-pulse rounded-lg bg-ink-2" />
|
|
260
301
|
</div>
|
|
261
302
|
);
|
|
262
303
|
}
|
|
263
304
|
|
|
264
305
|
function exportCsv(signups: SignupRow[]) {
|
|
265
|
-
const cell = (v: string) =>
|
|
266
|
-
|
|
267
|
-
const rows = [
|
|
268
|
-
"email,joined_at",
|
|
269
|
-
...signups.map((s) => `${cell(s.email)},${cell(s.createdAt)}`),
|
|
270
|
-
];
|
|
306
|
+
const cell = (v: string) => (/[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
|
|
307
|
+
const rows = ["email,joined_at", ...signups.map((s) => `${cell(s.email)},${cell(s.createdAt)}`)];
|
|
271
308
|
const blob = new Blob([rows.join("\n")], { type: "text/csv;charset=utf-8" });
|
|
272
309
|
const url = URL.createObjectURL(blob);
|
|
273
310
|
const a = document.createElement("a");
|
|
@@ -282,13 +319,13 @@ function exportCsv(signups: SignupRow[]) {
|
|
|
282
319
|
function fmtDay(iso?: string) {
|
|
283
320
|
if (!iso) return "";
|
|
284
321
|
const d = new Date(iso + "T00:00:00Z");
|
|
285
|
-
return d.toLocaleDateString(
|
|
322
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
286
323
|
}
|
|
287
324
|
|
|
288
325
|
function fmtDateTime(iso: string) {
|
|
289
326
|
const t = Date.parse(iso);
|
|
290
327
|
if (Number.isNaN(t)) return "";
|
|
291
|
-
return new Date(t).toLocaleString(
|
|
328
|
+
return new Date(t).toLocaleString("en-US", {
|
|
292
329
|
month: "short",
|
|
293
330
|
day: "numeric",
|
|
294
331
|
hour: "numeric",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { Link, type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
+
import { ArrowUpRight } from "lucide-react";
|
|
3
4
|
import { siteConfig } from "@/lib/site.config";
|
|
4
5
|
import { UserMenu, WaitlistDashboard } from "./dashboard-client";
|
|
5
6
|
|
|
@@ -35,35 +36,35 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
|
|
|
35
36
|
);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
// Dashboard chrome
|
|
39
|
-
//
|
|
39
|
+
// Dashboard chrome in the site's own look: the same near-black page, the brand
|
|
40
|
+
// name in the display face, a link back to the public site, and the account
|
|
41
|
+
// menu (a client island for sign-out).
|
|
40
42
|
function Shell({ email, children }: { email: string; children: React.ReactNode }) {
|
|
41
43
|
const { brand } = siteConfig;
|
|
42
44
|
return (
|
|
43
|
-
<div className="flex min-h-screen flex-col bg-
|
|
44
|
-
<header className="border-b border-
|
|
45
|
-
<div className="mx-auto flex h-
|
|
46
|
-
<div className="flex items-
|
|
47
|
-
<
|
|
48
|
-
{brand.
|
|
49
|
-
</
|
|
50
|
-
<span className="text-[
|
|
51
|
-
{brand.name} <span className="text-zinc-400">/ waitlist</span>
|
|
52
|
-
</span>
|
|
45
|
+
<div className="flex min-h-screen flex-col bg-ink text-chalk">
|
|
46
|
+
<header className="border-b border-line">
|
|
47
|
+
<div className="mx-auto flex h-16 w-full max-w-5xl items-center justify-between px-6">
|
|
48
|
+
<div className="flex items-baseline gap-2.5">
|
|
49
|
+
<Link href="/" className="font-display text-[17px] font-bold tracking-tight text-chalk">
|
|
50
|
+
{brand.name}
|
|
51
|
+
</Link>
|
|
52
|
+
<span className="font-mono-ui text-[12px] text-chalk-2">owner</span>
|
|
53
53
|
</div>
|
|
54
|
-
<div className="flex items-center gap-
|
|
54
|
+
<div className="flex items-center gap-5">
|
|
55
55
|
<Link
|
|
56
56
|
href="/"
|
|
57
|
-
className="text-[13px] text-
|
|
57
|
+
className="inline-flex items-center gap-1 text-[13px] text-chalk-2 transition-colors hover:text-chalk"
|
|
58
58
|
>
|
|
59
|
-
View site
|
|
59
|
+
View site
|
|
60
|
+
<ArrowUpRight aria-hidden className="size-3.5" />
|
|
60
61
|
</Link>
|
|
61
62
|
<UserMenu email={email} />
|
|
62
63
|
</div>
|
|
63
64
|
</div>
|
|
64
65
|
</header>
|
|
65
66
|
<main className="flex-1">
|
|
66
|
-
<div className="mx-auto max-w-
|
|
67
|
+
<div className="mx-auto w-full max-w-5xl px-6 py-10 sm:py-12">{children}</div>
|
|
67
68
|
</main>
|
|
68
69
|
</div>
|
|
69
70
|
);
|
|
@@ -18,21 +18,21 @@ export default function LoginPage({ auth, response }: PageProps) {
|
|
|
18
18
|
if (auth.user_id && !auth.user_id.startsWith("guest_")) response.redirect("/dashboard");
|
|
19
19
|
const { brand } = siteConfig;
|
|
20
20
|
return (
|
|
21
|
-
<div className="flex min-h-screen
|
|
22
|
-
<div className="w-full max-w-
|
|
23
|
-
<Link href="/" className="
|
|
24
|
-
|
|
25
|
-
{brand.letter}
|
|
26
|
-
</span>
|
|
21
|
+
<div className="flex min-h-screen flex-col bg-ink px-6 text-chalk">
|
|
22
|
+
<div className="mx-auto flex h-16 w-full max-w-5xl items-center">
|
|
23
|
+
<Link href="/" className="font-display text-[17px] font-bold tracking-tight text-chalk">
|
|
24
|
+
{brand.name}
|
|
27
25
|
</Link>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<
|
|
26
|
+
</div>
|
|
27
|
+
<div className="flex flex-1 items-center justify-center pb-24">
|
|
28
|
+
<div className="w-full max-w-[380px]">
|
|
29
|
+
<h1 className="font-display text-[1.75rem] font-bold leading-tight tracking-[-0.02em]">
|
|
30
|
+
Sign in to the waitlist
|
|
31
|
+
</h1>
|
|
32
|
+
<p className="mt-2 text-[14px] text-chalk-2">Owner access to signups and the export.</p>
|
|
33
|
+
<div className="mt-8 rounded-lg border border-line bg-paper p-6">
|
|
34
|
+
<AuthForm />
|
|
35
|
+
</div>
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
@@ -44,8 +44,8 @@ const Signup = entity(
|
|
|
44
44
|
);
|
|
45
45
|
|
|
46
46
|
// A single-row, PII-FREE aggregate the public page can safely read live. It
|
|
47
|
-
// holds only the signup count — no emails.
|
|
48
|
-
//
|
|
47
|
+
// holds only the signup count — no emails. Every Signup writer recounts into
|
|
48
|
+
// it (lib/stats.ts → syncWaitlistStat). The landing page subscribes with
|
|
49
49
|
// `db.useQuery("WaitlistStat")`, which syncs across every open tab through the
|
|
50
50
|
// replica — so the counter ticks up everywhere the instant someone joins. This
|
|
51
51
|
// is the cross-tab-safe realtime primitive (entity sync), not a per-connection
|
|
@@ -81,9 +81,9 @@ const User = entity(
|
|
|
81
81
|
// PRIVACY — the heart of the spec. Signup holds visitor emails, so it denies
|
|
82
82
|
// EVERY client read and write. No `db.useQuery("Signup")` can ever pull a row,
|
|
83
83
|
// and no client can insert/update/delete directly. Writes happen only inside
|
|
84
|
-
// the server-side `joinWaitlist`
|
|
85
|
-
// happen only inside
|
|
86
|
-
//
|
|
84
|
+
// the server-side `joinWaitlist` and dev-only `seedDemo` mutations (functions
|
|
85
|
+
// bypass policies); reads happen only inside the owner-gated `waitlistStats`.
|
|
86
|
+
// The public page sees the count through WaitlistStat. A marketing site must never leak its own
|
|
87
87
|
// customers' emails — this policy is what guarantees it.
|
|
88
88
|
const signupPolicy = policy({
|
|
89
89
|
name: "signup_private",
|
|
@@ -96,7 +96,7 @@ const signupPolicy = policy({
|
|
|
96
96
|
|
|
97
97
|
// The aggregate count is public to READ (it's just a number — the whole point
|
|
98
98
|
// is that the landing page shows it live to everyone). Clients can't WRITE it;
|
|
99
|
-
// only the
|
|
99
|
+
// only the Signup writers maintain it server-side.
|
|
100
100
|
const waitlistStatPolicy = policy({
|
|
101
101
|
name: "waitlist_stat_public_read",
|
|
102
102
|
entity: "WaitlistStat",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { syncWaitlistStat } from "../lib/stats";
|
|
2
3
|
|
|
3
4
|
// Normalize + sanity-check an email without pulling in a dependency. This is a
|
|
4
5
|
// pragmatic "looks like an email" check, not RFC 5322 — the unique index is the
|
|
@@ -14,9 +15,8 @@ function normalizeEmail(raw: string): string | null {
|
|
|
14
15
|
|
|
15
16
|
// joinWaitlist — the ONLY way a Signup row is ever written. It's a `mutation`
|
|
16
17
|
// (not an `action`): mutations get `ctx.db` access and run as one atomic
|
|
17
|
-
// transaction
|
|
18
|
-
//
|
|
19
|
-
// up in realtime.
|
|
18
|
+
// transaction. It also rewrites the public WaitlistStat row, and that change
|
|
19
|
+
// syncs to every open tab, which is what makes the counter tick up live.
|
|
20
20
|
//
|
|
21
21
|
// `auth: "public"` because a landing-page visitor has no account. Public
|
|
22
22
|
// mutations are still rate-limited at the HTTP layer in production (the
|
|
@@ -61,21 +61,9 @@ export default mutation<{ email: string }, { ok: boolean; alreadyJoined: boolean
|
|
|
61
61
|
throw e;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Keep the public, PII-free WaitlistStat
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
// the recount-then-write is consistent. The landing page reads this row via
|
|
68
|
-
// `db.useQuery`, which syncs the new value to every open tab.
|
|
69
|
-
const total = (await ctx.db.unsafe.list("Signup")).length;
|
|
70
|
-
const stat = (await ctx.db.unsafe.list("WaitlistStat"))[0] as
|
|
71
|
-
| { id: string }
|
|
72
|
-
| undefined;
|
|
73
|
-
const now = new Date().toISOString();
|
|
74
|
-
if (stat) {
|
|
75
|
-
await ctx.db.unsafe.update("WaitlistStat", stat.id, { count: total, updatedAt: now });
|
|
76
|
-
} else {
|
|
77
|
-
await ctx.db.unsafe.insert("WaitlistStat", { count: total, updatedAt: now });
|
|
78
|
-
}
|
|
64
|
+
// Keep the public, PII-free WaitlistStat row equal to the Signup count.
|
|
65
|
+
// It is a recount, not +1, so the number cannot drift from the table.
|
|
66
|
+
await syncWaitlistStat(ctx.db.unsafe);
|
|
79
67
|
|
|
80
68
|
return { ok: true, alreadyJoined: false };
|
|
81
69
|
},
|