@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,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { callFn, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { FileText, Plus } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Kbd } from "@/components/kbd";
|
|
8
|
+
import { PageHeader } from "@/components/page-header";
|
|
9
|
+
import { DataTable, type ColumnDef } from "@/components/data-table";
|
|
10
|
+
import { EmptyState } from "@/components/empty-state";
|
|
11
|
+
import { SummaryBar } from "@/components/summary-bar";
|
|
12
|
+
import { StatusBadge } from "@/components/status-badge";
|
|
13
|
+
import {
|
|
14
|
+
daysOverdue,
|
|
15
|
+
displayStatus,
|
|
16
|
+
money,
|
|
17
|
+
totals,
|
|
18
|
+
} from "@/lib/billing";
|
|
19
|
+
import { Workspace, type InvoiceRow } from "./workspace";
|
|
20
|
+
|
|
21
|
+
export function InvoicesView({
|
|
22
|
+
email,
|
|
23
|
+
openNew,
|
|
24
|
+
}: {
|
|
25
|
+
email: string;
|
|
26
|
+
openNew?: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
const router = useRouter();
|
|
29
|
+
const [creating, setCreating] = useState(false);
|
|
30
|
+
|
|
31
|
+
async function create() {
|
|
32
|
+
if (creating) return;
|
|
33
|
+
setCreating(true);
|
|
34
|
+
try {
|
|
35
|
+
// The number is allocated server-side from the existing series, so the
|
|
36
|
+
// client never invents one.
|
|
37
|
+
const result = await callFn<{ id: string }>("createInvoice", {});
|
|
38
|
+
if (result?.id) router.push(`/invoices/${result.id}`);
|
|
39
|
+
} finally {
|
|
40
|
+
setCreating(false);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ?new=invoice from the ⌘K action.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (openNew) void create();
|
|
47
|
+
// Fire once on mount for the deep link; `create` is stable enough here.
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [openNew]);
|
|
50
|
+
|
|
51
|
+
// "c" creates an invoice. Ignored while typing.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
function onKey(event: KeyboardEvent) {
|
|
54
|
+
const target = event.target as HTMLElement | null;
|
|
55
|
+
const typing =
|
|
56
|
+
!!target &&
|
|
57
|
+
(target.tagName === "INPUT" ||
|
|
58
|
+
target.tagName === "TEXTAREA" ||
|
|
59
|
+
target.isContentEditable);
|
|
60
|
+
if (event.key === "c" && !typing && !event.metaKey && !event.ctrlKey) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
void create();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
window.addEventListener("keydown", onKey);
|
|
66
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
67
|
+
}, [creating]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Workspace email={email} pathname="/">
|
|
71
|
+
{(data) => {
|
|
72
|
+
const columns: ColumnDef<InvoiceRow>[] = [
|
|
73
|
+
{
|
|
74
|
+
key: "number",
|
|
75
|
+
header: "Invoice",
|
|
76
|
+
cell: (row) => <span className="tabular font-medium">{row.number}</span>,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "client",
|
|
80
|
+
header: "Client",
|
|
81
|
+
cell: (row) => (
|
|
82
|
+
<span className="text-muted-foreground">
|
|
83
|
+
{data.clientName(row.clientId) ?? "—"}
|
|
84
|
+
</span>
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "status",
|
|
89
|
+
header: "Status",
|
|
90
|
+
cell: (row) => {
|
|
91
|
+
const t = totals(row, data.items, data.payments);
|
|
92
|
+
return <StatusBadge status={displayStatus(row, t.balanceCents)} />;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: "due",
|
|
97
|
+
header: "Due",
|
|
98
|
+
cell: (row) => {
|
|
99
|
+
const t = totals(row, data.items, data.payments);
|
|
100
|
+
const late = displayStatus(row, t.balanceCents) === "overdue";
|
|
101
|
+
const days = daysOverdue(row);
|
|
102
|
+
if (!row.dueDate) return <span className="text-muted-foreground">—</span>;
|
|
103
|
+
return (
|
|
104
|
+
<span className={late ? "text-destructive" : "text-muted-foreground"}>
|
|
105
|
+
{late && days !== null
|
|
106
|
+
? `${days}d overdue`
|
|
107
|
+
: new Date(row.dueDate).toLocaleDateString("en-US", {
|
|
108
|
+
month: "short",
|
|
109
|
+
day: "numeric",
|
|
110
|
+
})}
|
|
111
|
+
</span>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "total",
|
|
117
|
+
header: "Total",
|
|
118
|
+
numeric: true,
|
|
119
|
+
cell: (row) => money(totals(row, data.items, data.payments).totalCents),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: "balance",
|
|
123
|
+
header: "Balance",
|
|
124
|
+
numeric: true,
|
|
125
|
+
cell: (row) => {
|
|
126
|
+
const t = totals(row, data.items, data.payments);
|
|
127
|
+
return (
|
|
128
|
+
<span className={t.balanceCents > 0 ? "" : "text-muted-foreground"}>
|
|
129
|
+
{money(t.balanceCents)}
|
|
130
|
+
</span>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Newest first — an invoice list is read from the top, and the number
|
|
137
|
+
// series already encodes the order.
|
|
138
|
+
const rows = [...data.invoices].sort((a, b) =>
|
|
139
|
+
b.number.localeCompare(a.number),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<PageHeader title="Invoices" count={rows.length}>
|
|
145
|
+
<Button size="sm" onClick={() => void create()} disabled={creating}>
|
|
146
|
+
<Plus />
|
|
147
|
+
{creating ? "Creating…" : "New invoice"}
|
|
148
|
+
<Kbd className="ml-1 border-primary-foreground/25 bg-primary-foreground/10 text-primary-foreground/70">
|
|
149
|
+
c
|
|
150
|
+
</Kbd>
|
|
151
|
+
</Button>
|
|
152
|
+
</PageHeader>
|
|
153
|
+
|
|
154
|
+
<SummaryBar
|
|
155
|
+
invoices={data.invoices}
|
|
156
|
+
items={data.items}
|
|
157
|
+
payments={data.payments}
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<DataTable
|
|
161
|
+
rows={rows}
|
|
162
|
+
columns={columns}
|
|
163
|
+
onRowClick={(row) => router.push(`/invoices/${row.id}`)}
|
|
164
|
+
empty={
|
|
165
|
+
<EmptyState
|
|
166
|
+
icon={<FileText />}
|
|
167
|
+
title={data.loading ? "Loading…" : "No invoices yet"}
|
|
168
|
+
description="Create one, add the billable lines, then send it. Totals and ageing are derived — there's nothing to keep in sync."
|
|
169
|
+
action={
|
|
170
|
+
<Button size="sm" onClick={() => void create()}>
|
|
171
|
+
<Plus />
|
|
172
|
+
New invoice
|
|
173
|
+
</Button>
|
|
174
|
+
}
|
|
175
|
+
/>
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
}}
|
|
181
|
+
</Workspace>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface LayoutProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The document shell. The app chrome — sidebar, command palette, shortcuts —
|
|
9
|
+
* lives in `app/workspace.tsx` instead, because it needs the synced data; this
|
|
10
|
+
* layer only sets up the page.
|
|
11
|
+
*
|
|
12
|
+
* `class="dark"` makes dark the default theme. The light tokens in globals.css
|
|
13
|
+
* are complete and tested, so removing the class flips the whole app; swap it
|
|
14
|
+
* for a stored preference when you want a toggle.
|
|
15
|
+
*/
|
|
16
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" className="dark">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charSet="utf-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
+
<title>__APP_NAME__</title>
|
|
23
|
+
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
24
|
+
stylesheet link is injected here automatically. Inter is declared in
|
|
25
|
+
app.ts (fonts: [...]) and self-hosted by the build — @font-face, the
|
|
26
|
+
preload link, and a size-adjusted fallback are injected too. */}
|
|
27
|
+
</head>
|
|
28
|
+
<body className="h-full">{children}</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ApiError,
|
|
6
|
+
passwordLogin,
|
|
7
|
+
passwordRegister,
|
|
8
|
+
persistSession,
|
|
9
|
+
} from "@pylonsync/client";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import { Label } from "@/components/ui/label";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One form, two modes. Calls the built-in auth API (`/api/auth/password/*`),
|
|
16
|
+
* then `persistSession` writes the token so the sync engine and `callFn`
|
|
17
|
+
* authenticate as this user on the next load.
|
|
18
|
+
*
|
|
19
|
+
* The navigation is a full `assign`, not a client route change, so the SSR
|
|
20
|
+
* runtime re-resolves auth from the HttpOnly cookie and the first paint of the
|
|
21
|
+
* workspace is already signed in.
|
|
22
|
+
*/
|
|
23
|
+
export function AuthForm() {
|
|
24
|
+
const [mode, setMode] = useState<"login" | "signup">("login");
|
|
25
|
+
const [email, setEmail] = useState("");
|
|
26
|
+
const [password, setPassword] = useState("");
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
const [pending, setPending] = useState(false);
|
|
29
|
+
|
|
30
|
+
async function onSubmit(event: React.FormEvent) {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
setError(null);
|
|
33
|
+
setPending(true);
|
|
34
|
+
try {
|
|
35
|
+
const session =
|
|
36
|
+
mode === "login"
|
|
37
|
+
? await passwordLogin({ email, password })
|
|
38
|
+
: await passwordRegister({ email, password });
|
|
39
|
+
persistSession(session);
|
|
40
|
+
window.location.assign("/");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError(messageFor(err));
|
|
43
|
+
setPending(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="space-y-5">
|
|
49
|
+
<form onSubmit={onSubmit} className="space-y-3.5">
|
|
50
|
+
<div className="space-y-1.5">
|
|
51
|
+
<Label htmlFor="email">Email</Label>
|
|
52
|
+
<Input
|
|
53
|
+
id="email"
|
|
54
|
+
type="email"
|
|
55
|
+
value={email}
|
|
56
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
57
|
+
required
|
|
58
|
+
autoComplete="email"
|
|
59
|
+
placeholder="you@company.com"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="space-y-1.5">
|
|
63
|
+
<Label htmlFor="password">Password</Label>
|
|
64
|
+
<Input
|
|
65
|
+
id="password"
|
|
66
|
+
type="password"
|
|
67
|
+
value={password}
|
|
68
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
69
|
+
required
|
|
70
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
71
|
+
placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
{error ? (
|
|
75
|
+
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-[12px] leading-snug text-destructive">
|
|
76
|
+
{error}
|
|
77
|
+
</p>
|
|
78
|
+
) : null}
|
|
79
|
+
<Button type="submit" disabled={pending} className="w-full">
|
|
80
|
+
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
81
|
+
</Button>
|
|
82
|
+
</form>
|
|
83
|
+
|
|
84
|
+
<p className="text-center text-[12px] text-muted-foreground">
|
|
85
|
+
{mode === "login" ? "First time here?" : "Already have an account?"}{" "}
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => {
|
|
89
|
+
setMode(mode === "login" ? "signup" : "login");
|
|
90
|
+
setError(null);
|
|
91
|
+
}}
|
|
92
|
+
className="font-medium text-foreground underline underline-offset-2"
|
|
93
|
+
>
|
|
94
|
+
{mode === "login" ? "Create an account" : "Sign in"}
|
|
95
|
+
</button>
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// `ApiError` carries a stable `.code`, so branch on the code rather than the
|
|
102
|
+
// message — the copy here is ours, the message is the framework's.
|
|
103
|
+
function messageFor(err: unknown): string {
|
|
104
|
+
if (err instanceof ApiError) {
|
|
105
|
+
switch (err.code) {
|
|
106
|
+
case "INVALID_CREDENTIALS":
|
|
107
|
+
return "Wrong email or password.";
|
|
108
|
+
case "USER_EXISTS":
|
|
109
|
+
return "That email is already registered — sign in instead.";
|
|
110
|
+
case "WEAK_PASSWORD":
|
|
111
|
+
return "Pick a longer password — at least 10 characters.";
|
|
112
|
+
case "PWNED_PASSWORD":
|
|
113
|
+
return "That password has appeared in a known breach. Choose another.";
|
|
114
|
+
case "RATE_LIMITED":
|
|
115
|
+
return "Too many attempts — try again in a minute.";
|
|
116
|
+
default:
|
|
117
|
+
return err.message;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (err instanceof Error) return err.message;
|
|
121
|
+
return "Something went wrong. Try again.";
|
|
122
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { AuthForm } from "./auth-form";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Sign in",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function LoginPage({ auth, response }: PageProps) {
|
|
11
|
+
// Already signed in? Nothing to do here.
|
|
12
|
+
if (auth.user_id && !auth.user_id.startsWith("guest_")) {
|
|
13
|
+
response.redirect("/");
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<main className="flex min-h-screen items-center justify-center px-6">
|
|
19
|
+
<div className="w-full max-w-[320px]">
|
|
20
|
+
<div className="mb-7 flex items-center gap-2">
|
|
21
|
+
<span className="flex size-7 items-center justify-center rounded-md bg-primary text-[12px] font-bold text-primary-foreground">
|
|
22
|
+
C
|
|
23
|
+
</span>
|
|
24
|
+
<span className="text-[15px] font-semibold tracking-tight">CRM</span>
|
|
25
|
+
</div>
|
|
26
|
+
<h1 className="text-[19px] font-semibold tracking-tight">
|
|
27
|
+
Sign in to your workspace
|
|
28
|
+
</h1>
|
|
29
|
+
<p className="mt-1 text-[13px] text-muted-foreground">
|
|
30
|
+
Your team shares one pipeline. Anyone with an account sees it.
|
|
31
|
+
</p>
|
|
32
|
+
<div className="mt-6">
|
|
33
|
+
<AuthForm />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</main>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -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,36 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { InvoicesView } from "./invoices-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Invoices",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `/` — the invoice 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
|
+
* The signed-in email comes from the User row — `PageAuth` is
|
|
17
|
+
* `{ user_id, is_admin, tenant_id, roles }` and carries no email.
|
|
18
|
+
*/
|
|
19
|
+
export default function InvoicesPage({
|
|
20
|
+
auth,
|
|
21
|
+
response,
|
|
22
|
+
searchParams,
|
|
23
|
+
serverData,
|
|
24
|
+
}: PageProps) {
|
|
25
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
26
|
+
response.redirect("/login");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
30
|
+
return (
|
|
31
|
+
<InvoicesView
|
|
32
|
+
email={me?.email ?? ""}
|
|
33
|
+
openNew={searchParams?.new === "invoice"}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -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,174 @@
|
|
|
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 type { Invoice, LineItem, Payment } from "@/lib/billing";
|
|
10
|
+
|
|
11
|
+
export interface ClientRow {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
email?: string | null;
|
|
15
|
+
address?: string | null;
|
|
16
|
+
taxId?: string | null;
|
|
17
|
+
createdAt?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export type InvoiceRow = Invoice & { ownerId?: string | null };
|
|
20
|
+
export type LineItemRow = LineItem & { position?: number };
|
|
21
|
+
export type PaymentRow = Payment & { reference?: string | null };
|
|
22
|
+
export interface UserRow {
|
|
23
|
+
id: string;
|
|
24
|
+
email: string;
|
|
25
|
+
displayName?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkspaceData {
|
|
29
|
+
clients: ClientRow[];
|
|
30
|
+
invoices: InvoiceRow[];
|
|
31
|
+
items: LineItemRow[];
|
|
32
|
+
payments: PaymentRow[];
|
|
33
|
+
clientName: (id: string | null | undefined) => string | null;
|
|
34
|
+
loading: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The app shell, and the ONLY place that touches `db`.
|
|
39
|
+
*
|
|
40
|
+
* Every view below receives plain data through the render prop and reports
|
|
41
|
+
* changes through callbacks, which is what lets the components in `components/`
|
|
42
|
+
* render from fixtures in a test with nothing mocked.
|
|
43
|
+
*
|
|
44
|
+
* The queries are live: recording a payment updates the balance and flips the
|
|
45
|
+
* status on every open tab, so two people chasing the same overdue invoice stop
|
|
46
|
+
* the moment one of them marks it paid.
|
|
47
|
+
*/
|
|
48
|
+
export function Workspace({
|
|
49
|
+
email,
|
|
50
|
+
pathname,
|
|
51
|
+
children,
|
|
52
|
+
}: {
|
|
53
|
+
email: string;
|
|
54
|
+
pathname: string;
|
|
55
|
+
children: (data: WorkspaceData) => React.ReactNode;
|
|
56
|
+
}) {
|
|
57
|
+
const router = useRouter();
|
|
58
|
+
const { signOut } = useAuth();
|
|
59
|
+
const { data: invoices, loading } = db.useQuery<InvoiceRow>("Invoice");
|
|
60
|
+
const { data: clients } = db.useQuery<ClientRow>("Client");
|
|
61
|
+
const { data: items } = db.useQuery<LineItemRow>("LineItem");
|
|
62
|
+
const { data: payments } = db.useQuery<PaymentRow>("Payment");
|
|
63
|
+
|
|
64
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
65
|
+
const seeded = useRef(false);
|
|
66
|
+
|
|
67
|
+
const invoiceList = invoices ?? [];
|
|
68
|
+
const clientList = clients ?? [];
|
|
69
|
+
|
|
70
|
+
// A brand-new workspace gets a realistic book once, so the list never opens
|
|
71
|
+
// empty. `seedWorkspace` no-ops when anything exists; the ref stops a
|
|
72
|
+
// re-render from firing a second call mid-flight.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (loading || seeded.current) return;
|
|
75
|
+
if (invoiceList.length > 0) {
|
|
76
|
+
seeded.current = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
seeded.current = true;
|
|
80
|
+
callFn("seedWorkspace", {}).catch(() => {
|
|
81
|
+
seeded.current = false;
|
|
82
|
+
});
|
|
83
|
+
}, [loading, invoiceList.length]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
function onKey(event: KeyboardEvent) {
|
|
87
|
+
const target = event.target as HTMLElement | null;
|
|
88
|
+
const typing =
|
|
89
|
+
!!target &&
|
|
90
|
+
(target.tagName === "INPUT" ||
|
|
91
|
+
target.tagName === "TEXTAREA" ||
|
|
92
|
+
target.isContentEditable);
|
|
93
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
setPaletteOpen((open) => !open);
|
|
96
|
+
} else if (event.key === "/" && !typing) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
setPaletteOpen(true);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
window.addEventListener("keydown", onKey);
|
|
102
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const clientById = useMemo(
|
|
106
|
+
() => new Map(clientList.map((c) => [c.id, c] as const)),
|
|
107
|
+
[clientList],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const searchIndex = useMemo<SearchItem[]>(
|
|
111
|
+
() => [
|
|
112
|
+
...invoiceList.map((invoice) => ({
|
|
113
|
+
id: `invoice:${invoice.id}`,
|
|
114
|
+
type: "deal" as const,
|
|
115
|
+
title: invoice.number,
|
|
116
|
+
subtitle: clientById.get(invoice.clientId ?? "")?.name,
|
|
117
|
+
href: `/invoices/${invoice.id}`,
|
|
118
|
+
})),
|
|
119
|
+
...clientList.map((client) => ({
|
|
120
|
+
id: `client:${client.id}`,
|
|
121
|
+
type: "company" as const,
|
|
122
|
+
title: client.name,
|
|
123
|
+
subtitle: client.email ?? undefined,
|
|
124
|
+
href: "/clients",
|
|
125
|
+
})),
|
|
126
|
+
],
|
|
127
|
+
[invoiceList, clientList, clientById],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const data: WorkspaceData = {
|
|
131
|
+
clients: clientList,
|
|
132
|
+
invoices: invoiceList,
|
|
133
|
+
items: items ?? [],
|
|
134
|
+
payments: payments ?? [],
|
|
135
|
+
clientName: (id) => clientById.get(id ?? "")?.name ?? null,
|
|
136
|
+
loading,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="flex h-screen overflow-hidden">
|
|
141
|
+
<Sidebar
|
|
142
|
+
workspace="Invoices"
|
|
143
|
+
email={email}
|
|
144
|
+
pathname={pathname}
|
|
145
|
+
counts={{ "/": invoiceList.length, "/clients": clientList.length }}
|
|
146
|
+
onOpenCommand={() => setPaletteOpen(true)}
|
|
147
|
+
onSignOut={async () => {
|
|
148
|
+
await signOut();
|
|
149
|
+
window.location.assign("/login");
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
<main className="flex min-w-0 flex-1 flex-col">{children(data)}</main>
|
|
153
|
+
|
|
154
|
+
<CommandPalette
|
|
155
|
+
open={paletteOpen}
|
|
156
|
+
items={searchIndex}
|
|
157
|
+
actions={[
|
|
158
|
+
{
|
|
159
|
+
id: "new-invoice",
|
|
160
|
+
label: "New invoice",
|
|
161
|
+
run: () => router.push("/?new=invoice"),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "go-clients",
|
|
165
|
+
label: "Go to Clients",
|
|
166
|
+
run: () => router.push("/clients"),
|
|
167
|
+
},
|
|
168
|
+
]}
|
|
169
|
+
onClose={() => setPaletteOpen(false)}
|
|
170
|
+
onSelect={(item) => router.push(item.href)}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|