@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,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, useRouter, type NotFoundProps } from "@pylonsync/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
|
|
5
|
+
// `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when
|
|
6
|
+
// a page calls `response.notFound()`). It's HYDRATED, so it's interactive:
|
|
7
|
+
// the buttons below use the client router. Not-found boundaries receive the
|
|
8
|
+
// standard page props (and, matching Next, no `reset`).
|
|
9
|
+
export default function NotFound(_props: NotFoundProps) {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-6">
|
|
13
|
+
<section>
|
|
14
|
+
<h1 className="text-2xl font-semibold tracking-tight">404</h1>
|
|
15
|
+
<p className="mt-2 text-muted-foreground">
|
|
16
|
+
We couldn't find that page.
|
|
17
|
+
</p>
|
|
18
|
+
</section>
|
|
19
|
+
<div className="flex items-center gap-3">
|
|
20
|
+
<Button onClick={() => router.back()} variant="outline">
|
|
21
|
+
← Go back
|
|
22
|
+
</Button>
|
|
23
|
+
<Button asChild>
|
|
24
|
+
<Link href="/">Home</Link>
|
|
25
|
+
</Button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { ProjectsView } from "./projects-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Projects",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `/` — the project list.
|
|
12
|
+
*
|
|
13
|
+
* Server-side auth gate only. Reading `auth` opts this render out of caching,
|
|
14
|
+
* which is correct: every screen here is private.
|
|
15
|
+
*/
|
|
16
|
+
export default function ProjectsPage({
|
|
17
|
+
auth,
|
|
18
|
+
response,
|
|
19
|
+
searchParams,
|
|
20
|
+
serverData,
|
|
21
|
+
}: PageProps) {
|
|
22
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
23
|
+
response.redirect("/login");
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
27
|
+
return (
|
|
28
|
+
<ProjectsView email={me?.email ?? ""} openNew={searchParams?.new === "project"} />
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { ProjectView } from "./project-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Project",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** `app/projects/[id]/page.tsx` -> `/projects/:id`. */
|
|
11
|
+
export default function ProjectPage({
|
|
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 <ProjectView email={me?.email ?? ""} projectId={params.id} />;
|
|
23
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Link, callFn, db, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { ArrowLeft, Clock, Plus } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Input } from "@/components/ui/input";
|
|
8
|
+
import { Select } from "@/components/ui/select";
|
|
9
|
+
import { PageHeader } from "@/components/page-header";
|
|
10
|
+
import { EmptyState } from "@/components/empty-state";
|
|
11
|
+
import { TaskBoard } from "@/components/task-board";
|
|
12
|
+
import { BoardSkeleton } from "@/components/board-skeleton";
|
|
13
|
+
import { BudgetBar } from "@/components/budget-bar";
|
|
14
|
+
import { TimeDialog } from "@/components/time-dialog";
|
|
15
|
+
import { PROJECT_STATUSES, duration, minutesForProject, progress } from "@/lib/work";
|
|
16
|
+
import { Workspace } from "../../workspace";
|
|
17
|
+
|
|
18
|
+
export function ProjectView({
|
|
19
|
+
email,
|
|
20
|
+
projectId,
|
|
21
|
+
}: {
|
|
22
|
+
email: string;
|
|
23
|
+
projectId: string;
|
|
24
|
+
}) {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
const [newTask, setNewTask] = useState("");
|
|
27
|
+
const [timeFor, setTimeFor] = useState<{ id: string; title: string } | null>(null);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Workspace email={email} pathname="/">
|
|
31
|
+
{(data) => {
|
|
32
|
+
const project = data.projects.find((p) => p.id === projectId);
|
|
33
|
+
|
|
34
|
+
if (!project) {
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<PageHeader title="Project" />
|
|
38
|
+
<EmptyState
|
|
39
|
+
title={data.loading ? "Loading…" : "Project not found"}
|
|
40
|
+
description={
|
|
41
|
+
data.loading ? undefined : "It may have been deleted, or the link is wrong."
|
|
42
|
+
}
|
|
43
|
+
action={
|
|
44
|
+
data.loading ? undefined : (
|
|
45
|
+
<Button size="sm" variant="secondary" asChild>
|
|
46
|
+
<Link href="/">Back to projects</Link>
|
|
47
|
+
</Button>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
/>
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Captured after the guard above: TypeScript's narrowing doesn't reach
|
|
56
|
+
// into the nested handler, and widening the check would hide a real bug.
|
|
57
|
+
const current = project;
|
|
58
|
+
const tasks = data.tasks.filter((task) => task.projectId === current.id);
|
|
59
|
+
const logged = minutesForProject(current.id, data.entries);
|
|
60
|
+
const p = progress(tasks);
|
|
61
|
+
|
|
62
|
+
async function addTask(event: React.FormEvent) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
const title = newTask.trim();
|
|
65
|
+
if (!title) return;
|
|
66
|
+
setNewTask("");
|
|
67
|
+
await db.insert("Task", {
|
|
68
|
+
projectId: current.id,
|
|
69
|
+
title,
|
|
70
|
+
status: "todo",
|
|
71
|
+
position: tasks.filter((t) => t.status === "todo").length,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<PageHeader title={project.name}>
|
|
78
|
+
<span className="tabular text-[12px] text-muted-foreground">
|
|
79
|
+
{p.done}/{p.total} done · {duration(logged)}
|
|
80
|
+
</span>
|
|
81
|
+
<div className="w-28">
|
|
82
|
+
<Select
|
|
83
|
+
aria-label="Project status"
|
|
84
|
+
value={project.status}
|
|
85
|
+
className="h-7 text-[12px]"
|
|
86
|
+
onChange={(event) =>
|
|
87
|
+
void db.update("Project", project.id, {
|
|
88
|
+
status: event.target.value,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
>
|
|
92
|
+
{PROJECT_STATUSES.map((s) => (
|
|
93
|
+
<option key={s.id} value={s.id}>
|
|
94
|
+
{s.label}
|
|
95
|
+
</option>
|
|
96
|
+
))}
|
|
97
|
+
</Select>
|
|
98
|
+
</div>
|
|
99
|
+
</PageHeader>
|
|
100
|
+
|
|
101
|
+
<div className="flex items-center gap-6 border-b border-border px-4 py-3">
|
|
102
|
+
<Link
|
|
103
|
+
href="/"
|
|
104
|
+
className="inline-flex shrink-0 items-center gap-1.5 text-[12px] text-muted-foreground transition-colors hover:text-foreground"
|
|
105
|
+
>
|
|
106
|
+
<ArrowLeft className="size-3.5" />
|
|
107
|
+
Projects
|
|
108
|
+
</Link>
|
|
109
|
+
<span className="shrink-0 text-[12px] text-muted-foreground">
|
|
110
|
+
{data.clientName(project.clientId) ?? "No client"}
|
|
111
|
+
</span>
|
|
112
|
+
<div className="min-w-0 flex-1 max-w-md">
|
|
113
|
+
<BudgetBar
|
|
114
|
+
budgetMinutes={project.budgetMinutes}
|
|
115
|
+
loggedMinutes={logged}
|
|
116
|
+
hourlyRateCents={project.hourlyRateCents}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<form onSubmit={addTask} className="ml-auto flex shrink-0 items-center gap-2">
|
|
120
|
+
<Input
|
|
121
|
+
aria-label="New task"
|
|
122
|
+
placeholder="Add a task…"
|
|
123
|
+
value={newTask}
|
|
124
|
+
onChange={(event) => setNewTask(event.target.value)}
|
|
125
|
+
className="h-7 w-48 text-[12px]"
|
|
126
|
+
/>
|
|
127
|
+
<Button type="submit" size="sm" className="h-7" disabled={!newTask.trim()}>
|
|
128
|
+
<Plus />
|
|
129
|
+
</Button>
|
|
130
|
+
</form>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="min-h-0 flex-1">
|
|
134
|
+
{data.loading ? (
|
|
135
|
+
<BoardSkeleton />
|
|
136
|
+
) : tasks.length === 0 ? (
|
|
137
|
+
<EmptyState
|
|
138
|
+
icon={<Clock />}
|
|
139
|
+
title="No tasks yet"
|
|
140
|
+
description="Add the first one above. Drag it between columns as it moves, and log time against it from the card."
|
|
141
|
+
/>
|
|
142
|
+
) : (
|
|
143
|
+
<TaskBoard
|
|
144
|
+
tasks={tasks}
|
|
145
|
+
assigneeName={data.memberName}
|
|
146
|
+
minutesFor={(taskId) => data.taskMinutes.get(taskId) ?? 0}
|
|
147
|
+
onMove={(taskId, status) => {
|
|
148
|
+
// Through the mutation: the new position is computed
|
|
149
|
+
// server-side so two simultaneous drags don\'t collide.
|
|
150
|
+
void callFn("moveTask", { taskId, status });
|
|
151
|
+
}}
|
|
152
|
+
onOpen={(taskId) => {
|
|
153
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
154
|
+
if (task) setTimeFor({ id: task.id, title: task.title });
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<TimeDialog
|
|
161
|
+
open={timeFor !== null}
|
|
162
|
+
taskTitle={timeFor?.title ?? ""}
|
|
163
|
+
onOpenChange={(open) => {
|
|
164
|
+
if (!open) setTimeFor(null);
|
|
165
|
+
}}
|
|
166
|
+
onLog={(minutes, note) =>
|
|
167
|
+
callFn("logTime", {
|
|
168
|
+
taskId: timeFor?.id ?? "",
|
|
169
|
+
minutes,
|
|
170
|
+
...(note ? { note } : {}),
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
}}
|
|
177
|
+
</Workspace>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { db, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { FolderKanban, 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 {
|
|
12
|
+
budgetState,
|
|
13
|
+
duration,
|
|
14
|
+
isOpen,
|
|
15
|
+
minutesForProject,
|
|
16
|
+
parseDuration,
|
|
17
|
+
progress,
|
|
18
|
+
} from "@/lib/work";
|
|
19
|
+
import { Workspace, type ProjectRow } from "./workspace";
|
|
20
|
+
|
|
21
|
+
export function ProjectsView({
|
|
22
|
+
email,
|
|
23
|
+
openNew,
|
|
24
|
+
}: {
|
|
25
|
+
email: string;
|
|
26
|
+
openNew?: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
const router = useRouter();
|
|
29
|
+
const [open, setOpen] = useState(Boolean(openNew));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Workspace email={email} pathname="/">
|
|
33
|
+
{(data) => {
|
|
34
|
+
const columns: ColumnDef<ProjectRow>[] = [
|
|
35
|
+
{
|
|
36
|
+
key: "name",
|
|
37
|
+
header: "Project",
|
|
38
|
+
cell: (row) => <span className="truncate font-medium">{row.name}</span>,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "client",
|
|
42
|
+
header: "Client",
|
|
43
|
+
cell: (row) => (
|
|
44
|
+
<span className="text-muted-foreground">
|
|
45
|
+
{data.clientName(row.clientId) ?? "—"}
|
|
46
|
+
</span>
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "tasks",
|
|
51
|
+
header: "Tasks",
|
|
52
|
+
cell: (row) => {
|
|
53
|
+
const tasks = data.tasks.filter((t) => t.projectId === row.id);
|
|
54
|
+
const p = progress(tasks);
|
|
55
|
+
return (
|
|
56
|
+
<span className="tabular text-muted-foreground">
|
|
57
|
+
{p.done}/{p.total}
|
|
58
|
+
</span>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "open",
|
|
64
|
+
header: "Open",
|
|
65
|
+
numeric: true,
|
|
66
|
+
cell: (row) =>
|
|
67
|
+
data.tasks.filter((t) => t.projectId === row.id && isOpen(t)).length,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "time",
|
|
71
|
+
header: "Logged",
|
|
72
|
+
numeric: true,
|
|
73
|
+
cell: (row) => {
|
|
74
|
+
const logged = minutesForProject(row.id, data.entries);
|
|
75
|
+
const state = budgetState(row.budgetMinutes, logged);
|
|
76
|
+
return (
|
|
77
|
+
<span
|
|
78
|
+
className={
|
|
79
|
+
state === "over"
|
|
80
|
+
? "text-destructive"
|
|
81
|
+
: state === "near"
|
|
82
|
+
? "text-stage-proposal"
|
|
83
|
+
: ""
|
|
84
|
+
}
|
|
85
|
+
title={
|
|
86
|
+
row.budgetMinutes
|
|
87
|
+
? `Budget ${duration(row.budgetMinutes)}`
|
|
88
|
+
: undefined
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
{duration(logged)}
|
|
92
|
+
</span>
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: "due",
|
|
98
|
+
header: "Due",
|
|
99
|
+
cell: (row) => (
|
|
100
|
+
<span className="text-muted-foreground">
|
|
101
|
+
{row.dueDate
|
|
102
|
+
? new Date(row.dueDate).toLocaleDateString("en-US", {
|
|
103
|
+
month: "short",
|
|
104
|
+
day: "numeric",
|
|
105
|
+
})
|
|
106
|
+
: "—"}
|
|
107
|
+
</span>
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// Active first, then by name — a finished project shouldn\'t sit above
|
|
113
|
+
// the work in flight.
|
|
114
|
+
const rows = [...data.projects].sort((a, b) => {
|
|
115
|
+
const rank = (p: ProjectRow) => (p.status === "active" ? 0 : p.status === "paused" ? 1 : 2);
|
|
116
|
+
return rank(a) - rank(b) || a.name.localeCompare(b.name);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
<PageHeader title="Projects" count={rows.length}>
|
|
122
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
123
|
+
<Plus />
|
|
124
|
+
New project
|
|
125
|
+
</Button>
|
|
126
|
+
</PageHeader>
|
|
127
|
+
|
|
128
|
+
<DataTable
|
|
129
|
+
rows={rows}
|
|
130
|
+
columns={columns}
|
|
131
|
+
onRowClick={(row) => router.push(`/projects/${row.id}`)}
|
|
132
|
+
empty={
|
|
133
|
+
<EmptyState
|
|
134
|
+
icon={<FolderKanban />}
|
|
135
|
+
title={data.loading ? "Loading…" : "No projects yet"}
|
|
136
|
+
description="A project holds a task board and the time logged against it. Create one to get started."
|
|
137
|
+
action={
|
|
138
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
139
|
+
<Plus />
|
|
140
|
+
New project
|
|
141
|
+
</Button>
|
|
142
|
+
}
|
|
143
|
+
/>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
<RecordDialog
|
|
148
|
+
open={open}
|
|
149
|
+
title="New project"
|
|
150
|
+
submitLabel="Create project"
|
|
151
|
+
onOpenChange={setOpen}
|
|
152
|
+
fields={[
|
|
153
|
+
{ name: "name", label: "Name", required: true, placeholder: "Dispatch portal" },
|
|
154
|
+
{
|
|
155
|
+
name: "clientId",
|
|
156
|
+
label: "Client",
|
|
157
|
+
options: data.clients
|
|
158
|
+
.slice()
|
|
159
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
160
|
+
.map((c) => ({ value: c.id, label: c.name })),
|
|
161
|
+
},
|
|
162
|
+
{ name: "budget", label: "Budget", placeholder: "80h" },
|
|
163
|
+
{ name: "rate", label: "Hourly rate", placeholder: "165" },
|
|
164
|
+
]}
|
|
165
|
+
onCreate={async (values) => {
|
|
166
|
+
// Parsed at the edge — everything below this line is integers.
|
|
167
|
+
await db.insert("Project", {
|
|
168
|
+
name: values.name,
|
|
169
|
+
clientId: values.clientId ?? null,
|
|
170
|
+
status: "active",
|
|
171
|
+
budgetMinutes: parseDuration(values.budget ?? "") ?? 0,
|
|
172
|
+
hourlyRateCents: Math.round(Number(values.rate ?? 0) * 100) || 0,
|
|
173
|
+
});
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
}}
|
|
179
|
+
</Workspace>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Robots } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/robots.ts → served at /robots.txt. The default export may also be async.
|
|
4
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
5
|
+
|
|
6
|
+
export default function robots(): Robots {
|
|
7
|
+
return {
|
|
8
|
+
// Keep the authenticated app and the API out of the index.
|
|
9
|
+
rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/api/"] },
|
|
10
|
+
sitemap: `${SITE}/sitemap.xml`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Sitemap } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/sitemap.ts → served at /sitemap.xml. The default export can be async, so
|
|
4
|
+
// it can enumerate dynamic pages from your database. Point SITE_URL at your
|
|
5
|
+
// domain in production.
|
|
6
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
7
|
+
|
|
8
|
+
export default async function sitemap(): Promise<Sitemap> {
|
|
9
|
+
// Only public pages belong here — /dashboard is private (and noindex), so
|
|
10
|
+
// it's intentionally left out.
|
|
11
|
+
const staticRoutes: Sitemap = [
|
|
12
|
+
{ url: `${SITE}/`, changeFrequency: "weekly", priority: 1 },
|
|
13
|
+
{ url: `${SITE}/login`, changeFrequency: "yearly", priority: 0.3 },
|
|
14
|
+
{ url: `${SITE}/signup`, changeFrequency: "yearly", priority: 0.5 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// The export is async, so you can enumerate dynamic pages from a DB read:
|
|
18
|
+
//
|
|
19
|
+
// const posts = await fetchPublishedPosts();
|
|
20
|
+
// const postRoutes: Sitemap = posts.map((p) => ({
|
|
21
|
+
// url: `${SITE}/blog/${p.slug}`,
|
|
22
|
+
// lastModified: p.updatedAt,
|
|
23
|
+
// }));
|
|
24
|
+
// return [...staticRoutes, ...postRoutes];
|
|
25
|
+
|
|
26
|
+
return staticRoutes;
|
|
27
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { callFn, db, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { useAuth } from "@pylonsync/client";
|
|
6
|
+
import { Sidebar } from "@/components/sidebar";
|
|
7
|
+
import { CommandPalette } from "@/components/command-palette";
|
|
8
|
+
import type { SearchItem } from "@/lib/search";
|
|
9
|
+
import { minutesByTask, type Project, type Task, type TimeEntry } from "@/lib/work";
|
|
10
|
+
|
|
11
|
+
export interface ClientRow {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
email?: string | null;
|
|
15
|
+
createdAt?: string | null;
|
|
16
|
+
}
|
|
17
|
+
export type ProjectRow = Project;
|
|
18
|
+
export type TaskRow = Task;
|
|
19
|
+
export type TimeEntryRow = TimeEntry;
|
|
20
|
+
export interface UserRow {
|
|
21
|
+
id: string;
|
|
22
|
+
email: string;
|
|
23
|
+
displayName?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkspaceData {
|
|
27
|
+
clients: ClientRow[];
|
|
28
|
+
projects: ProjectRow[];
|
|
29
|
+
tasks: TaskRow[];
|
|
30
|
+
entries: TimeEntryRow[];
|
|
31
|
+
/** Minutes per task, computed once per render rather than per card. */
|
|
32
|
+
taskMinutes: Map<string, number>;
|
|
33
|
+
clientName: (id: string | null | undefined) => string | null;
|
|
34
|
+
memberName: (id: string | null | undefined) => string | null;
|
|
35
|
+
members: UserRow[];
|
|
36
|
+
loading: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The app shell, and the ONLY place that touches `db`.
|
|
41
|
+
*
|
|
42
|
+
* Every view below receives plain data through the render prop and reports
|
|
43
|
+
* changes through callbacks, which is what lets the components in `components/`
|
|
44
|
+
* render from fixtures in a test with nothing mocked.
|
|
45
|
+
*
|
|
46
|
+
* The queries are live: drag a task and it moves on every teammate\'s board, so
|
|
47
|
+
* a standup doesn\'t start with reconciling two views of the work.
|
|
48
|
+
*/
|
|
49
|
+
export function Workspace({
|
|
50
|
+
email,
|
|
51
|
+
pathname,
|
|
52
|
+
children,
|
|
53
|
+
}: {
|
|
54
|
+
email: string;
|
|
55
|
+
pathname: string;
|
|
56
|
+
children: (data: WorkspaceData) => React.ReactNode;
|
|
57
|
+
}) {
|
|
58
|
+
const router = useRouter();
|
|
59
|
+
const { signOut } = useAuth();
|
|
60
|
+
const { data: projects, loading } = db.useQuery<ProjectRow>("Project");
|
|
61
|
+
const { data: clients } = db.useQuery<ClientRow>("Client");
|
|
62
|
+
const { data: tasks } = db.useQuery<TaskRow>("Task");
|
|
63
|
+
const { data: entries } = db.useQuery<TimeEntryRow>("TimeEntry");
|
|
64
|
+
const { data: users } = db.useQuery<UserRow>("User");
|
|
65
|
+
|
|
66
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
67
|
+
const seeded = useRef(false);
|
|
68
|
+
|
|
69
|
+
const projectList = projects ?? [];
|
|
70
|
+
const clientList = clients ?? [];
|
|
71
|
+
const taskList = tasks ?? [];
|
|
72
|
+
const entryList = entries ?? [];
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (loading || seeded.current) return;
|
|
76
|
+
if (projectList.length > 0) {
|
|
77
|
+
seeded.current = true;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
seeded.current = true;
|
|
81
|
+
callFn("seedWorkspace", {}).catch(() => {
|
|
82
|
+
seeded.current = false;
|
|
83
|
+
});
|
|
84
|
+
}, [loading, projectList.length]);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
function onKey(event: KeyboardEvent) {
|
|
88
|
+
const target = event.target as HTMLElement | null;
|
|
89
|
+
const typing =
|
|
90
|
+
!!target &&
|
|
91
|
+
(target.tagName === "INPUT" ||
|
|
92
|
+
target.tagName === "TEXTAREA" ||
|
|
93
|
+
target.isContentEditable);
|
|
94
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
setPaletteOpen((open) => !open);
|
|
97
|
+
} else if (event.key === "/" && !typing) {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
setPaletteOpen(true);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
window.addEventListener("keydown", onKey);
|
|
103
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
// One pass over the ledger for the whole render — per-card lookups would be
|
|
107
|
+
// O(tasks x entries).
|
|
108
|
+
const taskMinutes = useMemo(() => minutesByTask(entryList), [entryList]);
|
|
109
|
+
const clientById = useMemo(
|
|
110
|
+
() => new Map(clientList.map((c) => [c.id, c] as const)),
|
|
111
|
+
[clientList],
|
|
112
|
+
);
|
|
113
|
+
const userById = useMemo(
|
|
114
|
+
() => new Map((users ?? []).map((u) => [u.id, u] as const)),
|
|
115
|
+
[users],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const searchIndex = useMemo<SearchItem[]>(
|
|
119
|
+
() => [
|
|
120
|
+
...projectList.map((project) => ({
|
|
121
|
+
id: `project:${project.id}`,
|
|
122
|
+
type: "company" as const,
|
|
123
|
+
title: project.name,
|
|
124
|
+
subtitle: clientById.get(project.clientId ?? "")?.name,
|
|
125
|
+
href: `/projects/${project.id}`,
|
|
126
|
+
})),
|
|
127
|
+
...taskList.map((task) => ({
|
|
128
|
+
id: `task:${task.id}`,
|
|
129
|
+
type: "deal" as const,
|
|
130
|
+
title: task.title,
|
|
131
|
+
href: `/projects/${task.projectId}`,
|
|
132
|
+
})),
|
|
133
|
+
],
|
|
134
|
+
[projectList, taskList, clientById],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const data: WorkspaceData = {
|
|
138
|
+
clients: clientList,
|
|
139
|
+
projects: projectList,
|
|
140
|
+
tasks: taskList,
|
|
141
|
+
entries: entryList,
|
|
142
|
+
taskMinutes,
|
|
143
|
+
clientName: (id) => clientById.get(id ?? "")?.name ?? null,
|
|
144
|
+
memberName: (id) => {
|
|
145
|
+
const user = userById.get(id ?? "");
|
|
146
|
+
return user?.displayName || user?.email || null;
|
|
147
|
+
},
|
|
148
|
+
members: users ?? [],
|
|
149
|
+
loading,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex h-screen overflow-hidden">
|
|
154
|
+
<Sidebar
|
|
155
|
+
workspace="Projects"
|
|
156
|
+
email={email}
|
|
157
|
+
pathname={pathname}
|
|
158
|
+
counts={{
|
|
159
|
+
"/": projectList.filter((p) => p.status === "active").length,
|
|
160
|
+
"/clients": clientList.length,
|
|
161
|
+
}}
|
|
162
|
+
onOpenCommand={() => setPaletteOpen(true)}
|
|
163
|
+
onSignOut={async () => {
|
|
164
|
+
await signOut();
|
|
165
|
+
window.location.assign("/login");
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
<main className="flex min-w-0 flex-1 flex-col">{children(data)}</main>
|
|
169
|
+
|
|
170
|
+
<CommandPalette
|
|
171
|
+
open={paletteOpen}
|
|
172
|
+
items={searchIndex}
|
|
173
|
+
actions={[
|
|
174
|
+
{
|
|
175
|
+
id: "new-project",
|
|
176
|
+
label: "New project",
|
|
177
|
+
run: () => router.push("/?new=project"),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "go-clients",
|
|
181
|
+
label: "Go to Clients",
|
|
182
|
+
run: () => router.push("/clients"),
|
|
183
|
+
},
|
|
184
|
+
]}
|
|
185
|
+
onClose={() => setPaletteOpen(false)}
|
|
186
|
+
onSelect={(item) => router.push(item.href)}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|