@pylonsync/create-pylon 0.19.1 → 0.21.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/_root/AGENTS.md +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/ai-studio/AGENTS.md +1 -1
- 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/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/todo/AGENTS.md +1 -1
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/AGENTS.md +1 -1
- 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,12 +1,19 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
browseListings,
|
|
4
|
+
listingSrcSet,
|
|
5
|
+
pendingOfferCounts,
|
|
6
|
+
percentOfAsk,
|
|
7
|
+
suggestedOffers,
|
|
8
|
+
} from "../lib/catalog";
|
|
9
|
+
import { makeSlug, type Listing, type Offer } from "../client/market";
|
|
4
10
|
|
|
5
11
|
const listings: Listing[] = [
|
|
6
12
|
{
|
|
7
13
|
id: "1",
|
|
8
14
|
sellerId: "seller-1",
|
|
9
15
|
sellerName: "maple-fox",
|
|
16
|
+
location: "Brooklyn, NY",
|
|
10
17
|
title: "Walnut lounge chair",
|
|
11
18
|
slug: "walnut-lounge-chair-1",
|
|
12
19
|
description: "Restored frame with new upholstery.",
|
|
@@ -78,3 +85,58 @@ test("listing slugs remain readable and unique", () => {
|
|
|
78
85
|
"vintage-technics-sl-1200-mk2-a1f3",
|
|
79
86
|
);
|
|
80
87
|
});
|
|
88
|
+
|
|
89
|
+
test("browse searches the seller's location", () => {
|
|
90
|
+
expect(
|
|
91
|
+
browseListings(listings, { query: "brooklyn" }).map((listing) => listing.id),
|
|
92
|
+
).toEqual(["1"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("pending offer counts ignore answered offers", () => {
|
|
96
|
+
const offer = (id: string, listingId: string, status: Offer["status"]): Offer => ({
|
|
97
|
+
id,
|
|
98
|
+
listingId,
|
|
99
|
+
listingTitle: "",
|
|
100
|
+
sellerId: "s",
|
|
101
|
+
buyerId: "b",
|
|
102
|
+
buyerName: "B",
|
|
103
|
+
amount: 10,
|
|
104
|
+
status,
|
|
105
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
106
|
+
});
|
|
107
|
+
const counts = pendingOfferCounts([
|
|
108
|
+
offer("a", "1", "pending"),
|
|
109
|
+
offer("b", "1", "pending"),
|
|
110
|
+
offer("c", "1", "declined"),
|
|
111
|
+
offer("d", "2", "accepted"),
|
|
112
|
+
]);
|
|
113
|
+
expect(counts.get("1")).toBe(2);
|
|
114
|
+
expect(counts.has("2")).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("suggested offers step below the asking price", () => {
|
|
118
|
+
expect(suggestedOffers(680)).toEqual([650, 610, 580]);
|
|
119
|
+
expect(suggestedOffers(95)).toEqual([90, 86, 81]);
|
|
120
|
+
expect(suggestedOffers(1)).toEqual([]);
|
|
121
|
+
expect(suggestedOffers(0)).toEqual([]);
|
|
122
|
+
for (const price of [5, 49, 120, 240, 1150]) {
|
|
123
|
+
for (const amount of suggestedOffers(price)) {
|
|
124
|
+
expect(amount).toBeGreaterThan(0);
|
|
125
|
+
expect(amount).toBeLessThan(price);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("offer percentage of the asking price", () => {
|
|
131
|
+
expect(percentOfAsk(620, 680)).toBe(91);
|
|
132
|
+
expect(percentOfAsk(10, 0)).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("bundled photos get a responsive srcset; others do not", () => {
|
|
136
|
+
expect(listingSrcSet("/images/listings/watch.webp")).toBe(
|
|
137
|
+
"/images/listings/watch-640.webp 640w, /images/listings/watch.webp 1120w",
|
|
138
|
+
);
|
|
139
|
+
expect(listingSrcSet("/api/files/abc")).toBeUndefined();
|
|
140
|
+
expect(listingSrcSet("https://example.com/a.webp")).toBeUndefined();
|
|
141
|
+
expect(listingSrcSet(undefined)).toBeUndefined();
|
|
142
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
4
|
+
import type { Listing } from "../client/market";
|
|
5
|
+
|
|
6
|
+
mock.module("@pylonsync/react", () => ({
|
|
7
|
+
Link: ({ href, children, seed: _seed, ...rest }: Record<string, unknown>) => (
|
|
8
|
+
<a href={href as string} {...rest}>
|
|
9
|
+
{children as React.ReactNode}
|
|
10
|
+
</a>
|
|
11
|
+
),
|
|
12
|
+
db: { useQuery: () => ({ data: [], loading: false }) },
|
|
13
|
+
init: () => {},
|
|
14
|
+
configureClient: () => {},
|
|
15
|
+
callFn: async () => ({}),
|
|
16
|
+
setSessionToken: async () => {},
|
|
17
|
+
storageKey: (key: string) => key,
|
|
18
|
+
}));
|
|
19
|
+
const { ListingCard } = await import("../client/ListingCard");
|
|
20
|
+
|
|
21
|
+
afterEach(cleanup);
|
|
22
|
+
|
|
23
|
+
const listing: Listing = {
|
|
24
|
+
id: "1",
|
|
25
|
+
sellerId: "s1",
|
|
26
|
+
sellerName: "Nora Lind",
|
|
27
|
+
location: "Brooklyn, NY",
|
|
28
|
+
title: "Danish teak lounge chair",
|
|
29
|
+
slug: "danish-teak-lounge-chair-a1f3",
|
|
30
|
+
description: "",
|
|
31
|
+
price: 680,
|
|
32
|
+
category: "furniture",
|
|
33
|
+
condition: "good",
|
|
34
|
+
status: "active",
|
|
35
|
+
imageUrl: "/images/listings/lounge-chair.webp",
|
|
36
|
+
seed: "a1f3",
|
|
37
|
+
createdAt: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
test("card shows price, title, location, and the live offer count", () => {
|
|
41
|
+
render(<ListingCard listing={listing} pendingOffers={2} />);
|
|
42
|
+
expect(screen.getByText("$680")).toBeDefined();
|
|
43
|
+
expect(screen.getByText("Danish teak lounge chair")).toBeDefined();
|
|
44
|
+
expect(screen.getByText("Brooklyn, NY · Good")).toBeDefined();
|
|
45
|
+
expect(screen.getByText("2 offers")).toBeDefined();
|
|
46
|
+
const link = screen.getByRole("link");
|
|
47
|
+
expect(link.getAttribute("href")).toBe("/listing/danish-teak-lounge-chair-a1f3");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a sold card shows Sold instead of the offer count", () => {
|
|
51
|
+
render(
|
|
52
|
+
<ListingCard listing={{ ...listing, id: "2", status: "sold" }} pendingOffers={3} />,
|
|
53
|
+
);
|
|
54
|
+
expect(screen.getAllByText("Sold").length).toBe(1);
|
|
55
|
+
expect(screen.queryByText("3 offers")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a listing that arrived live is marked Just listed", () => {
|
|
59
|
+
render(<ListingCard listing={{ ...listing, id: "3" }} fresh />);
|
|
60
|
+
expect(screen.getByText("Just listed")).toBeDefined();
|
|
61
|
+
});
|
|
@@ -62,7 +62,7 @@ this pass before the user has to ask.
|
|
|
62
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
63
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
64
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
65
|
-
- **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`.
|
|
65
|
+
- **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
|
|
66
66
|
- **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
|
|
67
67
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
68
68
|
- **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
|
|
@@ -10,11 +10,16 @@ API, auth, and realtime sync from one process on one port.
|
|
|
10
10
|
__RUN_DEV__
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Open http://localhost:4321 and create an account. The first sign-in seeds
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Open http://localhost:4321 and create an account. The first sign-in seeds ten
|
|
14
|
+
projects across active, paused, and complete, with two active ones over budget,
|
|
15
|
+
so every state is visible on first load. Delete `functions/seedWorkspace.ts` and
|
|
16
16
|
`lib/seed.ts` once you have real work.
|
|
17
17
|
|
|
18
|
+
The tasks and timesheets are spread across you and four demo teammates. Each is a User
|
|
19
|
+
row with a `@demo.invalid` address and no password, so outside dev mode nobody
|
|
20
|
+
can sign in as one (see `lib/demo-team.ts`). Delete those users along with the
|
|
21
|
+
seed.
|
|
22
|
+
|
|
18
23
|
## What's interesting here
|
|
19
24
|
|
|
20
25
|
**Time is a ledger, not a total.** A task\'s logged hours are the sum of its
|
|
@@ -46,6 +51,22 @@ Rejecting "1h30" because it isn\'t "90" is the kind of friction that stops time
|
|
|
46
51
|
being logged at all, which costs far more than a forgiving parser. Values over a
|
|
47
52
|
day are refused — a typo on a timesheet becomes a typo on an invoice.
|
|
48
53
|
|
|
54
|
+
## Brand and layout
|
|
55
|
+
|
|
56
|
+
The product ships as **Workbench**. The name lives in `lib/brand.ts`, and the
|
|
57
|
+
sidebar, sign-in screen, page titles, and phone drawer all read it. The mark is
|
|
58
|
+
`components/brand-mark.tsx`; `app/icon.svg`, `app/apple-icon.png`, and
|
|
59
|
+
`public/favicon.ico` draw the same shape for browsers and home screens. The
|
|
60
|
+
accent colour is `--brand` at the top of `app/globals.css`, and the typeface
|
|
61
|
+
(Manrope) is set in `fonts` in `app.ts`.
|
|
62
|
+
|
|
63
|
+
The frame is `components/app-shell.tsx`. From 768px up the sidebar is a fixed
|
|
64
|
+
rail; below that it opens as a drawer from the menu button in each page header,
|
|
65
|
+
tables drop their lower-priority columns (`hideBelow` in
|
|
66
|
+
`components/data-table.tsx`), and boards scroll one column at a time. The nav
|
|
67
|
+
links are in `components/nav.tsx`. Every nav count comes from `navCounts` in
|
|
68
|
+
`app/workspace.tsx`, and page headers and tabs read the same numbers.
|
|
69
|
+
|
|
49
70
|
## Layout
|
|
50
71
|
|
|
51
72
|
```
|
|
Binary file
|
|
@@ -17,7 +17,7 @@ export function ClientsView() {
|
|
|
17
17
|
const [open, setOpen] = useState(false);
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<RequireAuth
|
|
20
|
+
<RequireAuth>
|
|
21
21
|
<Workspace pathname="/clients">
|
|
22
22
|
{(data) => {
|
|
23
23
|
const forClient = (clientId: string) =>
|
|
@@ -27,16 +27,23 @@ export function ClientsView() {
|
|
|
27
27
|
{
|
|
28
28
|
key: "name",
|
|
29
29
|
header: "Client",
|
|
30
|
+
className: "w-[60%] md:w-[30%]",
|
|
30
31
|
cell: (row) => (
|
|
31
|
-
<span className="flex items-center gap-2">
|
|
32
|
-
<Avatar name={row.name}
|
|
33
|
-
<span className="
|
|
32
|
+
<span className="flex min-w-0 items-center gap-2.5">
|
|
33
|
+
<Avatar name={row.name} shape="square" />
|
|
34
|
+
<span className="min-w-0">
|
|
35
|
+
<span className="block truncate font-medium">{row.name}</span>
|
|
36
|
+
<span className="block truncate text-[12px] text-muted-foreground md:hidden">
|
|
37
|
+
{row.email ?? ""}
|
|
38
|
+
</span>
|
|
39
|
+
</span>
|
|
34
40
|
</span>
|
|
35
41
|
),
|
|
36
42
|
},
|
|
37
43
|
{
|
|
38
44
|
key: "email",
|
|
39
45
|
header: "Email",
|
|
46
|
+
hideBelow: "md",
|
|
40
47
|
cell: (row) => (
|
|
41
48
|
<span className="text-muted-foreground">{row.email ?? "—"}</span>
|
|
42
49
|
),
|
|
@@ -44,12 +51,16 @@ export function ClientsView() {
|
|
|
44
51
|
{
|
|
45
52
|
key: "projects",
|
|
46
53
|
header: "Projects",
|
|
54
|
+
hideBelow: "sm",
|
|
55
|
+
className: "w-[96px]",
|
|
47
56
|
numeric: true,
|
|
48
57
|
cell: (row) => forClient(row.id).length,
|
|
49
58
|
},
|
|
50
59
|
{
|
|
51
60
|
key: "logged",
|
|
52
61
|
header: "Logged",
|
|
62
|
+
hideBelow: "lg",
|
|
63
|
+
className: "w-[110px]",
|
|
53
64
|
numeric: true,
|
|
54
65
|
cell: (row) =>
|
|
55
66
|
duration(
|
|
@@ -62,6 +73,7 @@ export function ClientsView() {
|
|
|
62
73
|
{
|
|
63
74
|
key: "billable",
|
|
64
75
|
header: "Billable",
|
|
76
|
+
className: "w-[120px] md:w-[140px]",
|
|
65
77
|
numeric: true,
|
|
66
78
|
cell: (row) =>
|
|
67
79
|
money(
|
|
@@ -82,16 +94,18 @@ export function ClientsView() {
|
|
|
82
94
|
|
|
83
95
|
return (
|
|
84
96
|
<>
|
|
85
|
-
<PageHeader title="Clients" count={
|
|
97
|
+
<PageHeader title="Clients" count={data.navCounts["/clients"]}>
|
|
86
98
|
<Button size="sm" onClick={() => setOpen(true)}>
|
|
87
99
|
<Plus />
|
|
88
|
-
New client
|
|
100
|
+
<span className="max-sm:sr-only">New client</span>
|
|
89
101
|
</Button>
|
|
90
102
|
</PageHeader>
|
|
91
103
|
|
|
92
104
|
<DataTable
|
|
93
105
|
rows={rows}
|
|
94
106
|
columns={columns}
|
|
107
|
+
loading={data.loading}
|
|
108
|
+
label="Clients"
|
|
95
109
|
empty={
|
|
96
110
|
<EmptyState
|
|
97
111
|
icon={<Users />}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { BRAND } from "@/lib/brand";
|
|
3
4
|
import { ClientsView } from "./clients-view";
|
|
4
5
|
|
|
5
6
|
export const metadata: Metadata = {
|
|
6
|
-
title:
|
|
7
|
+
title: `Clients · ${BRAND.name}`,
|
|
7
8
|
robots: "noindex",
|
|
8
9
|
};
|
|
9
10
|
|
|
@@ -9,15 +9,21 @@
|
|
|
9
9
|
/* ---------------------------------------------------------------------------
|
|
10
10
|
Design tokens.
|
|
11
11
|
|
|
12
|
-
Tuned for a dense internal tool
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
alternative, not an afterthought.
|
|
12
|
+
Tuned for a dense internal tool: a near-neutral surface ramp, hairline
|
|
13
|
+
borders instead of shadows, and one accent used sparingly so status colour
|
|
14
|
+
still means something. Light is the default; `.dark` holds a complete dark
|
|
15
|
+
set (add `class="dark"` to <html> in app/layout.tsx to switch).
|
|
17
16
|
|
|
18
|
-
These are the
|
|
17
|
+
These are the variable names shadcn/ui generates against, so
|
|
19
18
|
`npx shadcn@latest add <component>` drops in already themed.
|
|
20
19
|
--------------------------------------------------------------------------- */
|
|
20
|
+
:root {
|
|
21
|
+
/* The brand accent: primary buttons, the brand mark, focus rings, and the
|
|
22
|
+
active nav icon. Change these two lines to re-colour the product. */
|
|
23
|
+
--brand: oklch(0.57 0.17 42);
|
|
24
|
+
--brand-dark: oklch(0.68 0.16 45);
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
:root {
|
|
22
28
|
--radius: 0.5rem;
|
|
23
29
|
|
|
@@ -28,7 +34,7 @@
|
|
|
28
34
|
--popover: oklch(1 0 0);
|
|
29
35
|
--popover-foreground: var(--foreground);
|
|
30
36
|
|
|
31
|
-
--primary:
|
|
37
|
+
--primary: var(--brand);
|
|
32
38
|
--primary-foreground: oklch(1 0 0);
|
|
33
39
|
--secondary: oklch(0.968 0.002 286.4);
|
|
34
40
|
--secondary-foreground: oklch(0.27 0.006 286);
|
|
@@ -40,7 +46,7 @@
|
|
|
40
46
|
|
|
41
47
|
--border: oklch(0.918 0.004 286.3);
|
|
42
48
|
--input: oklch(0.918 0.004 286.3);
|
|
43
|
-
--ring:
|
|
49
|
+
--ring: var(--brand);
|
|
44
50
|
|
|
45
51
|
/* Surfaces above the page: sidebar, popovers, hover rows. */
|
|
46
52
|
--surface-1: oklch(0.985 0.001 286.4);
|
|
@@ -51,6 +57,7 @@
|
|
|
51
57
|
--stage-lead: oklch(0.62 0.03 264);
|
|
52
58
|
--stage-qualified: oklch(0.6 0.13 240);
|
|
53
59
|
--stage-proposal: oklch(0.68 0.15 78);
|
|
60
|
+
--stage-negotiation: oklch(0.64 0.16 45);
|
|
54
61
|
--stage-won: oklch(0.6 0.15 155);
|
|
55
62
|
--stage-lost: oklch(0.6 0.11 25);
|
|
56
63
|
}
|
|
@@ -63,7 +70,7 @@
|
|
|
63
70
|
--popover: oklch(0.216 0.006 285.9);
|
|
64
71
|
--popover-foreground: var(--foreground);
|
|
65
72
|
|
|
66
|
-
--primary:
|
|
73
|
+
--primary: var(--brand-dark);
|
|
67
74
|
--primary-foreground: oklch(1 0 0);
|
|
68
75
|
--secondary: oklch(0.25 0.006 286);
|
|
69
76
|
--secondary-foreground: oklch(0.93 0.002 286.3);
|
|
@@ -75,7 +82,7 @@
|
|
|
75
82
|
|
|
76
83
|
--border: oklch(1 0 0 / 9%);
|
|
77
84
|
--input: oklch(1 0 0 / 12%);
|
|
78
|
-
--ring:
|
|
85
|
+
--ring: var(--brand-dark);
|
|
79
86
|
|
|
80
87
|
--surface-1: oklch(0.186 0.004 285.9);
|
|
81
88
|
--surface-2: oklch(0.216 0.006 285.9);
|
|
@@ -83,6 +90,7 @@
|
|
|
83
90
|
--stage-lead: oklch(0.68 0.03 264);
|
|
84
91
|
--stage-qualified: oklch(0.68 0.13 240);
|
|
85
92
|
--stage-proposal: oklch(0.75 0.14 78);
|
|
93
|
+
--stage-negotiation: oklch(0.72 0.15 45);
|
|
86
94
|
--stage-won: oklch(0.7 0.15 155);
|
|
87
95
|
--stage-lost: oklch(0.68 0.14 25);
|
|
88
96
|
}
|
|
@@ -118,6 +126,7 @@
|
|
|
118
126
|
--color-stage-lead: var(--stage-lead);
|
|
119
127
|
--color-stage-qualified: var(--stage-qualified);
|
|
120
128
|
--color-stage-proposal: var(--stage-proposal);
|
|
129
|
+
--color-stage-negotiation: var(--stage-negotiation);
|
|
121
130
|
--color-stage-won: var(--stage-won);
|
|
122
131
|
--color-stage-lost: var(--stage-lost);
|
|
123
132
|
}
|
|
@@ -143,15 +152,21 @@
|
|
|
143
152
|
height: 100%;
|
|
144
153
|
background-color: var(--color-background);
|
|
145
154
|
color: var(--color-foreground);
|
|
146
|
-
/*
|
|
147
|
-
|
|
148
|
-
font-size:
|
|
155
|
+
/* 14px on phones, 13px from the md breakpoint up: the density these
|
|
156
|
+
screens are designed at on a desktop. */
|
|
157
|
+
font-size: 14px;
|
|
149
158
|
line-height: 1.5;
|
|
150
|
-
letter-spacing: -0.
|
|
159
|
+
letter-spacing: -0.003em;
|
|
151
160
|
-webkit-font-smoothing: antialiased;
|
|
152
161
|
text-rendering: optimizeLegibility;
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
@media (min-width: 768px) {
|
|
165
|
+
body {
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
button {
|
|
156
171
|
cursor: pointer;
|
|
157
172
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#c54a05"/><g fill="#fff"><rect x="6" y="6" width="5.6" height="20" rx="1.8"/><rect x="13.2" y="6" width="5.6" height="14.6" rx="1.8"/><rect x="20.4" y="6" width="5.6" height="9.2" rx="1.8"/></g></svg>
|
|
@@ -18,12 +18,14 @@ export default function RootLayout({ children }: LayoutProps) {
|
|
|
18
18
|
<html lang="en">
|
|
19
19
|
<head>
|
|
20
20
|
<meta charSet="utf-8" />
|
|
21
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
-
<title>
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
22
|
+
{/* No <title> here: each page's `metadata` sets "<Page> · <Brand>".
|
|
23
|
+
A title in the layout renders first and wins over the page's. */}
|
|
23
24
|
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
24
|
-
stylesheet link is injected here automatically.
|
|
25
|
-
app.ts (fonts: [...]) and self-hosted by the build
|
|
26
|
-
preload link, and a size-adjusted fallback are
|
|
25
|
+
stylesheet link is injected here automatically. The typeface is
|
|
26
|
+
declared in app.ts (fonts: [...]) and self-hosted by the build;
|
|
27
|
+
@font-face, the preload link, and a size-adjusted fallback are
|
|
28
|
+
injected too. */}
|
|
27
29
|
</head>
|
|
28
30
|
<body className="h-full">{children}</body>
|
|
29
31
|
</html>
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { BRAND } from "@/lib/brand";
|
|
3
4
|
import { ProjectsView } from "./projects-view";
|
|
4
5
|
|
|
5
6
|
export const metadata: Metadata = {
|
|
6
|
-
title:
|
|
7
|
+
title: `Projects · ${BRAND.name}`,
|
|
7
8
|
robots: "noindex",
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
* `/` — the project list.
|
|
12
|
-
*
|
|
13
|
-
*/
|
|
11
|
+
/** `/` — the project list. */
|
|
14
12
|
export default function ProjectsPage({ searchParams }: PageProps) {
|
|
15
13
|
return (
|
|
16
14
|
<ProjectsView openNew={searchParams?.new === "project"} />
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { BRAND } from "@/lib/brand";
|
|
3
4
|
import { ProjectView } from "./project-view";
|
|
4
5
|
|
|
5
6
|
export const metadata: Metadata = {
|
|
6
|
-
title:
|
|
7
|
+
title: `Project · ${BRAND.name}`,
|
|
7
8
|
robots: "noindex",
|
|
8
9
|
};
|
|
9
10
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { Link, callFn, db, useRouter } from "@pylonsync/react";
|
|
5
|
-
import {
|
|
5
|
+
import { ChevronRight, Clock, Plus } from "lucide-react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Input } from "@/components/ui/input";
|
|
8
8
|
import { Select } from "@/components/ui/select";
|
|
@@ -11,8 +11,9 @@ import { EmptyState } from "@/components/empty-state";
|
|
|
11
11
|
import { TaskBoard } from "@/components/task-board";
|
|
12
12
|
import { BoardSkeleton } from "@/components/board-skeleton";
|
|
13
13
|
import { BudgetBar } from "@/components/budget-bar";
|
|
14
|
+
import { Meter } from "@/components/meter";
|
|
14
15
|
import { TimeDialog } from "@/components/time-dialog";
|
|
15
|
-
import { PROJECT_STATUSES,
|
|
16
|
+
import { PROJECT_STATUSES, minutesForProject, progress } from "@/lib/work";
|
|
16
17
|
import { RequireAuth } from "@/components/require-auth";
|
|
17
18
|
import { Workspace } from "../../workspace";
|
|
18
19
|
|
|
@@ -26,7 +27,7 @@ export function ProjectView({
|
|
|
26
27
|
const [timeFor, setTimeFor] = useState<{ id: string; title: string } | null>(null);
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
|
-
<RequireAuth
|
|
30
|
+
<RequireAuth>
|
|
30
31
|
<Workspace pathname="/">
|
|
31
32
|
{(data) => {
|
|
32
33
|
const project = data.projects.find((p) => p.id === projectId);
|
|
@@ -36,7 +37,7 @@ export function ProjectView({
|
|
|
36
37
|
<>
|
|
37
38
|
<PageHeader title="Project" />
|
|
38
39
|
<EmptyState
|
|
39
|
-
title={data.loading ? "Loading…" : "Project not found"}
|
|
40
|
+
title={data.loading ? "Loading project…" : "Project not found"}
|
|
40
41
|
description={
|
|
41
42
|
data.loading ? undefined : "It may have been deleted, or the link is wrong."
|
|
42
43
|
}
|
|
@@ -74,15 +75,23 @@ export function ProjectView({
|
|
|
74
75
|
|
|
75
76
|
return (
|
|
76
77
|
<>
|
|
77
|
-
<PageHeader
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
<PageHeader
|
|
79
|
+
title={project.name}
|
|
80
|
+
leading={
|
|
81
|
+
<Link
|
|
82
|
+
href="/"
|
|
83
|
+
className="hidden items-center gap-1 text-[13px] text-muted-foreground transition-colors hover:text-foreground md:inline-flex"
|
|
84
|
+
>
|
|
85
|
+
Projects
|
|
86
|
+
<ChevronRight className="size-3.5" />
|
|
87
|
+
</Link>
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
<div className="w-28 md:w-32">
|
|
82
91
|
<Select
|
|
83
92
|
aria-label="Project status"
|
|
84
93
|
value={project.status}
|
|
85
|
-
className="h-
|
|
94
|
+
className="h-8 md:h-7.5"
|
|
86
95
|
onChange={(event) =>
|
|
87
96
|
void db.update("Project", project.id, {
|
|
88
97
|
status: event.target.value,
|
|
@@ -98,34 +107,36 @@ export function ProjectView({
|
|
|
98
107
|
</div>
|
|
99
108
|
</PageHeader>
|
|
100
109
|
|
|
101
|
-
<div className="
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<BudgetBar
|
|
114
|
-
budgetMinutes={project.budgetMinutes}
|
|
115
|
-
loggedMinutes={logged}
|
|
116
|
-
hourlyRateCents={project.hourlyRateCents}
|
|
110
|
+
<div className="grid shrink-0 gap-x-8 gap-y-3 border-b border-border px-4 py-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] md:items-center md:px-5">
|
|
111
|
+
<div className="min-w-0">
|
|
112
|
+
<p className="truncate text-[12.5px] text-muted-foreground">
|
|
113
|
+
{data.clientName(project.clientId) ?? "No client"}
|
|
114
|
+
{project.dueDate
|
|
115
|
+
? ` · due ${new Date(project.dueDate).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
|
|
116
|
+
: ""}
|
|
117
|
+
</p>
|
|
118
|
+
<Meter
|
|
119
|
+
className="mt-1.5"
|
|
120
|
+
ratio={p.ratio ?? 0}
|
|
121
|
+
label={`${p.done}/${p.total} tasks done`}
|
|
117
122
|
/>
|
|
118
123
|
</div>
|
|
119
|
-
<
|
|
124
|
+
<BudgetBar
|
|
125
|
+
budgetMinutes={project.budgetMinutes}
|
|
126
|
+
loggedMinutes={logged}
|
|
127
|
+
hourlyRateCents={project.hourlyRateCents}
|
|
128
|
+
/>
|
|
129
|
+
<form onSubmit={addTask} className="flex items-center gap-2">
|
|
120
130
|
<Input
|
|
121
131
|
aria-label="New task"
|
|
122
132
|
placeholder="Add a task…"
|
|
123
133
|
value={newTask}
|
|
124
134
|
onChange={(event) => setNewTask(event.target.value)}
|
|
125
|
-
className="h-
|
|
135
|
+
className="h-9 flex-1 md:h-8 md:w-52"
|
|
126
136
|
/>
|
|
127
|
-
<Button type="submit" size="sm" className="h-
|
|
137
|
+
<Button type="submit" size="sm" className="h-9 md:h-8" disabled={!newTask.trim()}>
|
|
128
138
|
<Plus />
|
|
139
|
+
<span className="sr-only">Add task</span>
|
|
129
140
|
</Button>
|
|
130
141
|
</form>
|
|
131
142
|
</div>
|
|
@@ -146,7 +157,7 @@ export function ProjectView({
|
|
|
146
157
|
minutesFor={(taskId) => data.taskMinutes.get(taskId) ?? 0}
|
|
147
158
|
onMove={(taskId, status) => {
|
|
148
159
|
// Through the mutation: the new position is computed
|
|
149
|
-
// server-side so two simultaneous drags don
|
|
160
|
+
// server-side so two simultaneous drags don't collide.
|
|
150
161
|
void callFn("moveTask", { taskId, status });
|
|
151
162
|
}}
|
|
152
163
|
onOpen={(taskId) => {
|
|
@@ -160,6 +171,10 @@ export function ProjectView({
|
|
|
160
171
|
<TimeDialog
|
|
161
172
|
open={timeFor !== null}
|
|
162
173
|
taskTitle={timeFor?.title ?? ""}
|
|
174
|
+
status={tasks.find((t) => t.id === timeFor?.id)?.status}
|
|
175
|
+
onMove={(status) => {
|
|
176
|
+
if (timeFor) void callFn("moveTask", { taskId: timeFor.id, status });
|
|
177
|
+
}}
|
|
163
178
|
onOpenChange={(open) => {
|
|
164
179
|
if (!open) setTimeFor(null);
|
|
165
180
|
}}
|