@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,188 @@
|
|
|
1
|
+
// Pure chat helpers: names, colors, grouping, and labels. No Pylon imports,
|
|
2
|
+
// so tests/chat.test.ts runs them without a server and the server functions
|
|
3
|
+
// and the client share one copy.
|
|
4
|
+
|
|
5
|
+
export interface ChatMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
channelId: string;
|
|
8
|
+
authorId: string;
|
|
9
|
+
text: string;
|
|
10
|
+
/** Absent on an optimistic row until the server stamps it. */
|
|
11
|
+
createdAt?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatMember {
|
|
15
|
+
id: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
hue: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MAX_MESSAGE_LENGTH = 4000;
|
|
22
|
+
export const MAX_NAME_LENGTH = 32;
|
|
23
|
+
|
|
24
|
+
/** Messages from one author closer together than this share one header. */
|
|
25
|
+
export const GROUP_GAP_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
const ADJECTIVES = [
|
|
28
|
+
"Amber", "Brisk", "Cobalt", "Dusky", "Ember", "Fern", "Gilded", "Hazel",
|
|
29
|
+
"Indigo", "Juniper", "Kestrel", "Lunar", "Maple", "Nimble", "Ochre", "Pewter",
|
|
30
|
+
"Quiet", "Russet", "Saffron", "Tidal", "Umber", "Velvet", "Willow", "Zephyr",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const ANIMALS = [
|
|
34
|
+
"Otter", "Heron", "Lynx", "Finch", "Marten", "Ibis", "Badger", "Wren",
|
|
35
|
+
"Puffin", "Stoat", "Crane", "Hare", "Magpie", "Newt", "Osprey", "Plover",
|
|
36
|
+
"Raven", "Seal", "Tern", "Vole", "Wolf", "Yak", "Gecko", "Moth",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** FNV-1a: a small, stable string hash. The same id always maps to the same name. */
|
|
40
|
+
export function hashString(input: string): number {
|
|
41
|
+
let h = 0x811c9dc5;
|
|
42
|
+
for (let i = 0; i < input.length; i++) {
|
|
43
|
+
h ^= input.charCodeAt(i);
|
|
44
|
+
h = Math.imul(h, 0x01000193);
|
|
45
|
+
}
|
|
46
|
+
return h >>> 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** "Amber Otter" style display name for a guest, derived from the user id. */
|
|
50
|
+
export function guestName(userId: string): string {
|
|
51
|
+
const h = hashString(userId);
|
|
52
|
+
const adjective = ADJECTIVES[h % ADJECTIVES.length];
|
|
53
|
+
const animal = ANIMALS[Math.floor(h / ADJECTIVES.length) % ANIMALS.length];
|
|
54
|
+
return `${adjective} ${animal}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Avatar hue (0–359) derived from the user id. */
|
|
58
|
+
export function guestHue(userId: string): number {
|
|
59
|
+
return hashString(`hue:${userId}`) % 360;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Trims, collapses whitespace, and validates a display name. Returns null when invalid. */
|
|
63
|
+
export function cleanName(raw: string): string | null {
|
|
64
|
+
// \p{Cf} covers zero-width and bidi control characters, which could make a
|
|
65
|
+
// name invisible or reorder the text around it.
|
|
66
|
+
const name = raw.replace(/\p{Cf}/gu, "").replace(/\s+/g, " ").trim();
|
|
67
|
+
if (name.length === 0 || name.length > MAX_NAME_LENGTH) return null;
|
|
68
|
+
return name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Up to two initials: "Maya Chen" → "MC", "theo" → "T". */
|
|
72
|
+
export function initials(name: string): string {
|
|
73
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
74
|
+
if (parts.length === 0) return "?";
|
|
75
|
+
// Array.from splits by code point, so an emoji stays whole.
|
|
76
|
+
const first = Array.from(parts[0])[0] ?? "";
|
|
77
|
+
const last = parts.length > 1 ? (Array.from(parts[parts.length - 1])[0] ?? "") : "";
|
|
78
|
+
return (first + last).toUpperCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Chronological order. Rows the server has not stamped yet sort last. Equal
|
|
83
|
+
* timestamps (Postgres stores whole seconds) fall back to the id, which is
|
|
84
|
+
* time-ordered, so every client shows the same order.
|
|
85
|
+
*/
|
|
86
|
+
export function sortMessages<T extends ChatMessage>(messages: readonly T[]): T[] {
|
|
87
|
+
return [...messages].sort((a, b) => {
|
|
88
|
+
if (!a.createdAt && !b.createdAt) return 0;
|
|
89
|
+
if (!a.createdAt) return 1;
|
|
90
|
+
if (!b.createdAt) return -1;
|
|
91
|
+
return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface MessageGroup<T extends ChatMessage = ChatMessage> {
|
|
96
|
+
key: string;
|
|
97
|
+
authorId: string;
|
|
98
|
+
/** Local-day key ("2026-09-26") of the first message; null while pending. */
|
|
99
|
+
day: string | null;
|
|
100
|
+
messages: T[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Local calendar day of an ISO timestamp, as "YYYY-MM-DD". */
|
|
104
|
+
export function dayKey(iso: string): string {
|
|
105
|
+
const d = new Date(iso);
|
|
106
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
107
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
108
|
+
return `${d.getFullYear()}-${m}-${day}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Groups sorted messages into runs by one author. A new group starts when the
|
|
113
|
+
* author changes, the local day changes, or the gap exceeds GROUP_GAP_MS. A
|
|
114
|
+
* pending row (no createdAt) joins the previous group when the author matches.
|
|
115
|
+
*/
|
|
116
|
+
export function groupMessages<T extends ChatMessage>(
|
|
117
|
+
sorted: readonly T[],
|
|
118
|
+
gapMs: number = GROUP_GAP_MS,
|
|
119
|
+
): MessageGroup<T>[] {
|
|
120
|
+
const groups: MessageGroup<T>[] = [];
|
|
121
|
+
let lastTime: number | null = null;
|
|
122
|
+
for (const m of sorted) {
|
|
123
|
+
const prev = groups[groups.length - 1];
|
|
124
|
+
const time = m.createdAt ? Date.parse(m.createdAt) : null;
|
|
125
|
+
const day = m.createdAt ? dayKey(m.createdAt) : null;
|
|
126
|
+
const sameAuthor = prev?.authorId === m.authorId;
|
|
127
|
+
const sameDay = day === null || prev?.day === null || prev?.day === day;
|
|
128
|
+
const closeInTime = time === null || lastTime === null || time - lastTime <= gapMs;
|
|
129
|
+
if (prev && sameAuthor && sameDay && closeInTime) {
|
|
130
|
+
prev.messages.push(m);
|
|
131
|
+
} else {
|
|
132
|
+
groups.push({ key: m.id, authorId: m.authorId, day, messages: [m] });
|
|
133
|
+
}
|
|
134
|
+
if (time !== null) lastTime = time;
|
|
135
|
+
}
|
|
136
|
+
return groups;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** "Today", "Yesterday", "Wednesday", or "Monday, September 21". */
|
|
140
|
+
export function dayLabel(iso: string, now: Date = new Date()): string {
|
|
141
|
+
const d = new Date(iso);
|
|
142
|
+
const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
143
|
+
const diffDays = Math.round((startOf(now) - startOf(d)) / 86_400_000);
|
|
144
|
+
if (diffDays === 0) return "Today";
|
|
145
|
+
if (diffDays === 1) return "Yesterday";
|
|
146
|
+
if (diffDays > 1 && diffDays < 7) {
|
|
147
|
+
return d.toLocaleDateString("en-US", { weekday: "long" });
|
|
148
|
+
}
|
|
149
|
+
return d.toLocaleDateString("en-US", {
|
|
150
|
+
weekday: "long",
|
|
151
|
+
month: "long",
|
|
152
|
+
day: "numeric",
|
|
153
|
+
...(d.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** "9:41 AM" in the viewer's locale. */
|
|
158
|
+
export function timeLabel(iso: string): string {
|
|
159
|
+
return new Date(iso).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** "Maya is typing", "Maya and Theo are typing", "Maya, Theo, and 2 others are typing". */
|
|
163
|
+
export function typingLabel(names: readonly string[]): string {
|
|
164
|
+
if (names.length === 0) return "";
|
|
165
|
+
if (names.length === 1) return `${names[0]} is typing`;
|
|
166
|
+
if (names.length === 2) return `${names[0]} and ${names[1]} are typing`;
|
|
167
|
+
if (names.length === 3) return `${names[0]}, ${names[1]}, and ${names[2]} are typing`;
|
|
168
|
+
return `${names[0]}, ${names[1]}, and ${names.length - 2} others are typing`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Messages from other people in a channel, newer than `lastSeen`. A channel the
|
|
173
|
+
* viewer has no mark for counts as read, so a first visit shows no badges.
|
|
174
|
+
*/
|
|
175
|
+
export function unreadCount(
|
|
176
|
+
messages: readonly ChatMessage[],
|
|
177
|
+
channelId: string,
|
|
178
|
+
lastSeen: string | undefined,
|
|
179
|
+
selfId: string,
|
|
180
|
+
): number {
|
|
181
|
+
if (!lastSeen) return 0;
|
|
182
|
+
let n = 0;
|
|
183
|
+
for (const m of messages) {
|
|
184
|
+
if (m.channelId !== channelId || m.authorId === selfId || !m.createdAt) continue;
|
|
185
|
+
if (m.createdAt > lastSeen) n++;
|
|
186
|
+
}
|
|
187
|
+
return n;
|
|
188
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Workspace loaded on first boot by functions/joinChat.ts. The channels are
|
|
2
|
+
// always created. With `includeDemoConversation` on, four demo people (a team
|
|
3
|
+
// shipping an offline-maps update for a trail-running app) and their messages
|
|
4
|
+
// are added too. Set it to false to start with empty channels.
|
|
5
|
+
//
|
|
6
|
+
// `minutesAgo` is relative to the moment the seed runs, so the conversation
|
|
7
|
+
// always reads as yesterday plus this morning.
|
|
8
|
+
|
|
9
|
+
export const includeDemoConversation = true;
|
|
10
|
+
|
|
11
|
+
export interface SeedChannel {
|
|
12
|
+
slug: string;
|
|
13
|
+
name: string;
|
|
14
|
+
topic: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SeedPerson {
|
|
18
|
+
/** Stable author id for seeded rows. Real users have session ids instead. */
|
|
19
|
+
userId: string;
|
|
20
|
+
name: string;
|
|
21
|
+
hue: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SeedMessage {
|
|
25
|
+
channel: string;
|
|
26
|
+
author: string;
|
|
27
|
+
minutesAgo: number;
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const seedChannels: SeedChannel[] = [
|
|
32
|
+
{ slug: "general", name: "general", topic: "Anything that doesn't fit elsewhere" },
|
|
33
|
+
{ slug: "launch", name: "launch", topic: "Ridgeline 2.4 ships Thursday at 10:00" },
|
|
34
|
+
{ slug: "design", name: "design", topic: "Screens, icons, and App Store art" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const seedPeople: SeedPerson[] = [
|
|
38
|
+
{ userId: "seed_maya", name: "Maya Chen", hue: 28 },
|
|
39
|
+
{ userId: "seed_theo", name: "Theo Okafor", hue: 250 },
|
|
40
|
+
{ userId: "seed_priya", name: "Priya Raman", hue: 150 },
|
|
41
|
+
{ userId: "seed_jonas", name: "Jonas Weber", hue: 330 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const DAY = 24 * 60;
|
|
45
|
+
|
|
46
|
+
export const seedMessages: SeedMessage[] = [
|
|
47
|
+
// #launch, yesterday afternoon
|
|
48
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: DAY + 190, text: "Status check for 2.4. We still have three open items: the iPad crash, the App Store screenshots, and the release notes." },
|
|
49
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: DAY + 186, text: "The iPad crash is in the tile cache. When you download a region bigger than about 2 GB, the eviction pass runs on the main thread and the watchdog kills the app." },
|
|
50
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: DAY + 185, text: "Moving eviction to a background queue now. Should have a build tonight." },
|
|
51
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: DAY + 171, text: "I can reproduce it every time on the 10th gen iPad with the Cascades region. Happy to run it again once the build is up." },
|
|
52
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: DAY + 168, text: "Great. If it slips past Wednesday noon we move the release to next week. I'd rather not ship offline maps that crash offline." },
|
|
53
|
+
{ channel: "launch", author: "seed_theo", minutesAgo: DAY + 122, text: "Screenshots are 80% done. Waiting on a clean capture of the elevation profile with the new colors." },
|
|
54
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: DAY + 34, text: "TestFlight build 412 is up. Eviction runs in the background, and I added a progress bar so a big cleanup doesn't look frozen." },
|
|
55
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: DAY + 21, text: "Installing now." },
|
|
56
|
+
|
|
57
|
+
// #launch, this morning
|
|
58
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: 58, text: "Build 412 survived six full downloads of the Cascades region on the iPad. No crash, memory stays under 600 MB." },
|
|
59
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: 57, text: "One small thing: the progress bar says 100% for about four seconds before it disappears." },
|
|
60
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: 49, text: "That's the index rebuild after the last tile lands. I'll change the label to \"Finishing up\" for that step instead of hiding it." },
|
|
61
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: 44, text: "Perfect. That takes the crash off the list 🎉" },
|
|
62
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: 43, text: "Remaining: screenshots (Theo) and release notes (me). Submitting for review Wednesday at 3." },
|
|
63
|
+
{ channel: "launch", author: "seed_theo", minutesAgo: 12, text: "Screenshots are uploaded to the shared folder: 6.9\" and 13\" iPad, light and dark. Can someone check the Spanish captions?" },
|
|
64
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: 6, text: "On it after standup." },
|
|
65
|
+
|
|
66
|
+
// #design
|
|
67
|
+
{ channel: "design", author: "seed_theo", minutesAgo: DAY + 240, text: "New elevation colors: climbs go from sand to rust, descents from sky to deep blue. The old green-to-red read like a heart rate chart." },
|
|
68
|
+
{ channel: "design", author: "seed_maya", minutesAgo: DAY + 231, text: "Much easier to read in sunlight. Is the rust still readable on the dark map style?" },
|
|
69
|
+
{ channel: "design", author: "seed_theo", minutesAgo: DAY + 226, text: "I bumped its lightness 8% in dark mode. Checked it on a phone outside at noon." },
|
|
70
|
+
{ channel: "design", author: "seed_theo", minutesAgo: 95, text: "Offline badge is final: a small filled cloud with a slash, bottom-left of the region card. No more \"Saved\" text label." },
|
|
71
|
+
{ channel: "design", author: "seed_jonas", minutesAgo: 88, text: "Support will like that. Half the tickets last month were people not knowing a region was already downloaded." },
|
|
72
|
+
|
|
73
|
+
// #general
|
|
74
|
+
{ channel: "general", author: "seed_priya", minutesAgo: DAY + 300, text: "Welcome to the team chat. #launch is for the 2.4 release, #design for screens and art." },
|
|
75
|
+
{ channel: "general", author: "seed_jonas", minutesAgo: DAY + 75, text: "Trail run Saturday at 8 at the Mill Creek trailhead. 12 km, easy pace, coffee after." },
|
|
76
|
+
{ channel: "general", author: "seed_theo", minutesAgo: DAY + 70, text: "In, as long as easy pace means easy pace." },
|
|
77
|
+
{ channel: "general", author: "seed_maya", minutesAgo: 30, text: "Standup moved to 10:30 today, Priya is on a call with the App Review team." },
|
|
78
|
+
];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
cleanName,
|
|
4
|
+
dayLabel,
|
|
5
|
+
groupMessages,
|
|
6
|
+
guestHue,
|
|
7
|
+
guestName,
|
|
8
|
+
initials,
|
|
9
|
+
sortMessages,
|
|
10
|
+
typingLabel,
|
|
11
|
+
unreadCount,
|
|
12
|
+
type ChatMessage,
|
|
13
|
+
} from "../lib/chat";
|
|
14
|
+
import { seedChannels, seedMessages, seedPeople } from "../lib/seed";
|
|
15
|
+
|
|
16
|
+
const at = (iso: string, authorId = "a", id = iso, channelId = "c1"): ChatMessage => ({
|
|
17
|
+
id,
|
|
18
|
+
channelId,
|
|
19
|
+
authorId,
|
|
20
|
+
text: "x",
|
|
21
|
+
createdAt: iso,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("guest identity", () => {
|
|
25
|
+
test("the same id always gets the same name and hue", () => {
|
|
26
|
+
expect(guestName("guest_abc")).toBe(guestName("guest_abc"));
|
|
27
|
+
expect(guestHue("guest_abc")).toBe(guestHue("guest_abc"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("names are two words and hues are angles", () => {
|
|
31
|
+
for (const id of ["guest_1", "guest_2", "guest_ffff", "u_42"]) {
|
|
32
|
+
expect(guestName(id)).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/);
|
|
33
|
+
const hue = guestHue(id);
|
|
34
|
+
expect(hue).toBeGreaterThanOrEqual(0);
|
|
35
|
+
expect(hue).toBeLessThan(360);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("cleanName", () => {
|
|
41
|
+
test("trims and collapses whitespace", () => {
|
|
42
|
+
expect(cleanName(" Maya Chen ")).toBe("Maya Chen");
|
|
43
|
+
});
|
|
44
|
+
test("removes zero-width and bidi control characters", () => {
|
|
45
|
+
expect(cleanName("Ma\u200bya\u202e")).toBe("Maya");
|
|
46
|
+
expect(cleanName("\u200b\u200b")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
test("rejects empty and over-long names", () => {
|
|
49
|
+
expect(cleanName(" ")).toBeNull();
|
|
50
|
+
expect(cleanName("x".repeat(33))).toBeNull();
|
|
51
|
+
expect(cleanName("x".repeat(32))).toBe("x".repeat(32));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("initials", () => {
|
|
56
|
+
expect(initials("Maya Chen")).toBe("MC");
|
|
57
|
+
expect(initials("theo")).toBe("T");
|
|
58
|
+
expect(initials("Priya de la Raman")).toBe("PR");
|
|
59
|
+
expect(initials(" ")).toBe("?");
|
|
60
|
+
expect(initials("🎉 Party")).toBe("🎉P");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("sortMessages", () => {
|
|
64
|
+
test("orders by time and puts unsent rows last", () => {
|
|
65
|
+
const pending: ChatMessage = { id: "p", channelId: "c1", authorId: "a", text: "x" };
|
|
66
|
+
const sorted = sortMessages([at("2026-09-26T10:05:00Z"), pending, at("2026-09-26T10:00:00Z")]);
|
|
67
|
+
expect(sorted.map((m) => m.id)).toEqual(["2026-09-26T10:00:00Z", "2026-09-26T10:05:00Z", "p"]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("equal timestamps fall back to id order", () => {
|
|
72
|
+
const sorted = sortMessages([
|
|
73
|
+
at("2026-09-26T10:00:00Z", "a", "b-id"),
|
|
74
|
+
at("2026-09-26T10:00:00Z", "a", "a-id"),
|
|
75
|
+
]);
|
|
76
|
+
expect(sorted.map((m) => m.id)).toEqual(["a-id", "b-id"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("groupMessages", () => {
|
|
80
|
+
test("joins consecutive messages from one author within five minutes", () => {
|
|
81
|
+
const groups = groupMessages([
|
|
82
|
+
at("2026-09-26T10:00:00Z", "a"),
|
|
83
|
+
at("2026-09-26T10:03:00Z", "a"),
|
|
84
|
+
at("2026-09-26T10:04:00Z", "b"),
|
|
85
|
+
at("2026-09-26T10:20:00Z", "b"),
|
|
86
|
+
]);
|
|
87
|
+
expect(groups.map((g) => [g.authorId, g.messages.length])).toEqual([
|
|
88
|
+
["a", 2],
|
|
89
|
+
["b", 1],
|
|
90
|
+
["b", 1],
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("starts a new group on a new day", () => {
|
|
95
|
+
const groups = groupMessages([
|
|
96
|
+
at(new Date(2026, 8, 25, 23, 58).toISOString(), "a"),
|
|
97
|
+
at(new Date(2026, 8, 26, 0, 1).toISOString(), "a"),
|
|
98
|
+
]);
|
|
99
|
+
expect(groups).toHaveLength(2);
|
|
100
|
+
expect(groups[0].day).not.toBe(groups[1].day);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a pending row joins the sender's last group", () => {
|
|
104
|
+
const pending: ChatMessage = { id: "p", channelId: "c1", authorId: "a", text: "x", createdAt: null };
|
|
105
|
+
const groups = groupMessages([at("2026-09-26T10:00:00Z", "a"), pending]);
|
|
106
|
+
expect(groups).toHaveLength(1);
|
|
107
|
+
expect(groups[0].messages.map((m) => m.id)).toEqual(["2026-09-26T10:00:00Z", "p"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("dayLabel", () => {
|
|
112
|
+
const now = new Date(2026, 8, 26, 12, 0);
|
|
113
|
+
test("today and yesterday", () => {
|
|
114
|
+
expect(dayLabel(new Date(2026, 8, 26, 8, 0).toISOString(), now)).toBe("Today");
|
|
115
|
+
expect(dayLabel(new Date(2026, 8, 25, 23, 0).toISOString(), now)).toBe("Yesterday");
|
|
116
|
+
});
|
|
117
|
+
test("weekday within a week, full date after", () => {
|
|
118
|
+
expect(dayLabel(new Date(2026, 8, 22, 9, 0).toISOString(), now)).toBe("Tuesday");
|
|
119
|
+
expect(dayLabel(new Date(2026, 8, 14, 9, 0).toISOString(), now)).toBe("Monday, September 14");
|
|
120
|
+
expect(dayLabel(new Date(2025, 8, 14, 9, 0).toISOString(), now)).toBe("Sunday, September 14, 2025");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("typingLabel", () => {
|
|
125
|
+
expect(typingLabel([])).toBe("");
|
|
126
|
+
expect(typingLabel(["Maya"])).toBe("Maya is typing");
|
|
127
|
+
expect(typingLabel(["Maya", "Theo"])).toBe("Maya and Theo are typing");
|
|
128
|
+
expect(typingLabel(["Maya", "Theo", "Priya"])).toBe("Maya, Theo, and Priya are typing");
|
|
129
|
+
expect(typingLabel(["Maya", "Theo", "Priya", "Jonas"])).toBe("Maya, Theo, and 2 others are typing");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("unreadCount", () => {
|
|
133
|
+
const msgs = [
|
|
134
|
+
at("2026-09-26T10:00:00Z", "other", "1"),
|
|
135
|
+
at("2026-09-26T10:05:00Z", "other", "2"),
|
|
136
|
+
at("2026-09-26T10:06:00Z", "me", "3"),
|
|
137
|
+
at("2026-09-26T10:07:00Z", "other", "4", "c2"),
|
|
138
|
+
];
|
|
139
|
+
test("counts other people's messages after the mark in that channel", () => {
|
|
140
|
+
expect(unreadCount(msgs, "c1", "2026-09-26T10:01:00Z", "me")).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
test("a channel without a mark counts as read", () => {
|
|
143
|
+
expect(unreadCount(msgs, "c1", undefined, "me")).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("seed", () => {
|
|
148
|
+
test("every seeded message points at a seeded channel and person", () => {
|
|
149
|
+
const channels = new Set(seedChannels.map((c) => c.slug));
|
|
150
|
+
const people = new Set(seedPeople.map((p) => p.userId));
|
|
151
|
+
for (const m of seedMessages) {
|
|
152
|
+
expect(channels.has(m.channel)).toBe(true);
|
|
153
|
+
expect(people.has(m.author)).toBe(true);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
test("seed ids cannot collide with session user ids", () => {
|
|
157
|
+
for (const p of seedPeople) expect(p.userId.startsWith("seed_")).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
|
+
// This project uses the classic JSX transform, so .tsx tests import React.
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
5
|
+
import { Composer } from "../components/chat/composer";
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
function setup() {
|
|
10
|
+
const onSend = mock((_text: string) => {});
|
|
11
|
+
const onTyping = mock(() => {});
|
|
12
|
+
const onStopTyping = mock(() => {});
|
|
13
|
+
render(
|
|
14
|
+
<Composer
|
|
15
|
+
channelName="launch"
|
|
16
|
+
channelKey="c1"
|
|
17
|
+
onSend={onSend}
|
|
18
|
+
onTyping={onTyping}
|
|
19
|
+
onStopTyping={onStopTyping}
|
|
20
|
+
/>,
|
|
21
|
+
);
|
|
22
|
+
const field = screen.getByPlaceholderText("Message #launch") as HTMLTextAreaElement;
|
|
23
|
+
return { field, onSend, onTyping, onStopTyping };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("Enter sends the trimmed text and clears the field", () => {
|
|
27
|
+
const { field, onSend, onStopTyping } = setup();
|
|
28
|
+
fireEvent.change(field, { target: { value: " ship it " } });
|
|
29
|
+
fireEvent.keyDown(field, { key: "Enter" });
|
|
30
|
+
expect(onSend).toHaveBeenCalledWith("ship it");
|
|
31
|
+
expect(field.value).toBe("");
|
|
32
|
+
expect(onStopTyping).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Shift+Enter does not send", () => {
|
|
36
|
+
const { field, onSend } = setup();
|
|
37
|
+
fireEvent.change(field, { target: { value: "line one" } });
|
|
38
|
+
fireEvent.keyDown(field, { key: "Enter", shiftKey: true });
|
|
39
|
+
expect(onSend).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("whitespace-only text does not send and the button stays disabled", () => {
|
|
43
|
+
const { field, onSend } = setup();
|
|
44
|
+
fireEvent.change(field, { target: { value: " " } });
|
|
45
|
+
fireEvent.keyDown(field, { key: "Enter" });
|
|
46
|
+
expect(onSend).not.toHaveBeenCalled();
|
|
47
|
+
expect((screen.getByRole("button", { name: "Send message" }) as HTMLButtonElement).disabled).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("typing reports activity", () => {
|
|
51
|
+
const { field, onTyping } = setup();
|
|
52
|
+
fireEvent.change(field, { target: { value: "h" } });
|
|
53
|
+
expect(onTyping).toHaveBeenCalled();
|
|
54
|
+
});
|
|
@@ -10,7 +10,7 @@ saved posts. Likes, comments, and follows update live in every open tab.
|
|
|
10
10
|
__RUN_DEV__
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Open http://localhost:4321. The first visit loads
|
|
13
|
+
Open http://localhost:4321. The first visit loads eight demo people with
|
|
14
14
|
their posts, likes, comments, and follows, and gives you a guest profile.
|
|
15
15
|
Post a photo with Create, like with a double tap, and open a second tab to
|
|
16
16
|
watch counts change.
|
|
@@ -30,7 +30,7 @@ lib/seed.ts the demo people and posts
|
|
|
30
30
|
lib/site.ts the app's name
|
|
31
31
|
functions/ ensureProfile, updateProfile, createPost,
|
|
32
32
|
addComment, seedFeed
|
|
33
|
-
public/images/
|
|
33
|
+
public/images/posts/ demo photos
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## How it works
|
|
@@ -55,7 +55,7 @@ public/images/ demo avatars and photos
|
|
|
55
55
|
move a guest's profile to the account on sign-in.
|
|
56
56
|
- **Scale:** `components/social/use-social.ts` reads every row, which is fine
|
|
57
57
|
for a demo. For a large network, query per screen with `where` and `limit`.
|
|
58
|
-
- **Launch:** delete `functions/seedFeed.ts`, `lib/seed.ts`, and
|
|
58
|
+
- **Launch:** delete `functions/seedFeed.ts`, `lib/seed.ts`, `tests/seed.test.ts`, and
|
|
59
59
|
`public/images/`, and remove the `seedFeed` call in `session.tsx`.
|
|
60
60
|
|
|
61
61
|
## Deploy
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
on <html>. */
|
|
17
17
|
:root {
|
|
18
18
|
--radius: 0.625rem;
|
|
19
|
-
/* The app's own colors: links and the follow button, and a liked heart.
|
|
20
|
-
|
|
21
|
-
--
|
|
19
|
+
/* The app's own colors: links and the follow button, and a liked heart.
|
|
20
|
+
They match the iOS app in apps/ios. */
|
|
21
|
+
--brand: #0095f6;
|
|
22
|
+
--like: #ff3040;
|
|
23
|
+
/* The ring around someone who posted in the last 24 hours. */
|
|
24
|
+
--story-ring: conic-gradient(from 180deg, #fcba45, #fa6133, #de2678, #8c38d9, #fcba45);
|
|
22
25
|
--background: oklch(1 0 0);
|
|
23
26
|
--foreground: oklch(0.141 0.005 285.823);
|
|
24
27
|
--card: oklch(1 0 0);
|
|
@@ -147,6 +150,10 @@
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
/* The wordmark. Instrument Serif is declared in app.ts. */
|
|
153
|
+
.bg-story-ring {
|
|
154
|
+
background: var(--story-ring);
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
.font-display {
|
|
151
158
|
font-family: var(--font-display, "Iowan Old Style", Georgia, serif);
|
|
152
159
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#fcba45"/><stop offset=".35" stop-color="#fa6133"/><stop offset=".7" stop-color="#de2678"/><stop offset="1" stop-color="#8c38d9"/></linearGradient></defs><rect width="64" height="64" rx="14" fill="url(#g)"/><text x="32" y="45" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-style="italic" font-weight="700" font-size="40" fill="#ffffff">G</text></svg>
|
|
@@ -4,7 +4,17 @@ import React from "react";
|
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
import type { Profile } from "@/lib/social";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** The same tints, in the same order, as the iOS app's AvatarView. */
|
|
8
|
+
const TINTS = ["#ff9500", "#ff2d55", "#af52de", "#5856d6", "#30b0c7", "#34c759", "#007aff", "#ff3b30"];
|
|
9
|
+
|
|
10
|
+
/** A stable tint per username. Same hash as the iOS app, so a person has one color everywhere. */
|
|
11
|
+
export function avatarTint(username: string): string {
|
|
12
|
+
let h = 0;
|
|
13
|
+
for (const ch of username) h = (Math.imul(h, 31) + (ch.codePointAt(0) ?? 0)) & 0xffff;
|
|
14
|
+
return TINTS[h % TINTS.length];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A round profile photo, or the first letter of the name on a tinted circle. */
|
|
8
18
|
export function Avatar({
|
|
9
19
|
profile,
|
|
10
20
|
size = 32,
|
|
@@ -13,11 +23,12 @@ export function Avatar({
|
|
|
13
23
|
}: {
|
|
14
24
|
profile: Pick<Profile, "username" | "displayName" | "avatarUrl"> | null | undefined;
|
|
15
25
|
size?: number;
|
|
16
|
-
/**
|
|
26
|
+
/** The gradient ring, for people who posted in the last 24 hours. */
|
|
17
27
|
ring?: boolean;
|
|
18
28
|
className?: string;
|
|
19
29
|
}) {
|
|
20
30
|
const letter = (profile?.displayName || profile?.username || "?").trim().charAt(0).toUpperCase();
|
|
31
|
+
const tint = avatarTint(profile?.username ?? "");
|
|
21
32
|
const inner = profile?.avatarUrl ? (
|
|
22
33
|
<img
|
|
23
34
|
src={profile.avatarUrl}
|
|
@@ -29,22 +40,25 @@ export function Avatar({
|
|
|
29
40
|
/>
|
|
30
41
|
) : (
|
|
31
42
|
<span
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
aria-hidden
|
|
44
|
+
className="grid size-full place-items-center rounded-full font-semibold text-white"
|
|
45
|
+
style={{
|
|
46
|
+
fontSize: Math.max(10, Math.round(size * 0.42)),
|
|
47
|
+
background: `linear-gradient(to bottom, color-mix(in srgb, ${tint} 78%, white), ${tint})`,
|
|
48
|
+
}}
|
|
34
49
|
>
|
|
35
50
|
{letter}
|
|
36
51
|
</span>
|
|
37
52
|
);
|
|
53
|
+
const pad = ring ? Math.max(2, Math.round(size * 0.05)) : 0;
|
|
38
54
|
return (
|
|
39
55
|
<span
|
|
40
|
-
className={cn(
|
|
41
|
-
|
|
42
|
-
ring ? "bg-brand p-[2px]" : "",
|
|
43
|
-
className,
|
|
44
|
-
)}
|
|
45
|
-
style={{ width: size + (ring ? 4 : 0), height: size + (ring ? 4 : 0) }}
|
|
56
|
+
className={cn("inline-block shrink-0 rounded-full", ring ? "bg-story-ring" : "", className)}
|
|
57
|
+
style={{ width: size + pad * 4, height: size + pad * 4, padding: pad }}
|
|
46
58
|
>
|
|
47
|
-
<span className=
|
|
59
|
+
<span className="block size-full rounded-full bg-background" style={{ padding: pad }}>
|
|
60
|
+
{inner}
|
|
61
|
+
</span>
|
|
48
62
|
</span>
|
|
49
63
|
);
|
|
50
64
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mutation } from "@pylonsync/functions";
|
|
2
|
-
import { shapeSeed } from "../lib/seed";
|
|
2
|
+
import { SEED_PROFILES, demoUserId, shapeSeed } from "../lib/seed";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Load the demo people, posts, likes, comments, and follows once. The feed
|
|
@@ -8,7 +8,7 @@ import { shapeSeed } from "../lib/seed";
|
|
|
8
8
|
*
|
|
9
9
|
* Public so an anonymous first visitor seeds it. `unsafe.insert` writes past
|
|
10
10
|
* the policies, which only accept the caller's own id, so the posts read as
|
|
11
|
-
*
|
|
11
|
+
* eight different people. It only writes lib/seed.ts. Delete this function
|
|
12
12
|
* and lib/seed.ts when you launch.
|
|
13
13
|
*/
|
|
14
14
|
export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
@@ -16,7 +16,7 @@ export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
|
16
16
|
args: {},
|
|
17
17
|
async handler(ctx) {
|
|
18
18
|
await ctx.db.advisoryLock("consumer_seed_feed");
|
|
19
|
-
const existing = await ctx.db.unsafe.query("Profile", { userId:
|
|
19
|
+
const existing = await ctx.db.unsafe.query("Profile", { userId: demoUserId(SEED_PROFILES[0].username), $limit: 1 });
|
|
20
20
|
if (existing.length > 0) return { seeded: false };
|
|
21
21
|
|
|
22
22
|
const seed = shapeSeed();
|