@pylonsync/create-pylon 0.3.343 → 0.3.345
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 +35 -0
- package/package.json +1 -1
- package/templates/agency/components/ui/badge.tsx +37 -0
- package/templates/agency/components/ui/dialog.tsx +110 -0
- package/templates/agency/components/ui/input.tsx +30 -0
- package/templates/agency/components/ui/label.tsx +21 -0
- package/templates/agency/components/ui/select.tsx +39 -0
- package/templates/agency/components/ui/table.tsx +124 -0
- package/templates/agency/components/ui/textarea.tsx +24 -0
- package/templates/agency/package.json +1 -0
- package/templates/ai-chat/components/ui/badge.tsx +37 -0
- package/templates/ai-chat/components/ui/button.tsx +56 -0
- package/templates/ai-chat/components/ui/card.tsx +90 -0
- package/templates/ai-chat/components/ui/dialog.tsx +110 -0
- package/templates/ai-chat/components/ui/input.tsx +30 -0
- package/templates/ai-chat/components/ui/label.tsx +21 -0
- package/templates/ai-chat/components/ui/select.tsx +39 -0
- package/templates/ai-chat/components/ui/table.tsx +124 -0
- package/templates/ai-chat/components/ui/textarea.tsx +24 -0
- package/templates/ai-chat/package.json +1 -0
- package/templates/ai-studio/components/ui/badge.tsx +37 -0
- package/templates/ai-studio/components/ui/button.tsx +56 -0
- package/templates/ai-studio/components/ui/card.tsx +90 -0
- package/templates/ai-studio/components/ui/dialog.tsx +110 -0
- package/templates/ai-studio/components/ui/input.tsx +30 -0
- package/templates/ai-studio/components/ui/label.tsx +21 -0
- package/templates/ai-studio/components/ui/select.tsx +39 -0
- package/templates/ai-studio/components/ui/table.tsx +124 -0
- package/templates/ai-studio/components/ui/textarea.tsx +24 -0
- package/templates/ai-studio/package.json +1 -0
- package/templates/barebones/components/ui/badge.tsx +37 -0
- package/templates/barebones/components/ui/dialog.tsx +110 -0
- package/templates/barebones/components/ui/input.tsx +30 -0
- package/templates/barebones/components/ui/label.tsx +21 -0
- package/templates/barebones/components/ui/select.tsx +39 -0
- package/templates/barebones/components/ui/table.tsx +124 -0
- package/templates/barebones/components/ui/textarea.tsx +24 -0
- package/templates/barebones/package.json +1 -0
- package/templates/chat/components/ui/badge.tsx +37 -0
- package/templates/chat/components/ui/dialog.tsx +110 -0
- package/templates/chat/components/ui/input.tsx +30 -0
- package/templates/chat/components/ui/label.tsx +21 -0
- package/templates/chat/components/ui/select.tsx +39 -0
- package/templates/chat/components/ui/table.tsx +124 -0
- package/templates/chat/components/ui/textarea.tsx +24 -0
- package/templates/chat/package.json +1 -0
- package/templates/consumer/components/ui/badge.tsx +37 -0
- package/templates/consumer/components/ui/dialog.tsx +110 -0
- package/templates/consumer/components/ui/input.tsx +30 -0
- package/templates/consumer/components/ui/label.tsx +21 -0
- package/templates/consumer/components/ui/select.tsx +39 -0
- package/templates/consumer/components/ui/table.tsx +124 -0
- package/templates/consumer/components/ui/textarea.tsx +24 -0
- package/templates/consumer/package.json +1 -0
- package/templates/creator/components/ui/badge.tsx +37 -0
- package/templates/creator/components/ui/dialog.tsx +110 -0
- package/templates/creator/components/ui/input.tsx +30 -0
- package/templates/creator/components/ui/label.tsx +21 -0
- package/templates/creator/components/ui/select.tsx +39 -0
- package/templates/creator/components/ui/table.tsx +124 -0
- package/templates/creator/components/ui/textarea.tsx +24 -0
- package/templates/creator/package.json +1 -0
- package/templates/crm/AGENTS.md +116 -0
- package/templates/crm/CLAUDE.md +7 -0
- package/templates/crm/README.md +96 -0
- package/templates/crm/app/companies/companies-view.tsx +129 -0
- package/templates/crm/app/companies/page.tsx +17 -0
- package/templates/crm/app/contacts/contacts-view.tsx +139 -0
- package/templates/crm/app/contacts/page.tsx +17 -0
- package/templates/crm/app/deals/[id]/deal-view.tsx +189 -0
- package/templates/crm/app/deals/[id]/page.tsx +23 -0
- package/templates/crm/app/error.tsx +43 -0
- package/templates/crm/app/globals.css +201 -0
- package/templates/crm/app/layout.tsx +31 -0
- package/templates/crm/app/login/auth-form.tsx +122 -0
- package/templates/crm/app/login/page.tsx +38 -0
- package/templates/crm/app/not-found.tsx +29 -0
- package/templates/crm/app/page.tsx +38 -0
- package/templates/crm/app/pipeline-view.tsx +99 -0
- package/templates/crm/app/robots.ts +12 -0
- package/templates/crm/app/sitemap.ts +27 -0
- package/templates/crm/app/workspace.tsx +231 -0
- package/templates/crm/app.ts +171 -0
- package/templates/crm/bunfig +4 -0
- package/templates/crm/components/activity-timeline.tsx +133 -0
- package/templates/crm/components/avatar.tsx +40 -0
- package/templates/crm/components/board-skeleton.tsx +44 -0
- package/templates/crm/components/command-palette.tsx +162 -0
- package/templates/crm/components/data-table.tsx +90 -0
- package/templates/crm/components/deal-card.tsx +91 -0
- package/templates/crm/components/deal-dialog.tsx +169 -0
- package/templates/crm/components/empty-state.tsx +43 -0
- package/templates/crm/components/kbd.tsx +23 -0
- package/templates/crm/components/metrics-bar.tsx +46 -0
- package/templates/crm/components/page-header.tsx +36 -0
- package/templates/crm/components/pipeline-board.tsx +105 -0
- package/templates/crm/components/record-dialog.tsx +126 -0
- package/templates/crm/components/sidebar.tsx +112 -0
- package/templates/crm/components/stage-badge.tsx +33 -0
- package/templates/crm/components/ui/badge.tsx +37 -0
- package/templates/crm/components/ui/button.tsx +56 -0
- package/templates/crm/components/ui/card.tsx +90 -0
- package/templates/crm/components/ui/dialog.tsx +110 -0
- package/templates/crm/components/ui/input.tsx +30 -0
- package/templates/crm/components/ui/label.tsx +21 -0
- package/templates/crm/components/ui/select.tsx +39 -0
- package/templates/crm/components/ui/table.tsx +124 -0
- package/templates/crm/components/ui/textarea.tsx +24 -0
- package/templates/crm/components.json +20 -0
- package/templates/crm/functions/createDeal.ts +59 -0
- package/templates/crm/functions/logActivity.ts +49 -0
- package/templates/crm/functions/moveDeal.ts +53 -0
- package/templates/crm/functions/seedWorkspace.ts +63 -0
- package/templates/crm/gitignore +10 -0
- package/templates/crm/lib/pipeline.ts +221 -0
- package/templates/crm/lib/search.ts +78 -0
- package/templates/crm/lib/seed.ts +130 -0
- package/templates/crm/lib/utils.ts +10 -0
- package/templates/crm/package.json +39 -0
- package/templates/crm/tests/components.test.tsx +245 -0
- package/templates/crm/tests/pipeline.test.ts +219 -0
- package/templates/crm/tests/search.test.ts +83 -0
- package/templates/crm/tests/setup.ts +5 -0
- package/templates/crm/tsconfig.json +33 -0
- package/templates/default/components/ui/badge.tsx +37 -0
- package/templates/default/components/ui/dialog.tsx +110 -0
- package/templates/default/components/ui/input.tsx +30 -0
- package/templates/default/components/ui/label.tsx +21 -0
- package/templates/default/components/ui/select.tsx +39 -0
- package/templates/default/components/ui/table.tsx +124 -0
- package/templates/default/components/ui/textarea.tsx +24 -0
- package/templates/default/package.json +1 -0
- package/templates/directory/components/ui/badge.tsx +37 -0
- package/templates/directory/components/ui/dialog.tsx +110 -0
- package/templates/directory/components/ui/input.tsx +30 -0
- package/templates/directory/components/ui/label.tsx +21 -0
- package/templates/directory/components/ui/select.tsx +39 -0
- package/templates/directory/components/ui/table.tsx +124 -0
- package/templates/directory/components/ui/textarea.tsx +24 -0
- package/templates/directory/package.json +1 -0
- package/templates/helpdesk/AGENTS.md +116 -0
- package/templates/helpdesk/CLAUDE.md +7 -0
- package/templates/helpdesk/README.md +90 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +140 -0
- package/templates/helpdesk/app/customers/page.tsx +17 -0
- package/templates/helpdesk/app/error.tsx +43 -0
- package/templates/helpdesk/app/globals.css +201 -0
- package/templates/helpdesk/app/inbox-view.tsx +147 -0
- package/templates/helpdesk/app/layout.tsx +31 -0
- package/templates/helpdesk/app/login/auth-form.tsx +122 -0
- package/templates/helpdesk/app/login/page.tsx +38 -0
- package/templates/helpdesk/app/not-found.tsx +29 -0
- package/templates/helpdesk/app/page.tsx +38 -0
- package/templates/helpdesk/app/robots.ts +12 -0
- package/templates/helpdesk/app/sitemap.ts +27 -0
- package/templates/helpdesk/app/tickets/[id]/page.tsx +23 -0
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +164 -0
- package/templates/helpdesk/app/workspace.tsx +210 -0
- package/templates/helpdesk/app.ts +142 -0
- package/templates/helpdesk/bunfig +4 -0
- package/templates/helpdesk/components/avatar.tsx +40 -0
- package/templates/helpdesk/components/command-palette.tsx +162 -0
- package/templates/helpdesk/components/data-table.tsx +90 -0
- package/templates/helpdesk/components/empty-state.tsx +43 -0
- package/templates/helpdesk/components/kbd.tsx +23 -0
- package/templates/helpdesk/components/new-ticket-dialog.tsx +147 -0
- package/templates/helpdesk/components/page-header.tsx +36 -0
- package/templates/helpdesk/components/priority-badge.tsx +61 -0
- package/templates/helpdesk/components/record-dialog.tsx +126 -0
- package/templates/helpdesk/components/sidebar.tsx +111 -0
- package/templates/helpdesk/components/sla-indicator.tsx +47 -0
- package/templates/helpdesk/components/thread.tsx +143 -0
- package/templates/helpdesk/components/ticket-list.tsx +78 -0
- package/templates/helpdesk/components/ui/badge.tsx +37 -0
- package/templates/helpdesk/components/ui/button.tsx +56 -0
- package/templates/helpdesk/components/ui/card.tsx +90 -0
- package/templates/helpdesk/components/ui/dialog.tsx +110 -0
- package/templates/helpdesk/components/ui/input.tsx +30 -0
- package/templates/helpdesk/components/ui/label.tsx +21 -0
- package/templates/helpdesk/components/ui/select.tsx +39 -0
- package/templates/helpdesk/components/ui/table.tsx +124 -0
- package/templates/helpdesk/components/ui/textarea.tsx +24 -0
- package/templates/helpdesk/components.json +20 -0
- package/templates/helpdesk/functions/createTicket.ts +62 -0
- package/templates/helpdesk/functions/replyToTicket.ts +55 -0
- package/templates/helpdesk/functions/seedWorkspace.ts +52 -0
- package/templates/helpdesk/functions/setTicketState.ts +58 -0
- package/templates/helpdesk/gitignore +10 -0
- package/templates/helpdesk/lib/format.ts +53 -0
- package/templates/helpdesk/lib/search.ts +78 -0
- package/templates/helpdesk/lib/seed.ts +185 -0
- package/templates/helpdesk/lib/tickets.ts +184 -0
- package/templates/helpdesk/lib/utils.ts +10 -0
- package/templates/helpdesk/package.json +39 -0
- package/templates/helpdesk/tests/components.test.tsx +202 -0
- package/templates/helpdesk/tests/search.test.ts +83 -0
- package/templates/helpdesk/tests/setup.ts +5 -0
- package/templates/helpdesk/tests/tickets.test.ts +215 -0
- package/templates/helpdesk/tsconfig.json +33 -0
- package/templates/inventory/AGENTS.md +116 -0
- package/templates/inventory/CLAUDE.md +7 -0
- package/templates/inventory/README.md +103 -0
- package/templates/inventory/app/error.tsx +43 -0
- package/templates/inventory/app/globals.css +201 -0
- package/templates/inventory/app/layout.tsx +31 -0
- package/templates/inventory/app/login/auth-form.tsx +122 -0
- package/templates/inventory/app/login/page.tsx +38 -0
- package/templates/inventory/app/movements/movements-view.tsx +103 -0
- package/templates/inventory/app/movements/page.tsx +17 -0
- package/templates/inventory/app/not-found.tsx +29 -0
- package/templates/inventory/app/page.tsx +34 -0
- package/templates/inventory/app/products/[id]/page.tsx +23 -0
- package/templates/inventory/app/products/[id]/product-view.tsx +178 -0
- package/templates/inventory/app/products-view.tsx +213 -0
- package/templates/inventory/app/robots.ts +12 -0
- package/templates/inventory/app/sitemap.ts +27 -0
- package/templates/inventory/app/workspace.tsx +172 -0
- package/templates/inventory/app.ts +154 -0
- package/templates/inventory/bunfig +4 -0
- package/templates/inventory/components/avatar.tsx +40 -0
- package/templates/inventory/components/command-palette.tsx +162 -0
- package/templates/inventory/components/data-table.tsx +90 -0
- package/templates/inventory/components/empty-state.tsx +43 -0
- package/templates/inventory/components/kbd.tsx +23 -0
- package/templates/inventory/components/movement-dialog.tsx +182 -0
- package/templates/inventory/components/page-header.tsx +36 -0
- package/templates/inventory/components/record-dialog.tsx +126 -0
- package/templates/inventory/components/sidebar.tsx +111 -0
- package/templates/inventory/components/stock-level.tsx +47 -0
- package/templates/inventory/components/summary-bar.tsx +56 -0
- package/templates/inventory/components/ui/badge.tsx +37 -0
- package/templates/inventory/components/ui/button.tsx +56 -0
- package/templates/inventory/components/ui/card.tsx +90 -0
- package/templates/inventory/components/ui/dialog.tsx +110 -0
- package/templates/inventory/components/ui/input.tsx +30 -0
- package/templates/inventory/components/ui/label.tsx +21 -0
- package/templates/inventory/components/ui/select.tsx +39 -0
- package/templates/inventory/components/ui/table.tsx +124 -0
- package/templates/inventory/components/ui/textarea.tsx +24 -0
- package/templates/inventory/components.json +20 -0
- package/templates/inventory/functions/recordMovement.ts +72 -0
- package/templates/inventory/functions/seedWorkspace.ts +40 -0
- package/templates/inventory/gitignore +10 -0
- package/templates/inventory/lib/format.ts +53 -0
- package/templates/inventory/lib/search.ts +78 -0
- package/templates/inventory/lib/seed.ts +102 -0
- package/templates/inventory/lib/stock.ts +207 -0
- package/templates/inventory/lib/utils.ts +10 -0
- package/templates/inventory/package.json +39 -0
- package/templates/inventory/tests/components.test.tsx +155 -0
- package/templates/inventory/tests/search.test.ts +83 -0
- package/templates/inventory/tests/setup.ts +5 -0
- package/templates/inventory/tests/stock.test.ts +255 -0
- package/templates/inventory/tsconfig.json +33 -0
- package/templates/invoices/AGENTS.md +116 -0
- package/templates/invoices/CLAUDE.md +7 -0
- package/templates/invoices/README.md +99 -0
- package/templates/invoices/app/clients/clients-view.tsx +130 -0
- package/templates/invoices/app/clients/page.tsx +17 -0
- package/templates/invoices/app/error.tsx +43 -0
- package/templates/invoices/app/globals.css +201 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +253 -0
- package/templates/invoices/app/invoices/[id]/page.tsx +23 -0
- package/templates/invoices/app/invoices-view.tsx +183 -0
- package/templates/invoices/app/layout.tsx +31 -0
- package/templates/invoices/app/login/auth-form.tsx +122 -0
- package/templates/invoices/app/login/page.tsx +38 -0
- package/templates/invoices/app/not-found.tsx +29 -0
- package/templates/invoices/app/page.tsx +36 -0
- package/templates/invoices/app/robots.ts +12 -0
- package/templates/invoices/app/sitemap.ts +27 -0
- package/templates/invoices/app/workspace.tsx +174 -0
- package/templates/invoices/app.ts +169 -0
- package/templates/invoices/bunfig +4 -0
- package/templates/invoices/components/avatar.tsx +40 -0
- package/templates/invoices/components/command-palette.tsx +162 -0
- package/templates/invoices/components/data-table.tsx +90 -0
- package/templates/invoices/components/empty-state.tsx +43 -0
- package/templates/invoices/components/kbd.tsx +23 -0
- package/templates/invoices/components/line-items.tsx +172 -0
- package/templates/invoices/components/page-header.tsx +36 -0
- package/templates/invoices/components/payment-dialog.tsx +132 -0
- package/templates/invoices/components/record-dialog.tsx +126 -0
- package/templates/invoices/components/sidebar.tsx +111 -0
- package/templates/invoices/components/status-badge.tsx +35 -0
- package/templates/invoices/components/summary-bar.tsx +65 -0
- package/templates/invoices/components/totals-panel.tsx +70 -0
- package/templates/invoices/components/ui/badge.tsx +37 -0
- package/templates/invoices/components/ui/button.tsx +56 -0
- package/templates/invoices/components/ui/card.tsx +90 -0
- package/templates/invoices/components/ui/dialog.tsx +110 -0
- package/templates/invoices/components/ui/input.tsx +30 -0
- package/templates/invoices/components/ui/label.tsx +21 -0
- package/templates/invoices/components/ui/select.tsx +39 -0
- package/templates/invoices/components/ui/table.tsx +124 -0
- package/templates/invoices/components/ui/textarea.tsx +24 -0
- package/templates/invoices/components.json +20 -0
- package/templates/invoices/functions/createInvoice.ts +49 -0
- package/templates/invoices/functions/recordPayment.ts +72 -0
- package/templates/invoices/functions/seedWorkspace.ts +57 -0
- package/templates/invoices/functions/setInvoiceStatus.ts +35 -0
- package/templates/invoices/gitignore +10 -0
- package/templates/invoices/lib/billing.ts +259 -0
- package/templates/invoices/lib/format.ts +53 -0
- package/templates/invoices/lib/search.ts +78 -0
- package/templates/invoices/lib/seed.ts +190 -0
- package/templates/invoices/lib/utils.ts +10 -0
- package/templates/invoices/package.json +39 -0
- package/templates/invoices/tests/billing.test.ts +283 -0
- package/templates/invoices/tests/components.test.tsx +172 -0
- package/templates/invoices/tests/search.test.ts +83 -0
- package/templates/invoices/tests/setup.ts +5 -0
- package/templates/invoices/tsconfig.json +33 -0
- package/templates/local-service/components/ui/badge.tsx +37 -0
- package/templates/local-service/components/ui/dialog.tsx +110 -0
- package/templates/local-service/components/ui/input.tsx +30 -0
- package/templates/local-service/components/ui/label.tsx +21 -0
- package/templates/local-service/components/ui/select.tsx +39 -0
- package/templates/local-service/components/ui/table.tsx +124 -0
- package/templates/local-service/components/ui/textarea.tsx +24 -0
- package/templates/local-service/package.json +1 -0
- package/templates/marketplace/components/ui/badge.tsx +37 -0
- package/templates/marketplace/components/ui/button.tsx +56 -0
- package/templates/marketplace/components/ui/card.tsx +90 -0
- package/templates/marketplace/components/ui/dialog.tsx +110 -0
- package/templates/marketplace/components/ui/input.tsx +30 -0
- package/templates/marketplace/components/ui/label.tsx +21 -0
- package/templates/marketplace/components/ui/select.tsx +39 -0
- package/templates/marketplace/components/ui/table.tsx +124 -0
- package/templates/marketplace/components/ui/textarea.tsx +24 -0
- package/templates/marketplace/lib/utils.ts +10 -0
- package/templates/marketplace/package.json +1 -0
- package/templates/projects/AGENTS.md +116 -0
- package/templates/projects/CLAUDE.md +7 -0
- package/templates/projects/README.md +96 -0
- package/templates/projects/app/clients/clients-view.tsx +126 -0
- package/templates/projects/app/clients/page.tsx +17 -0
- package/templates/projects/app/error.tsx +43 -0
- package/templates/projects/app/globals.css +201 -0
- package/templates/projects/app/layout.tsx +31 -0
- package/templates/projects/app/login/auth-form.tsx +122 -0
- package/templates/projects/app/login/page.tsx +38 -0
- package/templates/projects/app/not-found.tsx +29 -0
- package/templates/projects/app/page.tsx +30 -0
- package/templates/projects/app/projects/[id]/page.tsx +23 -0
- package/templates/projects/app/projects/[id]/project-view.tsx +179 -0
- package/templates/projects/app/projects-view.tsx +181 -0
- package/templates/projects/app/robots.ts +12 -0
- package/templates/projects/app/sitemap.ts +27 -0
- package/templates/projects/app/workspace.tsx +190 -0
- package/templates/projects/app.ts +186 -0
- package/templates/projects/bunfig +4 -0
- package/templates/projects/components/avatar.tsx +40 -0
- package/templates/projects/components/board-skeleton.tsx +37 -0
- package/templates/projects/components/budget-bar.tsx +61 -0
- package/templates/projects/components/command-palette.tsx +162 -0
- package/templates/projects/components/data-table.tsx +90 -0
- package/templates/projects/components/empty-state.tsx +43 -0
- package/templates/projects/components/kbd.tsx +23 -0
- package/templates/projects/components/page-header.tsx +36 -0
- package/templates/projects/components/record-dialog.tsx +126 -0
- package/templates/projects/components/sidebar.tsx +111 -0
- package/templates/projects/components/task-board.tsx +109 -0
- package/templates/projects/components/task-card.tsx +77 -0
- package/templates/projects/components/time-dialog.tsx +114 -0
- package/templates/projects/components/ui/badge.tsx +37 -0
- package/templates/projects/components/ui/button.tsx +56 -0
- package/templates/projects/components/ui/card.tsx +90 -0
- package/templates/projects/components/ui/dialog.tsx +110 -0
- package/templates/projects/components/ui/input.tsx +30 -0
- package/templates/projects/components/ui/label.tsx +21 -0
- package/templates/projects/components/ui/select.tsx +39 -0
- package/templates/projects/components/ui/table.tsx +124 -0
- package/templates/projects/components/ui/textarea.tsx +24 -0
- package/templates/projects/components.json +20 -0
- package/templates/projects/functions/logTime.ts +49 -0
- package/templates/projects/functions/moveTask.ts +40 -0
- package/templates/projects/functions/seedWorkspace.ts +59 -0
- package/templates/projects/gitignore +10 -0
- package/templates/projects/lib/format.ts +53 -0
- package/templates/projects/lib/search.ts +78 -0
- package/templates/projects/lib/seed.ts +140 -0
- package/templates/projects/lib/utils.ts +10 -0
- package/templates/projects/lib/work.ts +233 -0
- package/templates/projects/package.json +39 -0
- package/templates/projects/tests/components.test.tsx +200 -0
- package/templates/projects/tests/search.test.ts +83 -0
- package/templates/projects/tests/setup.ts +5 -0
- package/templates/projects/tests/work.test.ts +238 -0
- package/templates/projects/tsconfig.json +33 -0
- package/templates/restaurant/components/ui/badge.tsx +37 -0
- package/templates/restaurant/components/ui/dialog.tsx +110 -0
- package/templates/restaurant/components/ui/input.tsx +30 -0
- package/templates/restaurant/components/ui/label.tsx +21 -0
- package/templates/restaurant/components/ui/select.tsx +39 -0
- package/templates/restaurant/components/ui/table.tsx +124 -0
- package/templates/restaurant/components/ui/textarea.tsx +24 -0
- package/templates/restaurant/package.json +1 -0
- package/templates/shop/components/ui/badge.tsx +37 -0
- package/templates/shop/components/ui/dialog.tsx +110 -0
- package/templates/shop/components/ui/input.tsx +30 -0
- package/templates/shop/components/ui/label.tsx +21 -0
- package/templates/shop/components/ui/select.tsx +39 -0
- package/templates/shop/components/ui/table.tsx +124 -0
- package/templates/shop/components/ui/textarea.tsx +24 -0
- package/templates/shop/package.json +1 -0
- package/templates/todo/components/ui/badge.tsx +37 -0
- package/templates/todo/components/ui/dialog.tsx +110 -0
- package/templates/todo/components/ui/input.tsx +30 -0
- package/templates/todo/components/ui/label.tsx +21 -0
- package/templates/todo/components/ui/select.tsx +39 -0
- package/templates/todo/components/ui/table.tsx +124 -0
- package/templates/todo/components/ui/textarea.tsx +24 -0
- package/templates/todo/package.json +1 -0
- package/templates/waitlist/components/ui/badge.tsx +37 -0
- package/templates/waitlist/components/ui/dialog.tsx +110 -0
- package/templates/waitlist/components/ui/input.tsx +30 -0
- package/templates/waitlist/components/ui/label.tsx +21 -0
- package/templates/waitlist/components/ui/select.tsx +39 -0
- package/templates/waitlist/components/ui/table.tsx +124 -0
- package/templates/waitlist/components/ui/textarea.tsx +24 -0
- package/templates/waitlist/package.json +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A sales CRM for a small team — companies, contacts, and deals on a live
|
|
4
|
+
pipeline board. One [Pylon](https://pylonsync.com) app: SSR frontend, API, auth,
|
|
5
|
+
and realtime sync from one process on one port.
|
|
6
|
+
|
|
7
|
+
## Develop
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
__RUN_DEV__
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open http://localhost:4321 and create an account. The first sign-in seeds a demo
|
|
14
|
+
pipeline so the board isn't empty; delete `functions/seedWorkspace.ts` and
|
|
15
|
+
`lib/seed.ts` once you have real customers.
|
|
16
|
+
|
|
17
|
+
## What's interesting here
|
|
18
|
+
|
|
19
|
+
**The board is live.** Drag a deal to another column and it moves on every
|
|
20
|
+
teammate's board immediately — no refresh, no polling. Your own move takes the
|
|
21
|
+
same path as theirs: write → sync → re-render, so there's one behaviour to
|
|
22
|
+
reason about instead of two. Open the app in two windows and try it.
|
|
23
|
+
|
|
24
|
+
**⌘K searches everything.** Deals, companies, and contacts, ranked by how well
|
|
25
|
+
they match (`lib/search.ts`). It runs against the synced replica in memory, so
|
|
26
|
+
results appear as you type with no request per keystroke. `/` opens it too, and
|
|
27
|
+
`c` creates a deal.
|
|
28
|
+
|
|
29
|
+
**The forecast is derived, never stored.** Open pipeline, weighted value, and
|
|
30
|
+
win rate are computed from the deals themselves in `lib/pipeline.ts` — pure
|
|
31
|
+
functions with no React and no `db`, so the arithmetic is unit-tested directly
|
|
32
|
+
(`tests/pipeline.test.ts`).
|
|
33
|
+
|
|
34
|
+
## Layout
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
app.ts entities + policies
|
|
38
|
+
app/
|
|
39
|
+
page.tsx "/" — the pipeline board
|
|
40
|
+
pipeline-view.tsx board container
|
|
41
|
+
workspace.tsx app shell — the ONLY place that touches `db`
|
|
42
|
+
companies/, contacts/ list views
|
|
43
|
+
deals/[id]/ deal detail + activity timeline
|
|
44
|
+
login/ email/password auth
|
|
45
|
+
components/ presentational — props in, callbacks out
|
|
46
|
+
ui/ shadcn primitives
|
|
47
|
+
lib/pipeline.ts stages, money, metrics — pure
|
|
48
|
+
lib/search.ts ⌘K ranking — pure
|
|
49
|
+
functions/ server functions
|
|
50
|
+
tests/ pure logic + component tests
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The data boundary lives in exactly one module (`app/workspace.tsx`). Everything
|
|
54
|
+
under `components/` takes data as props and reports changes through callbacks,
|
|
55
|
+
which is why the component tests render them with fixtures and mock nothing.
|
|
56
|
+
|
|
57
|
+
## Access
|
|
58
|
+
|
|
59
|
+
Every entity is readable and writable by any **signed-in** user, and by nobody
|
|
60
|
+
else — a shared pipeline is the point of a team CRM. `ownerId` records who
|
|
61
|
+
created a row without partitioning access.
|
|
62
|
+
|
|
63
|
+
For per-rep isolation, tighten `allowRead` in `app.ts`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
allowRead: "auth.userId == data.ownerId"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Contacts hold customer email and phone. There is no public route in this app and
|
|
70
|
+
no policy allows an anonymous read — keep it that way if you add one.
|
|
71
|
+
|
|
72
|
+
## Theme
|
|
73
|
+
|
|
74
|
+
Dark by default (`class="dark"` in `app/layout.tsx`). The light tokens in
|
|
75
|
+
`app/globals.css` are complete, so removing that class flips the whole app. Both
|
|
76
|
+
use the standard shadcn variables, so `npx shadcn@latest add <component>` drops
|
|
77
|
+
in already themed.
|
|
78
|
+
|
|
79
|
+
## Grow it
|
|
80
|
+
|
|
81
|
+
- **Add a field:** edit an entity in `app.ts` — the typed client and
|
|
82
|
+
REST/realtime API follow, no migration.
|
|
83
|
+
- **Add a stage:** `PIPELINE` in `lib/pipeline.ts`. The board, the forecast, and
|
|
84
|
+
the stage picker all read from it.
|
|
85
|
+
- **Add a view:** drop `app/reports/page.tsx` and add it to `NAV` in
|
|
86
|
+
`components/sidebar.tsx`.
|
|
87
|
+
|
|
88
|
+
## Deploy
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pylon lint --strict
|
|
92
|
+
pylon test
|
|
93
|
+
pylon deploy
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Docs: https://docs.pylonsync.com
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { db } from "@pylonsync/react";
|
|
5
|
+
import { Building2, Plus } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { PageHeader } from "@/components/page-header";
|
|
8
|
+
import { DataTable, type ColumnDef } from "@/components/data-table";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { RecordDialog } from "@/components/record-dialog";
|
|
11
|
+
import { Avatar } from "@/components/avatar";
|
|
12
|
+
import { money, relativeTime } from "@/lib/pipeline";
|
|
13
|
+
import { Workspace, type CompanyRow, type DealRow } from "../workspace";
|
|
14
|
+
|
|
15
|
+
export function CompaniesView({ email }: { email: string }) {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Workspace email={email} pathname="/companies">
|
|
20
|
+
{(data) => {
|
|
21
|
+
// Pipeline per company, computed here rather than stored — one less
|
|
22
|
+
// thing to keep in sync when a deal moves.
|
|
23
|
+
const openValue = (companyId: string) =>
|
|
24
|
+
data.deals
|
|
25
|
+
.filter((deal: DealRow) => deal.companyId === companyId)
|
|
26
|
+
.reduce((sum, deal) => sum + (Number(deal.value) || 0), 0);
|
|
27
|
+
|
|
28
|
+
const columns: ColumnDef<CompanyRow>[] = [
|
|
29
|
+
{
|
|
30
|
+
key: "name",
|
|
31
|
+
header: "Company",
|
|
32
|
+
cell: (row) => (
|
|
33
|
+
<span className="flex items-center gap-2">
|
|
34
|
+
<Avatar name={row.name} size="sm" />
|
|
35
|
+
<span className="truncate font-medium">{row.name}</span>
|
|
36
|
+
</span>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "domain",
|
|
41
|
+
header: "Domain",
|
|
42
|
+
cell: (row) => (
|
|
43
|
+
<span className="text-muted-foreground">{row.domain ?? "—"}</span>
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "industry",
|
|
48
|
+
header: "Industry",
|
|
49
|
+
cell: (row) => (
|
|
50
|
+
<span className="text-muted-foreground">{row.industry ?? "—"}</span>
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: "size",
|
|
55
|
+
header: "Size",
|
|
56
|
+
cell: (row) => (
|
|
57
|
+
<span className="text-muted-foreground">{row.size ?? "—"}</span>
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "pipeline",
|
|
62
|
+
header: "Pipeline",
|
|
63
|
+
numeric: true,
|
|
64
|
+
cell: (row) => money(openValue(row.id)),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "created",
|
|
68
|
+
header: "Added",
|
|
69
|
+
cell: (row) => (
|
|
70
|
+
<span className="text-muted-foreground">
|
|
71
|
+
{relativeTime(row.createdAt)}
|
|
72
|
+
</span>
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const rows = [...data.companies].sort((a, b) => a.name.localeCompare(b.name));
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<PageHeader title="Companies" count={rows.length}>
|
|
82
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
83
|
+
<Plus />
|
|
84
|
+
New company
|
|
85
|
+
</Button>
|
|
86
|
+
</PageHeader>
|
|
87
|
+
|
|
88
|
+
<DataTable
|
|
89
|
+
rows={rows}
|
|
90
|
+
columns={columns}
|
|
91
|
+
empty={
|
|
92
|
+
<EmptyState
|
|
93
|
+
icon={<Building2 />}
|
|
94
|
+
title="No companies yet"
|
|
95
|
+
description="Companies are the accounts your deals and contacts hang off. Add the first one to start building a pipeline."
|
|
96
|
+
action={
|
|
97
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
98
|
+
<Plus />
|
|
99
|
+
New company
|
|
100
|
+
</Button>
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<RecordDialog
|
|
107
|
+
open={open}
|
|
108
|
+
title="New company"
|
|
109
|
+
submitLabel="Create company"
|
|
110
|
+
onOpenChange={setOpen}
|
|
111
|
+
fields={[
|
|
112
|
+
{ name: "name", label: "Name", required: true, placeholder: "Acme Inc" },
|
|
113
|
+
{ name: "domain", label: "Domain", placeholder: "acme.com" },
|
|
114
|
+
{ name: "industry", label: "Industry", placeholder: "Logistics" },
|
|
115
|
+
{ name: "size", label: "Size", placeholder: "50-200" },
|
|
116
|
+
]}
|
|
117
|
+
onCreate={async (values) => {
|
|
118
|
+
// A plain policy-checked insert — no server function needed,
|
|
119
|
+
// because there's nothing to validate beyond the field types
|
|
120
|
+
// and nothing to stamp that the policy doesn't already gate.
|
|
121
|
+
await db.insert("Company", values);
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
}}
|
|
127
|
+
</Workspace>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { CompaniesView } from "./companies-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Companies",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function CompaniesPage({ auth, response, serverData }: PageProps) {
|
|
11
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
12
|
+
response.redirect("/login");
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
16
|
+
return <CompaniesView email={me?.email ?? ""} />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { db } from "@pylonsync/react";
|
|
5
|
+
import { Plus, Users } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { PageHeader } from "@/components/page-header";
|
|
8
|
+
import { DataTable, type ColumnDef } from "@/components/data-table";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { RecordDialog } from "@/components/record-dialog";
|
|
11
|
+
import { Avatar } from "@/components/avatar";
|
|
12
|
+
import { relativeTime } from "@/lib/pipeline";
|
|
13
|
+
import { Workspace, type ContactRow } from "../workspace";
|
|
14
|
+
|
|
15
|
+
export function ContactsView({ email }: { email: string }) {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Workspace email={email} pathname="/contacts">
|
|
20
|
+
{(data) => {
|
|
21
|
+
const columns: ColumnDef<ContactRow>[] = [
|
|
22
|
+
{
|
|
23
|
+
key: "name",
|
|
24
|
+
header: "Name",
|
|
25
|
+
cell: (row) => (
|
|
26
|
+
<span className="flex items-center gap-2">
|
|
27
|
+
<Avatar name={row.name} size="sm" />
|
|
28
|
+
<span className="truncate font-medium">{row.name}</span>
|
|
29
|
+
</span>
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: "title",
|
|
34
|
+
header: "Title",
|
|
35
|
+
cell: (row) => (
|
|
36
|
+
<span className="text-muted-foreground">{row.title ?? "—"}</span>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "company",
|
|
41
|
+
header: "Company",
|
|
42
|
+
cell: (row) => (
|
|
43
|
+
<span className="text-muted-foreground">
|
|
44
|
+
{data.companyName(row.companyId) ?? "—"}
|
|
45
|
+
</span>
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "email",
|
|
50
|
+
header: "Email",
|
|
51
|
+
cell: (row) =>
|
|
52
|
+
row.email ? (
|
|
53
|
+
<a
|
|
54
|
+
href={`mailto:${row.email}`}
|
|
55
|
+
onClick={(event) => event.stopPropagation()}
|
|
56
|
+
className="text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
57
|
+
>
|
|
58
|
+
{row.email}
|
|
59
|
+
</a>
|
|
60
|
+
) : (
|
|
61
|
+
<span className="text-muted-foreground">—</span>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "phone",
|
|
66
|
+
header: "Phone",
|
|
67
|
+
cell: (row) => (
|
|
68
|
+
<span className="text-muted-foreground">{row.phone ?? "—"}</span>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "created",
|
|
73
|
+
header: "Added",
|
|
74
|
+
cell: (row) => (
|
|
75
|
+
<span className="text-muted-foreground">
|
|
76
|
+
{relativeTime(row.createdAt)}
|
|
77
|
+
</span>
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const rows = [...data.contacts].sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<PageHeader title="Contacts" count={rows.length}>
|
|
87
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
88
|
+
<Plus />
|
|
89
|
+
New contact
|
|
90
|
+
</Button>
|
|
91
|
+
</PageHeader>
|
|
92
|
+
|
|
93
|
+
<DataTable
|
|
94
|
+
rows={rows}
|
|
95
|
+
columns={columns}
|
|
96
|
+
empty={
|
|
97
|
+
<EmptyState
|
|
98
|
+
icon={<Users />}
|
|
99
|
+
title="No contacts yet"
|
|
100
|
+
description="The people you actually talk to. Add one and link it to a company to keep the account together."
|
|
101
|
+
action={
|
|
102
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
103
|
+
<Plus />
|
|
104
|
+
New contact
|
|
105
|
+
</Button>
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<RecordDialog
|
|
112
|
+
open={open}
|
|
113
|
+
title="New contact"
|
|
114
|
+
submitLabel="Create contact"
|
|
115
|
+
onOpenChange={setOpen}
|
|
116
|
+
fields={[
|
|
117
|
+
{ name: "name", label: "Name", required: true, placeholder: "Dana Whitfield" },
|
|
118
|
+
{ name: "title", label: "Title", placeholder: "VP Operations" },
|
|
119
|
+
{
|
|
120
|
+
name: "companyId",
|
|
121
|
+
label: "Company",
|
|
122
|
+
options: data.companies
|
|
123
|
+
.slice()
|
|
124
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
125
|
+
.map((company) => ({ value: company.id, label: company.name })),
|
|
126
|
+
},
|
|
127
|
+
{ name: "email", label: "Email", type: "email", placeholder: "dana@acme.com" },
|
|
128
|
+
{ name: "phone", label: "Phone", type: "tel", placeholder: "+1 555 0100" },
|
|
129
|
+
]}
|
|
130
|
+
onCreate={async (values) => {
|
|
131
|
+
await db.insert("Contact", values);
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}}
|
|
137
|
+
</Workspace>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { ContactsView } from "./contacts-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Contacts",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function ContactsPage({ auth, response, serverData }: PageProps) {
|
|
11
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
12
|
+
response.redirect("/login");
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
16
|
+
return <ContactsView email={me?.email ?? ""} />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Link, callFn, db, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { ArrowLeft, Trash2 } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Select } from "@/components/ui/select";
|
|
8
|
+
import { PageHeader } from "@/components/page-header";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { ActivityTimeline } from "@/components/activity-timeline";
|
|
11
|
+
import { StageBadge } from "@/components/stage-badge";
|
|
12
|
+
import { Avatar } from "@/components/avatar";
|
|
13
|
+
import { PIPELINE, daysUntil, money, relativeTime } from "@/lib/pipeline";
|
|
14
|
+
import { Workspace } from "../../workspace";
|
|
15
|
+
|
|
16
|
+
export function DealView({ email, dealId }: { email: string; dealId: string }) {
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Workspace email={email} pathname="/">
|
|
21
|
+
{(data) => {
|
|
22
|
+
const deal = data.deals.find((d) => d.id === dealId);
|
|
23
|
+
|
|
24
|
+
if (!deal) {
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<PageHeader title="Deal" />
|
|
28
|
+
<EmptyState
|
|
29
|
+
title={data.loading ? "Loading…" : "Deal not found"}
|
|
30
|
+
description={
|
|
31
|
+
data.loading
|
|
32
|
+
? undefined
|
|
33
|
+
: "It may have been deleted, or the link is wrong."
|
|
34
|
+
}
|
|
35
|
+
action={
|
|
36
|
+
data.loading ? undefined : (
|
|
37
|
+
<Button size="sm" variant="secondary" asChild>
|
|
38
|
+
<Link href="/">Back to pipeline</Link>
|
|
39
|
+
</Button>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
/>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const contact = data.contacts.find((c) => c.id === deal.contactId);
|
|
48
|
+
const activities = data.activities.filter((a) => a.dealId === deal.id);
|
|
49
|
+
const days = daysUntil(deal.closeDate);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<PageHeader title={deal.title}>
|
|
54
|
+
<Button
|
|
55
|
+
size="sm"
|
|
56
|
+
variant="ghost"
|
|
57
|
+
onClick={async () => {
|
|
58
|
+
await db.delete("Deal", deal.id);
|
|
59
|
+
router.push("/");
|
|
60
|
+
}}
|
|
61
|
+
className="text-muted-foreground hover:text-destructive"
|
|
62
|
+
>
|
|
63
|
+
<Trash2 />
|
|
64
|
+
Delete
|
|
65
|
+
</Button>
|
|
66
|
+
</PageHeader>
|
|
67
|
+
|
|
68
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
69
|
+
<div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-6">
|
|
70
|
+
<Link
|
|
71
|
+
href="/"
|
|
72
|
+
className="inline-flex w-fit items-center gap-1.5 text-[12px] text-muted-foreground transition-colors hover:text-foreground"
|
|
73
|
+
>
|
|
74
|
+
<ArrowLeft className="size-3.5" />
|
|
75
|
+
Pipeline
|
|
76
|
+
</Link>
|
|
77
|
+
|
|
78
|
+
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-2">
|
|
79
|
+
<h1 className="text-[22px] font-semibold tracking-tight">
|
|
80
|
+
{deal.title}
|
|
81
|
+
</h1>
|
|
82
|
+
<span className="tabular text-[18px] font-medium text-muted-foreground">
|
|
83
|
+
{money(deal.value)}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 rounded-lg border border-border bg-card p-4 sm:grid-cols-4">
|
|
88
|
+
<Field label="Stage">
|
|
89
|
+
{/* Changing stage here goes through the same mutation the
|
|
90
|
+
board uses, so it's logged identically. */}
|
|
91
|
+
<Select
|
|
92
|
+
aria-label="Stage"
|
|
93
|
+
value={deal.stage}
|
|
94
|
+
className="h-7 text-[12px]"
|
|
95
|
+
onChange={(event) =>
|
|
96
|
+
void callFn("moveDeal", {
|
|
97
|
+
dealId: deal.id,
|
|
98
|
+
stage: event.target.value,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
>
|
|
102
|
+
{PIPELINE.map((stage) => (
|
|
103
|
+
<option key={stage.id} value={stage.id}>
|
|
104
|
+
{stage.label}
|
|
105
|
+
</option>
|
|
106
|
+
))}
|
|
107
|
+
</Select>
|
|
108
|
+
</Field>
|
|
109
|
+
|
|
110
|
+
<Field label="Company">
|
|
111
|
+
{data.companyName(deal.companyId) ?? "—"}
|
|
112
|
+
</Field>
|
|
113
|
+
|
|
114
|
+
<Field label="Contact">
|
|
115
|
+
{contact ? (
|
|
116
|
+
<span className="flex items-center gap-1.5">
|
|
117
|
+
<Avatar name={contact.name} size="sm" />
|
|
118
|
+
{contact.name}
|
|
119
|
+
</span>
|
|
120
|
+
) : (
|
|
121
|
+
"—"
|
|
122
|
+
)}
|
|
123
|
+
</Field>
|
|
124
|
+
|
|
125
|
+
<Field label="Expected close">
|
|
126
|
+
{deal.closeDate ? (
|
|
127
|
+
<span className={days !== null && days < 0 ? "text-destructive" : ""}>
|
|
128
|
+
{new Date(deal.closeDate).toLocaleDateString("en-US", {
|
|
129
|
+
month: "short",
|
|
130
|
+
day: "numeric",
|
|
131
|
+
year: "numeric",
|
|
132
|
+
})}
|
|
133
|
+
</span>
|
|
134
|
+
) : (
|
|
135
|
+
"—"
|
|
136
|
+
)}
|
|
137
|
+
</Field>
|
|
138
|
+
</dl>
|
|
139
|
+
|
|
140
|
+
<div className="flex items-center gap-3 text-[12px] text-muted-foreground">
|
|
141
|
+
<StageBadge stage={deal.stage} />
|
|
142
|
+
<span aria-hidden="true">·</span>
|
|
143
|
+
<span>Created {relativeTime(deal.createdAt)}</span>
|
|
144
|
+
{data.ownerName(deal.ownerId) ? (
|
|
145
|
+
<>
|
|
146
|
+
<span aria-hidden="true">·</span>
|
|
147
|
+
<span>Owned by {data.ownerName(deal.ownerId)}</span>
|
|
148
|
+
</>
|
|
149
|
+
) : null}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<section>
|
|
153
|
+
<h2 className="mb-3 text-[13px] font-semibold">Activity</h2>
|
|
154
|
+
<ActivityTimeline
|
|
155
|
+
activities={activities}
|
|
156
|
+
ownerName={data.ownerName}
|
|
157
|
+
onLog={(kind, body) =>
|
|
158
|
+
callFn("logActivity", {
|
|
159
|
+
kind,
|
|
160
|
+
body,
|
|
161
|
+
dealId: deal.id,
|
|
162
|
+
...(deal.companyId ? { companyId: deal.companyId } : {}),
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
/>
|
|
166
|
+
</section>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
}}
|
|
172
|
+
</Workspace>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function Field({
|
|
177
|
+
label,
|
|
178
|
+
children,
|
|
179
|
+
}: {
|
|
180
|
+
label: string;
|
|
181
|
+
children: React.ReactNode;
|
|
182
|
+
}) {
|
|
183
|
+
return (
|
|
184
|
+
<div className="min-w-0">
|
|
185
|
+
<dt className="text-[11px] text-muted-foreground">{label}</dt>
|
|
186
|
+
<dd className="mt-1 truncate text-[13px]">{children}</dd>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { DealView } from "./deal-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Deal",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** `app/deals/[id]/page.tsx` → `/deals/:id`. */
|
|
11
|
+
export default function DealPage({
|
|
12
|
+
auth,
|
|
13
|
+
response,
|
|
14
|
+
params,
|
|
15
|
+
serverData,
|
|
16
|
+
}: PageProps<{ id: string }>) {
|
|
17
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
18
|
+
response.redirect("/login");
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
22
|
+
return <DealView email={me?.email ?? ""} dealId={params.id} />;
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
|
|
5
|
+
// `app/error.tsx` → the error boundary for this segment. It catches a throw
|
|
6
|
+
// in any page/layout below it and renders at HTTP 500. It's HYDRATED, so
|
|
7
|
+
// this is a real interactive client component: `reset()` re-attempts the
|
|
8
|
+
// route, and useState/onClick work. The thrown error reaches the client as
|
|
9
|
+
// `{ message, digest }` only — the stack stays in the dev overlay
|
|
10
|
+
// (PYLON_DEV_MODE) and the server logs, never in the page.
|
|
11
|
+
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
12
|
+
const [tries, setTries] = React.useState(0);
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-6">
|
|
15
|
+
<section>
|
|
16
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
17
|
+
Something went wrong
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="mt-2 text-muted-foreground">{error.message}</p>
|
|
20
|
+
{error.digest ? (
|
|
21
|
+
<p className="mt-1 text-xs text-muted-foreground/70">
|
|
22
|
+
Reference: <code>{error.digest}</code>
|
|
23
|
+
</p>
|
|
24
|
+
) : null}
|
|
25
|
+
</section>
|
|
26
|
+
<div className="flex items-center gap-3">
|
|
27
|
+
<Button
|
|
28
|
+
onClick={() => {
|
|
29
|
+
setTries((n) => n + 1);
|
|
30
|
+
reset();
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
Try again
|
|
34
|
+
</Button>
|
|
35
|
+
{tries > 0 ? (
|
|
36
|
+
<span className="text-sm text-muted-foreground">
|
|
37
|
+
Retried {tries} {tries === 1 ? "time" : "times"}
|
|
38
|
+
</span>
|
|
39
|
+
) : null}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|