@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState } from "react";
|
|
4
|
-
import { callFn } from "@pylonsync/react";
|
|
5
|
-
import { Check, X } from "lucide-react";
|
|
4
|
+
import { callFn, Link } from "@pylonsync/react";
|
|
5
|
+
import { Check, ChevronRight, X } from "lucide-react";
|
|
6
6
|
|
|
7
7
|
export interface SetupState {
|
|
8
8
|
orgId: string;
|
|
@@ -13,9 +13,9 @@ export interface SetupState {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* "
|
|
16
|
+
* "Get started" steps on the Overview. Each step is derived from real data (a
|
|
17
17
|
* project exists, a teammate or pending invite exists, Pro is active), so the
|
|
18
|
-
* list ticks itself off
|
|
18
|
+
* list ticks itself off. It disappears when all three are done or an
|
|
19
19
|
* owner/admin dismisses it for the workspace.
|
|
20
20
|
*/
|
|
21
21
|
export function SetupChecklist({ state }: { state: SetupState }) {
|
|
@@ -23,10 +23,10 @@ export function SetupChecklist({ state }: { state: SetupState }) {
|
|
|
23
23
|
const items = [
|
|
24
24
|
{ done: state.hasProject, label: "Create a project", href: "/dashboard/projects" },
|
|
25
25
|
{ done: state.hasTeammate, label: "Invite a teammate", href: "/dashboard/members" },
|
|
26
|
-
{ done: state.hasBilling, label: "Start
|
|
26
|
+
{ done: state.hasBilling, label: "Start the Pro trial", href: "/dashboard/billing" },
|
|
27
27
|
];
|
|
28
|
-
const
|
|
29
|
-
if (hidden ||
|
|
28
|
+
const doneCount = items.filter((i) => i.done).length;
|
|
29
|
+
if (hidden || doneCount === items.length) return null;
|
|
30
30
|
|
|
31
31
|
async function dismiss() {
|
|
32
32
|
setHidden(true);
|
|
@@ -38,42 +38,44 @@ export function SetupChecklist({ state }: { state: SetupState }) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
|
-
<div className="rounded-xl border border-zinc-200 bg-white
|
|
42
|
-
<div className="flex items-center justify-between">
|
|
43
|
-
<div>
|
|
44
|
-
<h2 className="text-
|
|
45
|
-
<
|
|
46
|
-
{
|
|
47
|
-
</
|
|
41
|
+
<div className="rounded-xl border border-zinc-200/80 bg-white">
|
|
42
|
+
<div className="flex items-center justify-between gap-3 border-b border-zinc-100 px-4 py-3">
|
|
43
|
+
<div className="flex items-center gap-3">
|
|
44
|
+
<h2 className="text-[13px] font-semibold text-zinc-900">Get started</h2>
|
|
45
|
+
<span className="font-mono text-[11.5px] tabular-nums text-zinc-400">
|
|
46
|
+
{doneCount}/{items.length}
|
|
47
|
+
</span>
|
|
48
48
|
</div>
|
|
49
49
|
{state.canDismiss && (
|
|
50
50
|
<button
|
|
51
51
|
type="button"
|
|
52
52
|
onClick={() => void dismiss()}
|
|
53
|
-
aria-label="
|
|
53
|
+
aria-label="Hide the setup steps"
|
|
54
54
|
className="flex size-7 items-center justify-center rounded-md text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700"
|
|
55
55
|
>
|
|
56
56
|
<X className="size-4" />
|
|
57
57
|
</button>
|
|
58
58
|
)}
|
|
59
59
|
</div>
|
|
60
|
-
<ul className="
|
|
60
|
+
<ul className="grid divide-y divide-zinc-100 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
|
|
61
61
|
{items.map((it) => (
|
|
62
|
-
<li key={it.label}
|
|
63
|
-
<span
|
|
64
|
-
className={
|
|
65
|
-
"flex size-5 shrink-0 items-center justify-center rounded-full border " +
|
|
66
|
-
(it.done ? "border-emerald-500 bg-emerald-500 text-white" : "border-zinc-300")
|
|
67
|
-
}
|
|
68
|
-
>
|
|
69
|
-
{it.done && <Check className="size-3" />}
|
|
70
|
-
</span>
|
|
62
|
+
<li key={it.label}>
|
|
71
63
|
{it.done ? (
|
|
72
|
-
<
|
|
64
|
+
<div className="flex items-center gap-2.5 px-4 py-3 text-[13px] text-zinc-400">
|
|
65
|
+
<span className="flex size-[18px] items-center justify-center rounded-full bg-brand text-white">
|
|
66
|
+
<Check className="size-3" strokeWidth={2.5} />
|
|
67
|
+
</span>
|
|
68
|
+
<span className="line-through">{it.label}</span>
|
|
69
|
+
</div>
|
|
73
70
|
) : (
|
|
74
|
-
<
|
|
75
|
-
{it.
|
|
76
|
-
|
|
71
|
+
<Link
|
|
72
|
+
href={it.href}
|
|
73
|
+
className="group flex items-center gap-2.5 px-4 py-3 text-[13px] font-medium text-zinc-800 transition-colors hover:bg-zinc-50"
|
|
74
|
+
>
|
|
75
|
+
<span className="size-[18px] rounded-full border-[1.5px] border-dashed border-zinc-300" />
|
|
76
|
+
{it.label}
|
|
77
|
+
<ChevronRight className="ml-auto size-3.5 text-zinc-300 transition-transform group-hover:translate-x-0.5 group-hover:text-zinc-500" />
|
|
78
|
+
</Link>
|
|
77
79
|
)}
|
|
78
80
|
</li>
|
|
79
81
|
))}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { initials } from "@/lib/names";
|
|
3
|
+
import { daysUntil, type TaskStatus } from "@/lib/tasks";
|
|
4
|
+
|
|
5
|
+
// Small presentational pieces shared by the Overview, the project list, and
|
|
6
|
+
// the task board. No data access here: every component renders from props.
|
|
7
|
+
|
|
8
|
+
/** Status glyph: an outlined circle that fills as the task moves to done. */
|
|
9
|
+
export function StatusIcon({
|
|
10
|
+
status,
|
|
11
|
+
className = "size-3.5",
|
|
12
|
+
}: {
|
|
13
|
+
status: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}) {
|
|
16
|
+
const s = status as TaskStatus;
|
|
17
|
+
if (s === "done") {
|
|
18
|
+
return (
|
|
19
|
+
<svg viewBox="0 0 14 14" className={`${className} shrink-0 text-brand`} aria-hidden>
|
|
20
|
+
<circle cx="7" cy="7" r="6.25" fill="currentColor" stroke="currentColor" strokeWidth="1.5" />
|
|
21
|
+
<path d="M4.3 7.2 6.1 9l3.6-3.8" fill="none" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const tone =
|
|
26
|
+
s === "in_progress" ? "text-amber-500" : s === "in_review" ? "text-emerald-500" : "text-zinc-400";
|
|
27
|
+
// The share of the inner disc that is filled: 0 for to do, half for in
|
|
28
|
+
// progress, three quarters for in review.
|
|
29
|
+
const fill = s === "in_progress" ? 0.5 : s === "in_review" ? 0.75 : 0;
|
|
30
|
+
const r = 3.25;
|
|
31
|
+
const c = 2 * Math.PI * r;
|
|
32
|
+
return (
|
|
33
|
+
<svg viewBox="0 0 14 14" className={`${className} shrink-0 ${tone}`} aria-hidden>
|
|
34
|
+
<circle cx="7" cy="7" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.5" />
|
|
35
|
+
{fill > 0 ? (
|
|
36
|
+
<circle
|
|
37
|
+
cx="7"
|
|
38
|
+
cy="7"
|
|
39
|
+
r={r}
|
|
40
|
+
fill="none"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
strokeWidth={r * 2}
|
|
43
|
+
strokeDasharray={`${c * fill} ${c}`}
|
|
44
|
+
transform="rotate(-90 7 7)"
|
|
45
|
+
/>
|
|
46
|
+
) : null}
|
|
47
|
+
</svg>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Priority glyph: three bars for high/medium/low, a filled square for urgent. */
|
|
52
|
+
export function PriorityIcon({
|
|
53
|
+
priority,
|
|
54
|
+
className = "size-3.5",
|
|
55
|
+
}: {
|
|
56
|
+
priority?: string | null;
|
|
57
|
+
className?: string;
|
|
58
|
+
}) {
|
|
59
|
+
const p = priority ?? "none";
|
|
60
|
+
if (p === "urgent") {
|
|
61
|
+
return (
|
|
62
|
+
<svg viewBox="0 0 14 14" className={`${className} shrink-0 text-orange-500`} aria-hidden>
|
|
63
|
+
<rect x="1" y="1" width="12" height="12" rx="3" fill="currentColor" />
|
|
64
|
+
<path d="M7 3.8v4" stroke="white" strokeWidth="1.6" strokeLinecap="round" />
|
|
65
|
+
<circle cx="7" cy="10.1" r="0.95" fill="white" />
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (p === "none") {
|
|
70
|
+
return (
|
|
71
|
+
<svg viewBox="0 0 14 14" className={`${className} shrink-0 text-zinc-300`} aria-hidden>
|
|
72
|
+
<path d="M2.5 7h2M6 7h2M9.5 7h2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
73
|
+
</svg>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const level = p === "high" ? 3 : p === "medium" ? 2 : 1;
|
|
77
|
+
return (
|
|
78
|
+
<svg viewBox="0 0 14 14" className={`${className} shrink-0 text-zinc-600`} aria-hidden>
|
|
79
|
+
{[0, 1, 2].map((i) => (
|
|
80
|
+
<rect
|
|
81
|
+
key={i}
|
|
82
|
+
x={2 + i * 3.75}
|
|
83
|
+
y={9.5 - i * 3}
|
|
84
|
+
width="2.5"
|
|
85
|
+
height={2.5 + i * 3}
|
|
86
|
+
rx="0.75"
|
|
87
|
+
fill="currentColor"
|
|
88
|
+
opacity={i < level ? 1 : 0.22}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
</svg>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stable avatar color per person, hashed from their id.
|
|
96
|
+
const AVATAR_TONES = [
|
|
97
|
+
"bg-[#e0e7ff] text-[#3730a3]",
|
|
98
|
+
"bg-[#dcfce7] text-[#166534]",
|
|
99
|
+
"bg-[#fef3c7] text-[#92400e]",
|
|
100
|
+
"bg-[#fce7f3] text-[#9d174d]",
|
|
101
|
+
"bg-[#e0f2fe] text-[#075985]",
|
|
102
|
+
"bg-[#ede9fe] text-[#5b21b6]",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
export function toneFor(key: string): string {
|
|
106
|
+
let h = 0;
|
|
107
|
+
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0;
|
|
108
|
+
return AVATAR_TONES[h % AVATAR_TONES.length];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function Avatar({
|
|
112
|
+
id,
|
|
113
|
+
name,
|
|
114
|
+
size = "sm",
|
|
115
|
+
}: {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
size?: "xs" | "sm" | "md";
|
|
119
|
+
}) {
|
|
120
|
+
const dims =
|
|
121
|
+
size === "xs" ? "size-[18px] text-[8.5px]" : size === "sm" ? "size-6 text-[10px]" : "size-8 text-[11.5px]";
|
|
122
|
+
return (
|
|
123
|
+
<span
|
|
124
|
+
title={name}
|
|
125
|
+
className={`inline-flex shrink-0 items-center justify-center rounded-full font-semibold ${dims} ${toneFor(id)}`}
|
|
126
|
+
>
|
|
127
|
+
{initials(name)}
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A dashed ring for tasks with nobody assigned. */
|
|
133
|
+
export function EmptyAvatar({ size = "sm" }: { size?: "xs" | "sm" }) {
|
|
134
|
+
const dims = size === "xs" ? "size-[18px]" : "size-6";
|
|
135
|
+
return (
|
|
136
|
+
<span
|
|
137
|
+
title="Unassigned"
|
|
138
|
+
className={`inline-flex shrink-0 rounded-full border border-dashed border-zinc-300 ${dims}`}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
144
|
+
|
|
145
|
+
/** "Sep 30" from `YYYY-MM-DD`, read as a calendar day (no time zone shift). */
|
|
146
|
+
export function shortDay(day: string): string {
|
|
147
|
+
const [, m, d] = day.split("-").map(Number);
|
|
148
|
+
return `${MONTHS[(m ?? 1) - 1]} ${d}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Due date with its urgency: red when overdue, amber when due today or
|
|
153
|
+
* tomorrow. `today` comes from the server render so the SSR HTML and the
|
|
154
|
+
* hydrated client agree.
|
|
155
|
+
*/
|
|
156
|
+
export function DueLabel({
|
|
157
|
+
due,
|
|
158
|
+
today,
|
|
159
|
+
done,
|
|
160
|
+
}: {
|
|
161
|
+
due?: string | null;
|
|
162
|
+
today: string;
|
|
163
|
+
done?: boolean;
|
|
164
|
+
}) {
|
|
165
|
+
if (!due) return null;
|
|
166
|
+
const days = daysUntil(due, today);
|
|
167
|
+
const tone = done
|
|
168
|
+
? "text-zinc-400"
|
|
169
|
+
: days < 0
|
|
170
|
+
? "text-red-600"
|
|
171
|
+
: days <= 1
|
|
172
|
+
? "text-amber-600"
|
|
173
|
+
: "text-zinc-500";
|
|
174
|
+
const label = done ? shortDay(due) : days === 0 ? "Today" : days === 1 ? "Tomorrow" : shortDay(due);
|
|
175
|
+
return <span className={`whitespace-nowrap text-[12px] tabular-nums ${tone}`}>{label}</span>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A thin progress bar with the percentage beside it. */
|
|
179
|
+
export function Progress({ percent, className = "" }: { percent: number; className?: string }) {
|
|
180
|
+
return (
|
|
181
|
+
<div className={`flex items-center gap-2.5 ${className}`}>
|
|
182
|
+
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-zinc-100">
|
|
183
|
+
<div className="h-full rounded-full bg-brand" style={{ width: `${percent}%` }} />
|
|
184
|
+
</div>
|
|
185
|
+
<span className="w-9 text-right font-mono text-[11.5px] tabular-nums text-zinc-500">{percent}%</span>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Project monogram tile, colored per project. */
|
|
191
|
+
export function ProjectMark({ id, name, className = "size-7 text-[11px]" }: { id: string; name: string; className?: string }) {
|
|
192
|
+
return (
|
|
193
|
+
<span
|
|
194
|
+
className={`inline-flex shrink-0 items-center justify-center rounded-md font-semibold ${className} ${toneFor(id)}`}
|
|
195
|
+
>
|
|
196
|
+
{initials(name)}
|
|
197
|
+
</span>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mutation, v } from "@pylonsync/functions";
|
|
2
2
|
import { normalizeProjectName } from "../lib/projects";
|
|
3
|
-
import { canCreateProject,
|
|
3
|
+
import { canCreateProject, planFromSubscriptions } from "../lib/plans";
|
|
4
4
|
|
|
5
5
|
// createProject — the free-tier cap lives here, on the server. The Project
|
|
6
6
|
// policy denies client inserts, so a browser cannot skip the paywall by
|
|
@@ -23,21 +23,15 @@ export default mutation<
|
|
|
23
23
|
// Any member of the workspace may create a project. Fails closed.
|
|
24
24
|
await ctx.requireMember(args.orgId);
|
|
25
25
|
|
|
26
|
-
const subs = (await ctx.db.unsafe.
|
|
27
|
-
referenceId:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}>;
|
|
31
|
-
const sub = subs.find((s) => s.referenceId === args.orgId) ?? null;
|
|
32
|
-
const plan = planFromSubscription(sub);
|
|
26
|
+
const subs = (await ctx.db.unsafe.query("StripeSubscription", {
|
|
27
|
+
referenceId: args.orgId,
|
|
28
|
+
})) as Array<{ plan?: string; status?: string }>;
|
|
29
|
+
const plan = planFromSubscriptions(subs);
|
|
33
30
|
|
|
34
|
-
const projects = (await ctx.db.unsafe.
|
|
35
|
-
orgId: string;
|
|
31
|
+
const projects = (await ctx.db.unsafe.query("Project", { orgId: args.orgId })) as Array<{
|
|
36
32
|
status?: string;
|
|
37
33
|
}>;
|
|
38
|
-
const active = projects.filter(
|
|
39
|
-
(p) => p.orgId === args.orgId && (p.status ?? "active") !== "archived",
|
|
40
|
-
).length;
|
|
34
|
+
const active = projects.filter((p) => (p.status ?? "active") !== "archived").length;
|
|
41
35
|
if (!canCreateProject(plan, active)) {
|
|
42
36
|
throw ctx.error(
|
|
43
37
|
"LIMIT_REACHED",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { memberName } from "../lib/members";
|
|
3
|
+
import {
|
|
4
|
+
isTaskPriority,
|
|
5
|
+
isTaskStatus,
|
|
6
|
+
normalizeDueDate,
|
|
7
|
+
normalizeTaskTitle,
|
|
8
|
+
} from "../lib/tasks";
|
|
9
|
+
|
|
10
|
+
// createTask — adds a task to a project. The workspace comes from the
|
|
11
|
+
// project row, never from the caller, and the caller must be a member of it.
|
|
12
|
+
// An assignee must be a member of the same workspace.
|
|
13
|
+
export default mutation<
|
|
14
|
+
{
|
|
15
|
+
projectId: string;
|
|
16
|
+
title: string;
|
|
17
|
+
status?: string;
|
|
18
|
+
priority?: string;
|
|
19
|
+
assigneeId?: string;
|
|
20
|
+
dueDate?: string;
|
|
21
|
+
},
|
|
22
|
+
{ id: string }
|
|
23
|
+
>({
|
|
24
|
+
args: {
|
|
25
|
+
projectId: v.string(),
|
|
26
|
+
title: v.string(),
|
|
27
|
+
status: v.optional(v.string()),
|
|
28
|
+
priority: v.optional(v.string()),
|
|
29
|
+
assigneeId: v.optional(v.string()),
|
|
30
|
+
dueDate: v.optional(v.string()),
|
|
31
|
+
},
|
|
32
|
+
async handler(ctx, args) {
|
|
33
|
+
const title = normalizeTaskTitle(args.title);
|
|
34
|
+
if (!title) throw ctx.error("INVALID_ARGS", "Task title must be 1–200 characters.");
|
|
35
|
+
const status = args.status ?? "todo";
|
|
36
|
+
if (!isTaskStatus(status)) throw ctx.error("INVALID_ARGS", "Unknown task status.");
|
|
37
|
+
const priority = args.priority ?? "none";
|
|
38
|
+
if (!isTaskPriority(priority)) throw ctx.error("INVALID_ARGS", "Unknown task priority.");
|
|
39
|
+
const dueDate = args.dueDate ? normalizeDueDate(args.dueDate) : null;
|
|
40
|
+
if (args.dueDate && !dueDate) throw ctx.error("INVALID_ARGS", "Due date must be YYYY-MM-DD.");
|
|
41
|
+
|
|
42
|
+
const project = await ctx.db.unsafe.get("Project", args.projectId);
|
|
43
|
+
if (!project) throw ctx.error("NOT_FOUND", "Project not found.");
|
|
44
|
+
const orgId = String(project.orgId);
|
|
45
|
+
await ctx.requireMember(orgId);
|
|
46
|
+
|
|
47
|
+
let assigneeName: string | null = null;
|
|
48
|
+
if (args.assigneeId) {
|
|
49
|
+
assigneeName = await memberName(ctx.db, orgId, args.assigneeId);
|
|
50
|
+
if (assigneeName === null) {
|
|
51
|
+
throw ctx.error("INVALID_ARGS", "The assignee is not a member of this workspace.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const id = (await ctx.db.unsafe.insert("Task", {
|
|
56
|
+
orgId,
|
|
57
|
+
projectId: args.projectId,
|
|
58
|
+
title,
|
|
59
|
+
status,
|
|
60
|
+
priority,
|
|
61
|
+
...(args.assigneeId ? { assigneeId: args.assigneeId, assigneeName } : {}),
|
|
62
|
+
...(dueDate ? { dueDate } : {}),
|
|
63
|
+
...(status === "done" ? { completedAt: new Date().toISOString() } : {}),
|
|
64
|
+
})) as string;
|
|
65
|
+
return { id };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// deleteProject — removes a project and every task in it. The caller must be
|
|
4
|
+
// a member of the project's own workspace.
|
|
5
|
+
export default mutation<{ projectId: string }, { ok: true; tasks: number }>({
|
|
6
|
+
args: { projectId: v.string() },
|
|
7
|
+
async handler(ctx, args) {
|
|
8
|
+
const project = await ctx.db.unsafe.get("Project", args.projectId);
|
|
9
|
+
if (!project) throw ctx.error("NOT_FOUND", "Project not found.");
|
|
10
|
+
await ctx.requireMember(String(project.orgId));
|
|
11
|
+
|
|
12
|
+
const tasks = await ctx.db.unsafe.query("Task", { projectId: args.projectId });
|
|
13
|
+
for (const t of tasks) await ctx.db.unsafe.delete("Task", String(t.id));
|
|
14
|
+
await ctx.db.unsafe.delete("Project", args.projectId);
|
|
15
|
+
return { ok: true, tasks: tasks.length };
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// deleteTask — removes one task. The caller must be a member of the task's
|
|
4
|
+
// own workspace.
|
|
5
|
+
export default mutation<{ taskId: string }, { ok: true }>({
|
|
6
|
+
args: { taskId: v.string() },
|
|
7
|
+
async handler(ctx, args) {
|
|
8
|
+
const task = await ctx.db.unsafe.get("Task", args.taskId);
|
|
9
|
+
if (!task) throw ctx.error("NOT_FOUND", "Task not found.");
|
|
10
|
+
await ctx.requireMember(String(task.orgId));
|
|
11
|
+
await ctx.db.unsafe.delete("Task", args.taskId);
|
|
12
|
+
return { ok: true };
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { workspaceMembers } from "../lib/members";
|
|
3
|
+
import { planSampleWorkspace } from "../lib/sample-workspace";
|
|
4
|
+
|
|
5
|
+
// seedWorkspace — fills an empty workspace with the sample projects and
|
|
6
|
+
// tasks in lib/sample-workspace.ts. Owners and admins only. It does nothing
|
|
7
|
+
// when the workspace already has a project, so calling it twice is safe.
|
|
8
|
+
// Tasks are assigned to the workspace's current members.
|
|
9
|
+
export default mutation<{ orgId: string }, { seeded: boolean; projects: number; tasks: number }>({
|
|
10
|
+
args: { orgId: v.string() },
|
|
11
|
+
async handler(ctx, args) {
|
|
12
|
+
await ctx.requireMember(args.orgId, { role: ["owner", "admin"] });
|
|
13
|
+
|
|
14
|
+
const existing = await ctx.db.unsafe.query("Project", { orgId: args.orgId });
|
|
15
|
+
if (existing.length > 0) return { seeded: false, projects: 0, tasks: 0 };
|
|
16
|
+
|
|
17
|
+
const members = await workspaceMembers(ctx.db, args.orgId);
|
|
18
|
+
// The caller first, so the tasks marked for member 0 land on them.
|
|
19
|
+
members.sort((a, b) => (a.userId === ctx.auth.userId ? -1 : b.userId === ctx.auth.userId ? 1 : 0));
|
|
20
|
+
const plan = planSampleWorkspace(members, new Date());
|
|
21
|
+
|
|
22
|
+
let taskCount = 0;
|
|
23
|
+
for (const p of plan) {
|
|
24
|
+
const projectId = (await ctx.db.unsafe.insert("Project", {
|
|
25
|
+
orgId: args.orgId,
|
|
26
|
+
name: p.name,
|
|
27
|
+
description: p.description,
|
|
28
|
+
status: p.status,
|
|
29
|
+
})) as string;
|
|
30
|
+
for (const t of p.tasks) {
|
|
31
|
+
await ctx.db.unsafe.insert("Task", {
|
|
32
|
+
orgId: args.orgId,
|
|
33
|
+
projectId,
|
|
34
|
+
title: t.title,
|
|
35
|
+
status: t.status,
|
|
36
|
+
priority: t.priority,
|
|
37
|
+
dueDate: t.dueDate,
|
|
38
|
+
...(t.completedAt ? { completedAt: t.completedAt } : {}),
|
|
39
|
+
...(t.assigneeId ? { assigneeId: t.assigneeId, assigneeName: t.assigneeName } : {}),
|
|
40
|
+
});
|
|
41
|
+
taskCount++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { seeded: true, projects: plan.length, tasks: taskCount };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { canCreateProject, planFromSubscriptions } from "../lib/plans";
|
|
3
|
+
|
|
4
|
+
// setProjectStatus — archives or restores a project. Restoring counts
|
|
5
|
+
// against the free plan's active-project cap the same way createProject
|
|
6
|
+
// does, so archiving and restoring cannot get a free workspace past it.
|
|
7
|
+
export default mutation<{ projectId: string; status: string }, { id: string; status: string }>({
|
|
8
|
+
args: { projectId: v.string(), status: v.string() },
|
|
9
|
+
async handler(ctx, args) {
|
|
10
|
+
if (args.status !== "active" && args.status !== "archived") {
|
|
11
|
+
throw ctx.error("INVALID_ARGS", "Status must be active or archived.");
|
|
12
|
+
}
|
|
13
|
+
const project = await ctx.db.unsafe.get("Project", args.projectId);
|
|
14
|
+
if (!project) throw ctx.error("NOT_FOUND", "Project not found.");
|
|
15
|
+
const orgId = String(project.orgId);
|
|
16
|
+
await ctx.requireMember(orgId);
|
|
17
|
+
|
|
18
|
+
const current = (project.status as string | undefined) ?? "active";
|
|
19
|
+
if (current === args.status) return { id: args.projectId, status: current };
|
|
20
|
+
|
|
21
|
+
if (args.status === "active") {
|
|
22
|
+
const subs = (await ctx.db.unsafe.query("StripeSubscription", {
|
|
23
|
+
referenceId: orgId,
|
|
24
|
+
})) as Array<{ plan?: string; status?: string }>;
|
|
25
|
+
const plan = planFromSubscriptions(subs);
|
|
26
|
+
const projects = (await ctx.db.unsafe.query("Project", { orgId })) as Array<{
|
|
27
|
+
status?: string;
|
|
28
|
+
}>;
|
|
29
|
+
const active = projects.filter((p) => (p.status ?? "active") !== "archived").length;
|
|
30
|
+
if (!canCreateProject(plan, active)) {
|
|
31
|
+
throw ctx.error(
|
|
32
|
+
"LIMIT_REACHED",
|
|
33
|
+
"This workspace is at the free plan's project limit. Upgrade to Pro for unlimited projects.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await ctx.db.unsafe.update("Project", args.projectId, { status: args.status });
|
|
39
|
+
return { id: args.projectId, status: args.status };
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -3,6 +3,9 @@ import { mutation, v } from "@pylonsync/functions";
|
|
|
3
3
|
// The one User field a person may edit about themselves. Email changes
|
|
4
4
|
// need re-verification and passwords have their own route, so the User
|
|
5
5
|
// policy denies client writes and this function owns display-name edits.
|
|
6
|
+
// Tasks keep a copy of their assignee's name (see the Task entity in
|
|
7
|
+
// app.ts), so the person's assigned tasks in their current workspaces are
|
|
8
|
+
// updated with the new name.
|
|
6
9
|
export default mutation<{ displayName: string }, { displayName: string }>({
|
|
7
10
|
args: { displayName: v.string() },
|
|
8
11
|
async handler(ctx, args) {
|
|
@@ -13,6 +16,16 @@ export default mutation<{ displayName: string }, { displayName: string }>({
|
|
|
13
16
|
throw ctx.error("INVALID_ARGS", "Name must be 1–60 characters.");
|
|
14
17
|
}
|
|
15
18
|
await ctx.db.unsafe.update("User", userId, { displayName });
|
|
19
|
+
// Only in workspaces the person still belongs to: a former member must
|
|
20
|
+
// not be able to write into a workspace they have left.
|
|
21
|
+
const memberships = await ctx.db.unsafe.query("OrgMember", { userId });
|
|
22
|
+
const orgs = new Set(memberships.map((m) => String(m.orgId)));
|
|
23
|
+
const assigned = await ctx.db.unsafe.query("Task", { assigneeId: userId });
|
|
24
|
+
for (const t of assigned) {
|
|
25
|
+
if (orgs.has(String(t.orgId)) && t.assigneeName !== displayName) {
|
|
26
|
+
await ctx.db.unsafe.update("Task", String(t.id), { assigneeName: displayName });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
16
29
|
return { displayName };
|
|
17
30
|
},
|
|
18
31
|
});
|
|
@@ -3,8 +3,8 @@ import { normalizeProjectName } from "../lib/projects";
|
|
|
3
3
|
|
|
4
4
|
// updateProject — the reference example for a first-party server function, and
|
|
5
5
|
// the core authoring loop end to end: entity → policy → FUNCTION → call it from
|
|
6
|
-
// the client. The
|
|
7
|
-
//
|
|
6
|
+
// the client. The project dialog (app/dashboard/projects-client.tsx) saves
|
|
7
|
+
// through this via `callFn("updateProject", …)`.
|
|
8
8
|
//
|
|
9
9
|
// Why a server function and not a direct client `db.update`? Two reasons the
|
|
10
10
|
// pattern exists:
|
|
@@ -17,9 +17,8 @@ import { normalizeProjectName } from "../lib/projects";
|
|
|
17
17
|
// FORBIDDEN otherwise). Pass `{ role: ["owner", "admin"] }` to make edits
|
|
18
18
|
// admin-only.
|
|
19
19
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// Reach for a function when you need more than that.
|
|
20
|
+
// Archive/restore (setProjectStatus) and delete (deleteProject) are server
|
|
21
|
+
// functions too: the Project policy denies every direct client write.
|
|
23
22
|
export default mutation<
|
|
24
23
|
{ projectId: string; name: string; description?: string },
|
|
25
24
|
{ id: string; name: string }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { memberName } from "../lib/members";
|
|
3
|
+
import {
|
|
4
|
+
isTaskPriority,
|
|
5
|
+
isTaskStatus,
|
|
6
|
+
normalizeDueDate,
|
|
7
|
+
normalizeTaskTitle,
|
|
8
|
+
} from "../lib/tasks";
|
|
9
|
+
|
|
10
|
+
// updateTask — changes the fields a person edits on the board. Only the
|
|
11
|
+
// fields passed are written. An empty string clears `assigneeId` or
|
|
12
|
+
// `dueDate`. The caller must be a member of the task's own workspace, and the
|
|
13
|
+
// task can never move to another project or workspace.
|
|
14
|
+
export default mutation<
|
|
15
|
+
{
|
|
16
|
+
taskId: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
priority?: string;
|
|
20
|
+
assigneeId?: string;
|
|
21
|
+
dueDate?: string;
|
|
22
|
+
},
|
|
23
|
+
{ id: string }
|
|
24
|
+
>({
|
|
25
|
+
args: {
|
|
26
|
+
taskId: v.string(),
|
|
27
|
+
title: v.optional(v.string()),
|
|
28
|
+
status: v.optional(v.string()),
|
|
29
|
+
priority: v.optional(v.string()),
|
|
30
|
+
assigneeId: v.optional(v.string()),
|
|
31
|
+
dueDate: v.optional(v.string()),
|
|
32
|
+
},
|
|
33
|
+
async handler(ctx, args) {
|
|
34
|
+
const task = await ctx.db.unsafe.get("Task", args.taskId);
|
|
35
|
+
if (!task) throw ctx.error("NOT_FOUND", "Task not found.");
|
|
36
|
+
const orgId = String(task.orgId);
|
|
37
|
+
await ctx.requireMember(orgId);
|
|
38
|
+
|
|
39
|
+
const patch: Record<string, unknown> = {};
|
|
40
|
+
if (args.title !== undefined) {
|
|
41
|
+
const title = normalizeTaskTitle(args.title);
|
|
42
|
+
if (!title) throw ctx.error("INVALID_ARGS", "Task title must be 1–200 characters.");
|
|
43
|
+
patch.title = title;
|
|
44
|
+
}
|
|
45
|
+
if (args.status !== undefined) {
|
|
46
|
+
if (!isTaskStatus(args.status)) throw ctx.error("INVALID_ARGS", "Unknown task status.");
|
|
47
|
+
patch.status = args.status;
|
|
48
|
+
if (args.status === "done" && task.status !== "done") {
|
|
49
|
+
patch.completedAt = new Date().toISOString();
|
|
50
|
+
} else if (args.status !== "done") {
|
|
51
|
+
patch.completedAt = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (args.priority !== undefined) {
|
|
55
|
+
if (!isTaskPriority(args.priority)) throw ctx.error("INVALID_ARGS", "Unknown task priority.");
|
|
56
|
+
patch.priority = args.priority;
|
|
57
|
+
}
|
|
58
|
+
if (args.assigneeId !== undefined) {
|
|
59
|
+
if (args.assigneeId === (task.assigneeId ?? "")) {
|
|
60
|
+
// Unchanged. The dialog sends every field, and a person who has since
|
|
61
|
+
// left the workspace stays assigned until someone picks a new owner.
|
|
62
|
+
} else if (args.assigneeId === "") {
|
|
63
|
+
patch.assigneeId = null;
|
|
64
|
+
patch.assigneeName = null;
|
|
65
|
+
} else {
|
|
66
|
+
const name = await memberName(ctx.db, orgId, args.assigneeId);
|
|
67
|
+
if (name === null) {
|
|
68
|
+
throw ctx.error("INVALID_ARGS", "The assignee is not a member of this workspace.");
|
|
69
|
+
}
|
|
70
|
+
patch.assigneeId = args.assigneeId;
|
|
71
|
+
patch.assigneeName = name;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (args.dueDate !== undefined) {
|
|
75
|
+
if (args.dueDate === "") {
|
|
76
|
+
patch.dueDate = null;
|
|
77
|
+
} else {
|
|
78
|
+
const due = normalizeDueDate(args.dueDate);
|
|
79
|
+
if (!due) throw ctx.error("INVALID_ARGS", "Due date must be YYYY-MM-DD.");
|
|
80
|
+
patch.dueDate = due;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (Object.keys(patch).length > 0) {
|
|
84
|
+
await ctx.db.unsafe.update("Task", args.taskId, patch);
|
|
85
|
+
}
|
|
86
|
+
return { id: args.taskId };
|
|
87
|
+
},
|
|
88
|
+
});
|