@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
|
@@ -1,111 +1,117 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link } from "@pylonsync/react";
|
|
3
|
-
import {
|
|
3
|
+
import { LogOut, Search } from "lucide-react";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
|
+
import { BRAND } from "@/lib/brand";
|
|
6
|
+
import { NAV } from "@/components/nav";
|
|
5
7
|
import { Avatar } from "@/components/avatar";
|
|
8
|
+
import { BrandMark } from "@/components/brand-mark";
|
|
6
9
|
import { Kbd } from "@/components/kbd";
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
count?: number;
|
|
11
|
+
/** True when `href` is the section `pathname` belongs to. */
|
|
12
|
+
export function isActive(href: string, pathname: string): boolean {
|
|
13
|
+
if (href === "/") return pathname === "/";
|
|
14
|
+
return pathname === href || pathname.startsWith(`${href}/`);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export const NAV: Array<Omit<NavItem, "count">> = [
|
|
16
|
-
{ href: "/", label: "Inbox", icon: <Inbox /> },
|
|
17
|
-
{ href: "/customers", label: "Customers", icon: <Users /> },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
|
-
* The
|
|
22
|
-
*
|
|
18
|
+
* The navigation rail: brand, search, links with counts, and the signed-in
|
|
19
|
+
* person. The same component renders as the fixed rail on wide screens and
|
|
20
|
+
* inside the drawer on phones (see components/app-shell.tsx).
|
|
21
|
+
*
|
|
22
|
+
* Presentational: the path, counts, and handlers arrive as props, so it
|
|
23
|
+
* renders the same in a test.
|
|
23
24
|
*/
|
|
24
25
|
export function Sidebar({
|
|
25
|
-
|
|
26
|
-
email,
|
|
26
|
+
user,
|
|
27
27
|
pathname,
|
|
28
28
|
counts,
|
|
29
29
|
onOpenCommand,
|
|
30
|
+
onNavigate,
|
|
30
31
|
onSignOut,
|
|
32
|
+
className,
|
|
31
33
|
}: {
|
|
32
|
-
|
|
33
|
-
email: string;
|
|
34
|
+
user: { name: string; email: string };
|
|
34
35
|
pathname: string;
|
|
36
|
+
/** Badge per nav href. Omit a key to show no number. */
|
|
35
37
|
counts?: Record<string, number>;
|
|
36
38
|
onOpenCommand: () => void;
|
|
39
|
+
/** Called after a link is chosen, so the phone drawer can close. */
|
|
40
|
+
onNavigate?: () => void;
|
|
37
41
|
onSignOut: () => void;
|
|
42
|
+
className?: string;
|
|
38
43
|
}) {
|
|
39
44
|
return (
|
|
40
|
-
<
|
|
41
|
-
<div className="flex h-
|
|
42
|
-
<
|
|
43
|
-
|
|
45
|
+
<div className={cn("flex h-full w-full flex-col bg-surface-1", className)}>
|
|
46
|
+
<div className="flex h-13 shrink-0 items-center gap-2.5 px-4">
|
|
47
|
+
<BrandMark />
|
|
48
|
+
<span className="truncate text-[14px] font-semibold tracking-[-0.01em]">
|
|
49
|
+
{BRAND.name}
|
|
44
50
|
</span>
|
|
45
|
-
<span className="truncate text-[13px] font-semibold">{workspace}</span>
|
|
46
51
|
</div>
|
|
47
52
|
|
|
48
|
-
<div className="px-
|
|
53
|
+
<div className="px-3 pb-3">
|
|
49
54
|
<button
|
|
50
55
|
type="button"
|
|
51
56
|
onClick={onOpenCommand}
|
|
52
57
|
className={cn(
|
|
53
|
-
"flex w-full items-center gap-2 rounded-md border border-border bg-background px-2
|
|
54
|
-
"text-[
|
|
58
|
+
"flex h-8 w-full items-center gap-2 rounded-md border border-border bg-background px-2.5",
|
|
59
|
+
"text-[12.5px] text-muted-foreground shadow-[0_1px_1px_rgb(0_0_0/0.03)] transition-colors",
|
|
60
|
+
"hover:border-foreground/15 hover:text-foreground",
|
|
55
61
|
)}
|
|
56
62
|
>
|
|
57
|
-
<
|
|
58
|
-
<span className="flex-1 text-left">Search
|
|
59
|
-
<Kbd>⌘K</Kbd>
|
|
63
|
+
<Search className="size-3.5" />
|
|
64
|
+
<span className="flex-1 text-left">Search</span>
|
|
65
|
+
<Kbd className="max-md:hidden">⌘K</Kbd>
|
|
60
66
|
</button>
|
|
61
67
|
</div>
|
|
62
68
|
|
|
63
|
-
<nav className="flex-1 space-y-
|
|
69
|
+
<nav className="flex-1 space-y-px overflow-y-auto px-3" aria-label="Main">
|
|
64
70
|
{NAV.map((item) => {
|
|
65
|
-
const active =
|
|
66
|
-
|
|
71
|
+
const active = isActive(item.href, pathname);
|
|
72
|
+
const count = counts?.[item.href];
|
|
67
73
|
return (
|
|
68
74
|
<Link
|
|
69
75
|
key={item.href}
|
|
70
76
|
href={item.href}
|
|
77
|
+
onClick={onNavigate}
|
|
71
78
|
aria-current={active ? "page" : undefined}
|
|
72
79
|
className={cn(
|
|
73
|
-
"flex h-
|
|
74
|
-
"[&_svg]:size-
|
|
80
|
+
"flex h-8 items-center gap-2.5 rounded-md px-2.5 text-[13px] transition-colors md:h-7.5",
|
|
81
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
75
82
|
active
|
|
76
|
-
? "bg-
|
|
77
|
-
: "text-muted-foreground hover:bg-
|
|
83
|
+
? "bg-foreground/[0.06] font-medium text-foreground [&_svg]:text-primary"
|
|
84
|
+
: "text-muted-foreground hover:bg-foreground/[0.04] hover:text-foreground",
|
|
78
85
|
)}
|
|
79
86
|
>
|
|
80
87
|
{item.icon}
|
|
81
88
|
<span className="flex-1 truncate">{item.label}</span>
|
|
82
|
-
{
|
|
83
|
-
<span className="tabular text-[
|
|
84
|
-
{counts[item.href]}
|
|
85
|
-
</span>
|
|
89
|
+
{typeof count === "number" ? (
|
|
90
|
+
<span className="tabular text-[11.5px] text-muted-foreground">{count}</span>
|
|
86
91
|
) : null}
|
|
87
92
|
</Link>
|
|
88
93
|
);
|
|
89
94
|
})}
|
|
90
95
|
</nav>
|
|
91
96
|
|
|
92
|
-
<div className="border-t border-border p-
|
|
93
|
-
<div className="flex items-center gap-2
|
|
94
|
-
<Avatar name={email}
|
|
95
|
-
<
|
|
96
|
-
{
|
|
97
|
-
|
|
97
|
+
<div className="border-t border-border p-3">
|
|
98
|
+
<div className="flex items-center gap-2.5">
|
|
99
|
+
<Avatar name={user.name || user.email} />
|
|
100
|
+
<div className="min-w-0 flex-1 leading-tight">
|
|
101
|
+
<p className="truncate text-[12.5px] font-medium">{user.name || "Signed in"}</p>
|
|
102
|
+
<p className="truncate text-[11.5px] text-muted-foreground">{user.email}</p>
|
|
103
|
+
</div>
|
|
98
104
|
<button
|
|
99
105
|
type="button"
|
|
100
106
|
onClick={onSignOut}
|
|
101
107
|
title="Sign out"
|
|
102
108
|
aria-label="Sign out"
|
|
103
|
-
className="rounded p-1 text-muted-foreground transition-colors hover:bg-
|
|
109
|
+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-foreground/[0.06] hover:text-foreground"
|
|
104
110
|
>
|
|
105
|
-
<LogOut className="size-
|
|
111
|
+
<LogOut className="size-4" />
|
|
106
112
|
</button>
|
|
107
113
|
</div>
|
|
108
114
|
</div>
|
|
109
|
-
</
|
|
115
|
+
</div>
|
|
110
116
|
);
|
|
111
117
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
3
|
import { duration } from "@/lib/format";
|
|
4
|
-
import {
|
|
4
|
+
import { sla, type Ticket } from "@/lib/tickets";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* How long is left to reply, or how long we're already late.
|
|
@@ -19,19 +19,16 @@ export function SlaIndicator({
|
|
|
19
19
|
now?: number;
|
|
20
20
|
className?: string;
|
|
21
21
|
}) {
|
|
22
|
-
const state =
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
const minutes = minutesToBreach(ticket, now);
|
|
26
|
-
if (minutes === null) return null;
|
|
22
|
+
const { state, minutes } = sla(ticket, now);
|
|
23
|
+
if (minutes === null || (state !== "due" && state !== "breached")) return null;
|
|
27
24
|
|
|
28
25
|
const breached = state === "breached";
|
|
29
26
|
return (
|
|
30
27
|
<span
|
|
31
28
|
title={breached ? "First response overdue" : "First response due"}
|
|
32
29
|
className={cn(
|
|
33
|
-
"tabular inline-flex items-center gap-1 text-[
|
|
34
|
-
breached ? "text-destructive" : "text-muted-foreground",
|
|
30
|
+
"tabular inline-flex items-center gap-1.5 rounded px-1.5 py-px text-[11.5px] font-medium whitespace-nowrap",
|
|
31
|
+
breached ? "bg-destructive/10 text-destructive" : "text-muted-foreground",
|
|
35
32
|
className,
|
|
36
33
|
)}
|
|
37
34
|
>
|
|
@@ -58,7 +58,7 @@ export function Thread({
|
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
60
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
61
|
-
<div className="min-h-0 flex-1 space-y-
|
|
61
|
+
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-4 md:px-6 md:py-5">
|
|
62
62
|
{ordered.length === 0 ? (
|
|
63
63
|
<p className="py-8 text-center text-[12px] text-muted-foreground">
|
|
64
64
|
No messages on this ticket yet.
|
|
@@ -72,7 +72,7 @@ export function Thread({
|
|
|
72
72
|
<article
|
|
73
73
|
key={message.id}
|
|
74
74
|
className={cn(
|
|
75
|
-
"rounded-lg border p-3",
|
|
75
|
+
"rounded-lg border p-3 md:p-3.5",
|
|
76
76
|
message.internal
|
|
77
77
|
? "border-stage-proposal/30 bg-stage-proposal/5"
|
|
78
78
|
: message.fromCustomer
|
|
@@ -80,7 +80,7 @@ export function Thread({
|
|
|
80
80
|
: "border-border bg-card",
|
|
81
81
|
)}
|
|
82
82
|
>
|
|
83
|
-
<div className="mb-1.5 flex items-center gap-2 text-[
|
|
83
|
+
<div className="mb-1.5 flex items-center gap-2 text-[12px] text-muted-foreground">
|
|
84
84
|
<Avatar name={who ?? "?"} size="sm" />
|
|
85
85
|
<span className="font-medium text-foreground">{who ?? "Unknown"}</span>
|
|
86
86
|
{message.internal ? (
|
|
@@ -96,7 +96,7 @@ export function Thread({
|
|
|
96
96
|
{relativeTime(message.createdAt, now)}
|
|
97
97
|
</time>
|
|
98
98
|
</div>
|
|
99
|
-
<p className="whitespace-pre-wrap text-[
|
|
99
|
+
<p className="whitespace-pre-wrap text-[13.5px] leading-6 md:text-[13px]">
|
|
100
100
|
{message.body}
|
|
101
101
|
</p>
|
|
102
102
|
</article>
|
|
@@ -108,20 +108,20 @@ export function Thread({
|
|
|
108
108
|
<form
|
|
109
109
|
onSubmit={submit}
|
|
110
110
|
className={cn(
|
|
111
|
-
"shrink-0 border-t p-3 transition-colors",
|
|
111
|
+
"shrink-0 border-t p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] transition-colors md:px-6",
|
|
112
112
|
internal ? "border-stage-proposal/40 bg-stage-proposal/5" : "border-border",
|
|
113
113
|
)}
|
|
114
114
|
>
|
|
115
115
|
<Textarea
|
|
116
116
|
value={body}
|
|
117
|
-
rows={
|
|
117
|
+
rows={2}
|
|
118
118
|
placeholder={internal ? "Internal note — the customer won't see this…" : "Reply to the customer…"}
|
|
119
119
|
aria-label={internal ? "Internal note" : "Reply"}
|
|
120
120
|
onChange={(event) => setBody(event.target.value)}
|
|
121
121
|
onKeyDown={(event) => {
|
|
122
122
|
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) submit(event);
|
|
123
123
|
}}
|
|
124
|
-
className="min-h-
|
|
124
|
+
className="min-h-16 resize-none md:min-h-20"
|
|
125
125
|
/>
|
|
126
126
|
<div className="mt-2 flex items-center justify-between gap-2">
|
|
127
127
|
<label className="flex items-center gap-2 text-[12px] text-muted-foreground">
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
3
|
import { Avatar } from "@/components/avatar";
|
|
4
|
-
import {
|
|
4
|
+
import { PriorityIcon, StatusBadge } from "@/components/priority-badge";
|
|
5
5
|
import { SlaIndicator } from "@/components/sla-indicator";
|
|
6
6
|
import { relativeTime } from "@/lib/format";
|
|
7
|
-
import { queueOrder, ticketNumber, type Ticket } from "@/lib/tickets";
|
|
7
|
+
import { priorityById, queueOrder, ticketNumber, type Ticket } from "@/lib/tickets";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The queue. Rows are ordered by lib/tickets.queueOrder — breached first, then
|
|
11
|
-
* priority, then oldest — so the top of the list is
|
|
12
|
-
*
|
|
11
|
+
* priority, then oldest — so the top of the list is the next thing to work on,
|
|
12
|
+
* not the newest arrival.
|
|
13
|
+
*
|
|
14
|
+
* One line per ticket on a desktop, two on a phone. Keyboard: ↑/↓ or j/k move
|
|
15
|
+
* between tickets and Enter opens one.
|
|
13
16
|
*
|
|
14
17
|
* Presentational: ordering is pure and tested, and selection is delegated.
|
|
15
18
|
*/
|
|
@@ -18,6 +21,7 @@ export function TicketList({
|
|
|
18
21
|
customerName,
|
|
19
22
|
assigneeName,
|
|
20
23
|
selectedId,
|
|
24
|
+
showStatus = true,
|
|
21
25
|
now,
|
|
22
26
|
onSelect,
|
|
23
27
|
}: {
|
|
@@ -25,50 +29,85 @@ export function TicketList({
|
|
|
25
29
|
customerName: (id: string | null | undefined) => string | null;
|
|
26
30
|
assigneeName: (id: string | null | undefined) => string | null;
|
|
27
31
|
selectedId?: string | null;
|
|
32
|
+
/** Off when every row has the same status, such as on the Open tab. */
|
|
33
|
+
showStatus?: boolean;
|
|
28
34
|
now?: number;
|
|
29
35
|
onSelect: (id: string) => void;
|
|
30
36
|
}) {
|
|
31
37
|
const ordered = queueOrder(tickets, now);
|
|
32
38
|
|
|
39
|
+
function onKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
|
|
40
|
+
const down = event.key === "ArrowDown" || event.key === "j";
|
|
41
|
+
const up = event.key === "ArrowUp" || event.key === "k";
|
|
42
|
+
if (!down && !up) return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
const item = event.currentTarget.closest("li");
|
|
45
|
+
const next = (down ? item?.nextElementSibling : item?.previousElementSibling) as HTMLElement | null;
|
|
46
|
+
next?.querySelector("button")?.focus();
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
return (
|
|
34
|
-
<ul className="min-h-0 flex-1 overflow-y-auto">
|
|
50
|
+
<ul className="min-h-0 flex-1 overflow-y-auto" aria-label="Tickets">
|
|
35
51
|
{ordered.map((ticket) => {
|
|
36
52
|
const assignee = assigneeName(ticket.assigneeId);
|
|
53
|
+
const customer = customerName(ticket.customerId);
|
|
37
54
|
return (
|
|
38
55
|
<li key={ticket.id}>
|
|
39
56
|
<button
|
|
40
57
|
type="button"
|
|
41
58
|
onClick={() => onSelect(ticket.id)}
|
|
59
|
+
onKeyDown={onKeyDown}
|
|
42
60
|
aria-current={ticket.id === selectedId ? "true" : undefined}
|
|
43
61
|
className={cn(
|
|
44
|
-
"
|
|
45
|
-
|
|
62
|
+
"grid w-full grid-cols-[16px_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 border-b border-border/60 px-4 py-2.5 text-left transition-colors md:h-11 md:py-0 md:pl-5 md:pr-5",
|
|
63
|
+
"focus-visible:bg-primary/[0.06] focus-visible:outline-none",
|
|
64
|
+
ticket.id === selectedId ? "bg-foreground/[0.04]" : "hover:bg-foreground/[0.025]",
|
|
46
65
|
)}
|
|
47
66
|
>
|
|
48
|
-
<
|
|
49
|
-
|
|
67
|
+
<PriorityIcon
|
|
68
|
+
priority={ticket.priority}
|
|
69
|
+
title={priorityById(ticket.priority)?.label}
|
|
70
|
+
className="row-span-2 self-start pt-0.5 md:row-span-1 md:self-center md:pt-0"
|
|
71
|
+
/>
|
|
72
|
+
<span className="flex min-w-0 items-baseline gap-2.5">
|
|
73
|
+
<span className="tabular hidden w-11 shrink-0 text-[12px] text-muted-foreground md:inline">
|
|
50
74
|
{ticketNumber(ticket.id, ticket.createdAt)}
|
|
51
75
|
</span>
|
|
52
|
-
<span className="
|
|
53
|
-
|
|
76
|
+
<span className="truncate text-[13px] font-medium">{ticket.subject}</span>
|
|
77
|
+
<span className="hidden truncate text-[12.5px] text-muted-foreground lg:inline">
|
|
78
|
+
{customer ?? ""}
|
|
54
79
|
</span>
|
|
80
|
+
</span>
|
|
81
|
+
<span className="flex shrink-0 items-center gap-2.5 md:gap-3">
|
|
55
82
|
<SlaIndicator ticket={ticket} now={now} />
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
<span className="truncate">{customerName(ticket.customerId) ?? "—"}</span>
|
|
61
|
-
<span className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
83
|
+
{showStatus ? (
|
|
84
|
+
<StatusBadge status={ticket.status} className="hidden md:inline-flex" />
|
|
85
|
+
) : null}
|
|
86
|
+
<span className="hidden w-5 md:inline-flex" title={assignee ?? "Unassigned"}>
|
|
62
87
|
{assignee ? (
|
|
63
88
|
<Avatar name={assignee} size="sm" />
|
|
64
89
|
) : (
|
|
65
|
-
<span className="
|
|
90
|
+
<span className="size-5 rounded-full border border-dashed border-muted-foreground/50" />
|
|
66
91
|
)}
|
|
92
|
+
</span>
|
|
93
|
+
<time
|
|
94
|
+
className="tabular hidden w-14 text-right text-[12px] text-muted-foreground md:inline"
|
|
95
|
+
dateTime={ticket.createdAt ?? undefined}
|
|
96
|
+
>
|
|
97
|
+
{relativeTime(ticket.createdAt, now)}
|
|
98
|
+
</time>
|
|
99
|
+
</span>
|
|
100
|
+
<span className="col-start-2 col-end-4 flex min-w-0 items-center gap-2 text-[12px] text-muted-foreground md:hidden">
|
|
101
|
+
<span className="tabular">{ticketNumber(ticket.id, ticket.createdAt)}</span>
|
|
102
|
+
<span aria-hidden="true">·</span>
|
|
103
|
+
<span className="truncate">{customer ?? "No customer"}</span>
|
|
104
|
+
<span className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
105
|
+
{assignee ? <Avatar name={assignee} size="sm" /> : <span>Unassigned</span>}
|
|
67
106
|
<time dateTime={ticket.createdAt ?? undefined}>
|
|
68
107
|
{relativeTime(ticket.createdAt, now)}
|
|
69
108
|
</time>
|
|
70
109
|
</span>
|
|
71
|
-
</
|
|
110
|
+
</span>
|
|
72
111
|
</button>
|
|
73
112
|
</li>
|
|
74
113
|
);
|
|
@@ -76,3 +115,24 @@ export function TicketList({
|
|
|
76
115
|
</ul>
|
|
77
116
|
);
|
|
78
117
|
}
|
|
118
|
+
|
|
119
|
+
/** The queue's shape while the first sync is in flight. */
|
|
120
|
+
export function TicketListSkeleton() {
|
|
121
|
+
return (
|
|
122
|
+
<ul className="min-h-0 flex-1 overflow-hidden" aria-hidden="true">
|
|
123
|
+
{Array.from({ length: 10 }).map((_, index) => (
|
|
124
|
+
<li
|
|
125
|
+
key={index}
|
|
126
|
+
className="flex h-14 items-center gap-3 border-b border-border/60 px-4 md:h-11 md:px-5"
|
|
127
|
+
>
|
|
128
|
+
<span className="size-3.5 animate-pulse rounded bg-foreground/[0.07]" />
|
|
129
|
+
<span
|
|
130
|
+
className="h-2.5 animate-pulse rounded-full bg-foreground/[0.07]"
|
|
131
|
+
style={{ width: `${38 + ((index * 17) % 30)}%` }}
|
|
132
|
+
/>
|
|
133
|
+
<span className="ml-auto h-2.5 w-12 animate-pulse rounded-full bg-foreground/[0.07]" />
|
|
134
|
+
</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -5,15 +5,16 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
|
|
7
7
|
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-[13px] font-medium transition-[color,background-color,box-shadow,transform] duration-150 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
|
-
default:
|
|
12
|
+
default:
|
|
13
|
+
"bg-primary text-primary-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.15),0_1px_2px_rgb(0_0_0/0.1)] hover:bg-primary/90",
|
|
13
14
|
destructive:
|
|
14
15
|
"bg-destructive text-white hover:bg-destructive/90",
|
|
15
16
|
outline:
|
|
16
|
-
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
"border border-input bg-background shadow-[0_1px_1px_rgb(0_0_0/0.03)] hover:bg-accent hover:text-accent-foreground",
|
|
17
18
|
secondary:
|
|
18
19
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
20
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
@@ -21,7 +22,7 @@ const buttonVariants = cva(
|
|
|
21
22
|
},
|
|
22
23
|
size: {
|
|
23
24
|
default: "h-9 px-4 py-2",
|
|
24
|
-
sm: "h-8 rounded-md px-3 text-
|
|
25
|
+
sm: "h-8 rounded-md px-3 text-[12.5px] md:h-7.5 [&_svg]:size-3.5",
|
|
25
26
|
lg: "h-10 rounded-md px-8",
|
|
26
27
|
icon: "h-9 w-9",
|
|
27
28
|
},
|
|
@@ -35,7 +35,7 @@ const DialogContent = React.forwardRef<
|
|
|
35
35
|
<DialogPrimitive.Content
|
|
36
36
|
ref={ref}
|
|
37
37
|
className={cn(
|
|
38
|
-
"fixed left-[50%] top-[50%] z-50 grid w-
|
|
38
|
+
"fixed left-[50%] top-[50%] z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-xl border bg-background p-5 shadow-[0_24px_48px_-12px_rgb(0_0_0/0.25)] duration-200 sm:p-6",
|
|
39
39
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
40
40
|
className,
|
|
41
41
|
)}
|
|
@@ -12,7 +12,7 @@ function Input({
|
|
|
12
12
|
type={type}
|
|
13
13
|
data-slot="input"
|
|
14
14
|
className={cn(
|
|
15
|
-
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-
|
|
15
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-[13px] transition-colors",
|
|
16
16
|
"placeholder:text-muted-foreground",
|
|
17
17
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
18
18
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
@@ -22,7 +22,7 @@ function Select({
|
|
|
22
22
|
<select
|
|
23
23
|
data-slot="select"
|
|
24
24
|
className={cn(
|
|
25
|
-
"flex h-9 w-full appearance-none rounded-md border border-input bg-transparent py-1 pl-3 pr-8 text-
|
|
25
|
+
"flex h-9 w-full appearance-none rounded-md border border-input bg-transparent py-1 pl-3 pr-8 text-base shadow-sm md:text-[13px] transition-colors",
|
|
26
26
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
27
27
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
28
28
|
className,
|
|
@@ -10,7 +10,7 @@ function Textarea({
|
|
|
10
10
|
<textarea
|
|
11
11
|
data-slot="textarea"
|
|
12
12
|
className={cn(
|
|
13
|
-
"flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-
|
|
13
|
+
"flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm md:text-[13px] transition-colors",
|
|
14
14
|
"placeholder:text-muted-foreground",
|
|
15
15
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
16
16
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
@@ -1,49 +1,59 @@
|
|
|
1
1
|
import { mutation } from "@pylonsync/functions";
|
|
2
|
-
import { shapeSeed } from "../lib/seed";
|
|
2
|
+
import { SEED_TEAM, shapeSeed } from "../lib/seed";
|
|
3
|
+
import { ensureDemoUser } from "../lib/demo-team";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Fill a brand-new helpdesk with a realistic queue, once.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The client calls this after sign-in. It returns immediately if any ticket
|
|
9
|
+
* already exists, so it is safe on every load and never duplicates the
|
|
10
|
+
* fixtures or touches real data. An advisory lock stops two first loads from
|
|
11
|
+
* seeding twice.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
+
* Tickets are assigned across the person who signed in and four demo agents
|
|
14
|
+
* (see SEED_TEAM and lib/demo-team.ts). Delete this function,
|
|
15
|
+
* lib/seed.ts, and the `seedWorkspace` call in app/workspace.tsx once real
|
|
16
|
+
* tickets arrive, then delete the demo users.
|
|
13
17
|
*/
|
|
14
18
|
export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
15
19
|
auth: "user",
|
|
16
20
|
args: {},
|
|
17
21
|
async handler(ctx) {
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
if ((await ctx.db.query("Ticket", { $limit: 1 })).length > 0) return { seeded: false };
|
|
23
|
+
await ctx.db.advisoryLock("helpdesk_seed_workspace");
|
|
24
|
+
if ((await ctx.db.query("Ticket", { $limit: 1 })).length > 0) return { seeded: false };
|
|
20
25
|
|
|
21
26
|
const seed = shapeSeed();
|
|
22
27
|
const me = ctx.auth.userId;
|
|
23
28
|
|
|
29
|
+
// Position 0 is the person who signed in; SEED_TEAM follows.
|
|
30
|
+
const team: Array<string | null> = [me];
|
|
31
|
+
for (const person of SEED_TEAM) team.push(await ensureDemoUser(ctx.db, person));
|
|
32
|
+
|
|
24
33
|
const customerIds = new Map<string, string>();
|
|
25
34
|
for (const customer of seed.customers) {
|
|
26
35
|
const id = await ctx.db.insert("Customer", customer.row);
|
|
27
36
|
customerIds.set(customer.key, id as string);
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
const
|
|
39
|
+
const tickets = new Map<string, { id: string; assignee: string | null }>();
|
|
31
40
|
for (const ticket of seed.tickets) {
|
|
41
|
+
const assignee = ticket.assignee === null ? null : (team[ticket.assignee] ?? me);
|
|
32
42
|
const id = await ctx.db.insert("Ticket", {
|
|
33
43
|
...ticket.row,
|
|
34
44
|
customerId: customerIds.get(ticket.customer) ?? null,
|
|
35
|
-
|
|
36
|
-
// ones stay unassigned so the queue has something to pick up.
|
|
37
|
-
assigneeId: ticket.row.firstRespondedAt ? me : null,
|
|
45
|
+
assigneeId: assignee,
|
|
38
46
|
});
|
|
39
|
-
|
|
47
|
+
tickets.set(ticket.key, { id: id as string, assignee });
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
// Agent replies on a ticket are written by its assignee.
|
|
42
51
|
for (const message of seed.messages) {
|
|
52
|
+
const ticket = tickets.get(message.ticket);
|
|
43
53
|
await ctx.db.insert("Message", {
|
|
44
54
|
...message.row,
|
|
45
|
-
ticketId:
|
|
46
|
-
authorId: message.row.fromCustomer ? null : me,
|
|
55
|
+
ticketId: ticket?.id ?? null,
|
|
56
|
+
authorId: message.row.fromCustomer ? null : (ticket?.assignee ?? me),
|
|
47
57
|
});
|
|
48
58
|
}
|
|
49
59
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The product name, in one place. The sidebar, the sign-in screen, the page
|
|
2
|
+
// title, and the drawer on phones all read it. Rename the product here; the
|
|
3
|
+
// mark is in components/brand-mark.tsx and the favicon is app/icon.svg.
|
|
4
|
+
|
|
5
|
+
export const BRAND = {
|
|
6
|
+
name: "Frontdesk",
|
|
7
|
+
/** One line on the sign-in screen: what the app holds and who sees it. */
|
|
8
|
+
description: "Your team's support queue and customer threads. Every agent works from the same inbox.",
|
|
9
|
+
} as const;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Creates the demo teammates the seed spreads records across. Each is a User
|
|
2
|
+
// row with a `@demo.invalid` address and no password. Outside dev mode nobody
|
|
3
|
+
// can sign in as one: the address can't receive a sign-in or reset email.
|
|
4
|
+
// In dev mode (`PYLON_DEV_MODE=true`) the auth API returns sign-in codes
|
|
5
|
+
// directly, so anyone can sign in as a demo teammate there.
|
|
6
|
+
|
|
7
|
+
import type { DbWriter } from "@pylonsync/functions";
|
|
8
|
+
|
|
9
|
+
export interface DemoPerson {
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The address to try on `attempt` (0, 1, 2, …): the person's own address
|
|
16
|
+
* first, then `local+2@demo.invalid`, `local+3@…`, and so on.
|
|
17
|
+
*/
|
|
18
|
+
export function demoEmail(email: string, attempt: number): string {
|
|
19
|
+
if (attempt === 0) return email;
|
|
20
|
+
const at = email.indexOf("@");
|
|
21
|
+
return `${email.slice(0, at)}+${attempt + 1}${email.slice(at)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A User row the seed may reuse: one it created itself on an earlier run.
|
|
26
|
+
* A row with a password or a verified email belongs to someone who
|
|
27
|
+
* registered that address, so the seed never assigns demo records to it.
|
|
28
|
+
*/
|
|
29
|
+
export function isDemoRow(row: Record<string, unknown>): boolean {
|
|
30
|
+
return !row.passwordHash && !row.emailVerified;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Returns the id of a demo User row for `person`, creating it if needed. */
|
|
34
|
+
export async function ensureDemoUser(db: DbWriter, person: DemoPerson): Promise<string> {
|
|
35
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
36
|
+
const email = demoEmail(person.email, attempt);
|
|
37
|
+
// Users are written by auth, and their policy denies client inserts, so
|
|
38
|
+
// demo rows go through the server-trust surface. The read sees
|
|
39
|
+
// passwordHash, which is how a registered account is told apart.
|
|
40
|
+
const [existing] = await db.unsafe.query("User", { email, $limit: 1 });
|
|
41
|
+
if (!existing) {
|
|
42
|
+
return db.unsafe.insert("User", {
|
|
43
|
+
email,
|
|
44
|
+
displayName: person.name,
|
|
45
|
+
createdAt: new Date(Date.now() - 200 * 86_400_000).toISOString(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (isDemoRow(existing)) return existing.id as string;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`No free demo address for ${person.email}`);
|
|
51
|
+
}
|