@pylonsync/create-pylon 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +30 -7
- package/package.json +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/README.md +28 -3
- package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
- package/templates/agency/app/(marketing)/page.tsx +3 -3
- package/templates/agency/app/(marketing)/work/page.tsx +3 -3
- package/templates/agency/app/auth-form.tsx +4 -4
- package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
- package/templates/agency/app/dashboard/page.tsx +14 -16
- package/templates/agency/app/icon.svg +3 -0
- package/templates/agency/app/login/page.tsx +12 -14
- package/templates/agency/components/marketing.tsx +12 -5
- package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
- package/templates/agency/lib/agency.ts +22 -0
- package/templates/agency/lib/demo.ts +118 -0
- package/templates/agency/lib/site.config.ts +19 -13
- package/templates/agency/public/favicon.ico +0 -0
- package/templates/agency/tests/agency.test.ts +57 -0
- package/templates/ai-chat/.env.example +13 -27
- package/templates/ai-chat/README.md +53 -60
- package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
- package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
- package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
- package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
- package/templates/ai-chat/app/(chat)/page.tsx +7 -11
- package/templates/ai-chat/app/auth-form.tsx +45 -38
- package/templates/ai-chat/app/error.tsx +9 -12
- package/templates/ai-chat/app/globals.css +464 -102
- package/templates/ai-chat/app/layout.tsx +11 -19
- package/templates/ai-chat/app/login/page.tsx +39 -24
- package/templates/ai-chat/app.ts +118 -43
- package/templates/ai-chat/components/chat/composer.tsx +193 -0
- package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
- package/templates/ai-chat/components/chat/icons.tsx +198 -0
- package/templates/ai-chat/components/chat/markdown.tsx +164 -0
- package/templates/ai-chat/components/chat/message.tsx +183 -0
- package/templates/ai-chat/components/chat/setup.tsx +71 -0
- package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
- package/templates/ai-chat/functions/_beginTurn.ts +145 -0
- package/templates/ai-chat/functions/_finishTurn.ts +55 -0
- package/templates/ai-chat/functions/assistantStatus.ts +13 -0
- package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
- package/templates/ai-chat/functions/createConversation.ts +28 -0
- package/templates/ai-chat/functions/deleteConversation.ts +15 -20
- package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
- package/templates/ai-chat/functions/renameConversation.ts +19 -0
- package/templates/ai-chat/functions/respond.ts +88 -0
- package/templates/ai-chat/functions/seedExamples.ts +46 -0
- package/templates/ai-chat/functions/stopResponse.ts +30 -0
- package/templates/ai-chat/lib/brand-icon.ts +7 -0
- package/templates/ai-chat/lib/chat.ts +277 -0
- package/templates/ai-chat/lib/claim.ts +15 -0
- package/templates/ai-chat/lib/examples.ts +179 -0
- package/templates/ai-chat/lib/highlight.ts +107 -0
- package/templates/ai-chat/lib/markdown.ts +317 -0
- package/templates/ai-chat/lib/provider.ts +38 -0
- package/templates/ai-chat/lib/site.config.ts +55 -39
- package/templates/ai-chat/tests/chat.test.ts +174 -0
- package/templates/ai-chat/tests/components.test.tsx +57 -0
- package/templates/ai-chat/tests/markdown.test.ts +88 -0
- package/templates/ai-chat/tests/provider.test.ts +32 -0
- package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
- package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
- package/templates/backend/consumer/apps/api/app.ts +166 -0
- package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
- package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
- package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
- package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
- package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
- package/templates/backend/consumer/apps/api/package.json +8 -2
- package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
- package/templates/chat/README.md +43 -21
- package/templates/chat/app/chat-client.tsx +495 -85
- package/templates/chat/app/error.tsx +1 -1
- package/templates/chat/app/globals.css +170 -54
- package/templates/chat/app/layout.tsx +12 -9
- package/templates/chat/app/not-found.tsx +4 -2
- package/templates/chat/app/page.tsx +9 -19
- package/templates/chat/app.ts +96 -17
- package/templates/chat/components/chat/avatar.tsx +45 -0
- package/templates/chat/components/chat/composer.tsx +110 -0
- package/templates/chat/components/chat/message-list.tsx +285 -0
- package/templates/chat/components/chat/sidebar.tsx +190 -0
- package/templates/chat/functions/joinChat.ts +83 -0
- package/templates/chat/functions/renameMember.ts +25 -0
- package/templates/chat/lib/chat.ts +188 -0
- package/templates/chat/lib/seed.ts +78 -0
- package/templates/chat/tests/chat.test.ts +159 -0
- package/templates/chat/tests/composer.test.tsx +54 -0
- package/templates/consumer/README.md +3 -3
- package/templates/consumer/app/globals.css +10 -3
- package/templates/consumer/app/icon.svg +1 -0
- package/templates/consumer/components/social/avatar.tsx +25 -11
- package/templates/consumer/functions/seedFeed.ts +3 -3
- package/templates/consumer/lib/seed.ts +43 -85
- package/templates/consumer/public/favicon.ico +0 -0
- package/templates/consumer/public/images/posts/arch.jpg +0 -0
- package/templates/consumer/public/images/posts/coffee.jpg +0 -0
- package/templates/consumer/public/images/posts/dog.jpg +0 -0
- package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
- package/templates/consumer/public/images/posts/ramen.jpg +0 -0
- package/templates/consumer/public/images/posts/run.jpg +0 -0
- package/templates/consumer/public/images/posts/sunset.jpg +0 -0
- package/templates/consumer/tests/seed.test.ts +39 -0
- package/templates/creator/.env.example +7 -0
- package/templates/creator/README.md +24 -5
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
- package/templates/creator/app/auth-form.tsx +7 -7
- package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
- package/templates/creator/app/dashboard/page.tsx +18 -18
- package/templates/creator/app/icon.svg +2 -0
- package/templates/creator/app/login/page.tsx +14 -14
- package/templates/creator/app.ts +4 -4
- package/templates/creator/functions/seedDemo.ts +32 -0
- package/templates/creator/functions/subscribe.ts +4 -15
- package/templates/creator/lib/demo.ts +94 -0
- package/templates/creator/lib/site.config.ts +15 -10
- package/templates/creator/lib/stats.ts +35 -0
- package/templates/creator/public/favicon.ico +0 -0
- package/templates/creator/tests/counter.test.ts +92 -0
- package/templates/creator/tests/live-count.test.tsx +45 -0
- package/templates/crm/README.md +22 -1
- package/templates/crm/app/apple-icon.png +0 -0
- package/templates/crm/app/companies/companies-view.tsx +32 -14
- package/templates/crm/app/companies/page.tsx +2 -1
- package/templates/crm/app/contacts/contacts-view.tsx +34 -10
- package/templates/crm/app/contacts/page.tsx +2 -1
- package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
- package/templates/crm/app/deals/[id]/page.tsx +2 -1
- package/templates/crm/app/globals.css +29 -14
- package/templates/crm/app/icon.svg +1 -0
- package/templates/crm/app/layout.tsx +7 -5
- package/templates/crm/app/page.tsx +2 -1
- package/templates/crm/app/pipeline-view.tsx +5 -5
- package/templates/crm/app/workspace.tsx +29 -17
- package/templates/crm/app.ts +5 -5
- package/templates/crm/components/activity-timeline.tsx +14 -4
- package/templates/crm/components/app-shell.tsx +94 -0
- package/templates/crm/components/auth-form.tsx +35 -5
- package/templates/crm/components/avatar.tsx +5 -1
- package/templates/crm/components/board-skeleton.tsx +5 -5
- package/templates/crm/components/brand-mark.tsx +25 -0
- package/templates/crm/components/command-palette.tsx +17 -17
- package/templates/crm/components/data-table.tsx +97 -36
- package/templates/crm/components/deal-card.tsx +23 -27
- package/templates/crm/components/filter-tabs.tsx +101 -0
- package/templates/crm/components/metrics-bar.tsx +6 -6
- package/templates/crm/components/nav.tsx +15 -0
- package/templates/crm/components/page-header.tsx +43 -7
- package/templates/crm/components/pipeline-board.tsx +16 -10
- package/templates/crm/components/require-auth.tsx +24 -26
- package/templates/crm/components/sidebar.tsx +54 -49
- package/templates/crm/components/stage-badge.tsx +4 -2
- package/templates/crm/components/ui/button.tsx +5 -4
- package/templates/crm/components/ui/dialog.tsx +1 -1
- package/templates/crm/components/ui/input.tsx +1 -1
- package/templates/crm/components/ui/select.tsx +1 -1
- package/templates/crm/components/ui/textarea.tsx +1 -1
- package/templates/crm/functions/seedWorkspace.ts +32 -17
- package/templates/crm/lib/brand.ts +9 -0
- package/templates/crm/lib/demo-team.ts +51 -0
- package/templates/crm/lib/pipeline.ts +37 -0
- package/templates/crm/lib/search.ts +4 -3
- package/templates/crm/lib/seed.ts +53 -8
- package/templates/crm/public/favicon.ico +0 -0
- package/templates/crm/tests/components.test.tsx +5 -5
- package/templates/crm/tests/demo-team.test.ts +18 -0
- package/templates/crm/tests/pipeline.test.ts +59 -3
- package/templates/directory/.env.example +7 -0
- package/templates/directory/README.md +22 -3
- package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
- package/templates/directory/app/(marketing)/page.tsx +6 -4
- package/templates/directory/app/auth-form.tsx +4 -4
- package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
- package/templates/directory/app/dashboard/page.tsx +16 -14
- package/templates/directory/app/icon.svg +4 -0
- package/templates/directory/app/login/page.tsx +16 -15
- package/templates/directory/functions/seedDemo.ts +43 -0
- package/templates/directory/lib/demo.ts +145 -0
- package/templates/directory/lib/directory.ts +27 -0
- package/templates/directory/lib/site.config.ts +21 -16
- package/templates/directory/public/favicon.ico +0 -0
- package/templates/directory/tests/directory.test.ts +46 -0
- package/templates/expo/consumer/apps/expo/App.tsx +360 -288
- package/templates/expo/consumer/apps/expo/app.json +9 -1
- package/templates/expo/consumer/apps/expo/package.json +2 -1
- package/templates/helpdesk/README.md +21 -0
- package/templates/helpdesk/app/apple-icon.png +0 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
- package/templates/helpdesk/app/customers/page.tsx +2 -1
- package/templates/helpdesk/app/globals.css +29 -14
- package/templates/helpdesk/app/icon.svg +1 -0
- package/templates/helpdesk/app/inbox-view.tsx +66 -47
- package/templates/helpdesk/app/layout.tsx +7 -5
- package/templates/helpdesk/app/page.tsx +4 -7
- package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
- package/templates/helpdesk/app/workspace.tsx +33 -22
- package/templates/helpdesk/app.ts +2 -2
- package/templates/helpdesk/components/app-shell.tsx +94 -0
- package/templates/helpdesk/components/auth-form.tsx +35 -5
- package/templates/helpdesk/components/avatar.tsx +5 -1
- package/templates/helpdesk/components/brand-mark.tsx +28 -0
- package/templates/helpdesk/components/command-palette.tsx +17 -17
- package/templates/helpdesk/components/data-table.tsx +97 -36
- package/templates/helpdesk/components/filter-tabs.tsx +101 -0
- package/templates/helpdesk/components/nav.tsx +14 -0
- package/templates/helpdesk/components/page-header.tsx +43 -7
- package/templates/helpdesk/components/priority-badge.tsx +49 -6
- package/templates/helpdesk/components/require-auth.tsx +24 -26
- package/templates/helpdesk/components/sidebar.tsx +54 -48
- package/templates/helpdesk/components/sla-indicator.tsx +5 -8
- package/templates/helpdesk/components/thread.tsx +7 -7
- package/templates/helpdesk/components/ticket-list.tsx +79 -19
- package/templates/helpdesk/components/ui/button.tsx +5 -4
- package/templates/helpdesk/components/ui/dialog.tsx +1 -1
- package/templates/helpdesk/components/ui/input.tsx +1 -1
- package/templates/helpdesk/components/ui/select.tsx +1 -1
- package/templates/helpdesk/components/ui/textarea.tsx +1 -1
- package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
- package/templates/helpdesk/lib/brand.ts +9 -0
- package/templates/helpdesk/lib/demo-team.ts +51 -0
- package/templates/helpdesk/lib/search.ts +4 -3
- package/templates/helpdesk/lib/seed.ts +43 -2
- package/templates/helpdesk/lib/tickets.ts +68 -20
- package/templates/helpdesk/public/favicon.ico +0 -0
- package/templates/helpdesk/tests/components.test.tsx +10 -0
- package/templates/helpdesk/tests/demo-team.test.ts +18 -0
- package/templates/helpdesk/tests/tickets.test.ts +70 -0
- package/templates/imessage/.env.example +49 -0
- package/templates/imessage/AGENTS.md +153 -0
- package/templates/imessage/CLAUDE.md +7 -0
- package/templates/imessage/README.md +187 -0
- package/templates/imessage/app/console.tsx +382 -0
- package/templates/imessage/app/contacts/page.tsx +13 -0
- package/templates/imessage/app/error.tsx +16 -0
- package/templates/imessage/app/gate.tsx +31 -0
- package/templates/imessage/app/globals.css +183 -0
- package/templates/imessage/app/layout.tsx +17 -0
- package/templates/imessage/app/login/login-form.tsx +115 -0
- package/templates/imessage/app/login/page.tsx +30 -0
- package/templates/imessage/app/not-found.tsx +13 -0
- package/templates/imessage/app/page.tsx +14 -0
- package/templates/imessage/app/robots.ts +6 -0
- package/templates/imessage/app/setup/page.tsx +13 -0
- package/templates/imessage/app/sign-out.tsx +20 -0
- package/templates/imessage/app.ts +299 -0
- package/templates/imessage/bunfig +4 -0
- package/templates/imessage/components/avatar.tsx +46 -0
- package/templates/imessage/components/contact-panel.tsx +263 -0
- package/templates/imessage/components/contacts-view.tsx +172 -0
- package/templates/imessage/components/conversation-list.tsx +129 -0
- package/templates/imessage/components/nav-rail.tsx +67 -0
- package/templates/imessage/components/setup-view.tsx +408 -0
- package/templates/imessage/components/thread.tsx +229 -0
- package/templates/imessage/components/ui/badge.tsx +37 -0
- package/templates/imessage/components/ui/button.tsx +56 -0
- package/templates/imessage/components/ui/card.tsx +90 -0
- package/templates/imessage/components/ui/dialog.tsx +110 -0
- package/templates/imessage/components/ui/input.tsx +30 -0
- package/templates/imessage/components/ui/label.tsx +21 -0
- package/templates/imessage/components/ui/select.tsx +39 -0
- package/templates/imessage/components/ui/table.tsx +124 -0
- package/templates/imessage/components/ui/textarea.tsx +24 -0
- package/templates/imessage/components.json +20 -0
- package/templates/imessage/functions/answerNow.ts +34 -0
- package/templates/imessage/functions/assistant.ts +96 -0
- package/templates/imessage/functions/beginTurn.ts +101 -0
- package/templates/imessage/functions/cancelReminder.ts +13 -0
- package/templates/imessage/functions/claimOutbound.ts +44 -0
- package/templates/imessage/functions/clearDemo.ts +18 -0
- package/templates/imessage/functions/deleteNote.ts +11 -0
- package/templates/imessage/functions/deliverOutbound.ts +55 -0
- package/templates/imessage/functions/finishTurn.ts +84 -0
- package/templates/imessage/functions/fireReminder.ts +42 -0
- package/templates/imessage/functions/ingestInbound.ts +22 -0
- package/templates/imessage/functions/ownerAccount.ts +13 -0
- package/templates/imessage/functions/processTurn.ts +74 -0
- package/templates/imessage/functions/relayApply.ts +102 -0
- package/templates/imessage/functions/relaySync.ts +63 -0
- package/templates/imessage/functions/releaseOutbound.ts +18 -0
- package/templates/imessage/functions/resolveTurn.ts +25 -0
- package/templates/imessage/functions/saveContact.ts +27 -0
- package/templates/imessage/functions/seedDemo.ts +70 -0
- package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
- package/templates/imessage/functions/sendblueWebhook.ts +49 -0
- package/templates/imessage/functions/setContactStatus.ts +17 -0
- package/templates/imessage/functions/settleOutbound.ts +35 -0
- package/templates/imessage/functions/setupStatus.ts +33 -0
- package/templates/imessage/functions/sweep.ts +58 -0
- package/templates/imessage/functions/toolCancelReminder.ts +17 -0
- package/templates/imessage/functions/toolForgetNote.ts +15 -0
- package/templates/imessage/functions/toolListNotes.ts +16 -0
- package/templates/imessage/functions/toolListReminders.ts +17 -0
- package/templates/imessage/functions/toolSaveNote.ts +22 -0
- package/templates/imessage/functions/toolSetReminder.ts +55 -0
- package/templates/imessage/functions/updateSettings.ts +48 -0
- package/templates/imessage/gitignore +11 -0
- package/templates/imessage/lib/agent-call.ts +251 -0
- package/templates/imessage/lib/assistant.ts +74 -0
- package/templates/imessage/lib/chunk.ts +135 -0
- package/templates/imessage/lib/demo.ts +68 -0
- package/templates/imessage/lib/format.ts +170 -0
- package/templates/imessage/lib/gate.ts +104 -0
- package/templates/imessage/lib/handles.ts +67 -0
- package/templates/imessage/lib/provider.ts +38 -0
- package/templates/imessage/lib/relay-protocol.ts +95 -0
- package/templates/imessage/lib/reminders.ts +41 -0
- package/templates/imessage/lib/secrets.ts +59 -0
- package/templates/imessage/lib/sendblue.ts +173 -0
- package/templates/imessage/lib/store.ts +407 -0
- package/templates/imessage/lib/transport.ts +135 -0
- package/templates/imessage/lib/utils.ts +10 -0
- package/templates/imessage/package.json +40 -0
- package/templates/imessage/relay/.env.example +14 -0
- package/templates/imessage/relay/applescript.ts +70 -0
- package/templates/imessage/relay/chatdb.ts +165 -0
- package/templates/imessage/relay/client.ts +41 -0
- package/templates/imessage/relay/main.ts +137 -0
- package/templates/imessage/relay/state.ts +35 -0
- package/templates/imessage/tests/agent-call.test.ts +112 -0
- package/templates/imessage/tests/applescript.test.ts +88 -0
- package/templates/imessage/tests/chatdb.test.ts +139 -0
- package/templates/imessage/tests/chunk.test.ts +61 -0
- package/templates/imessage/tests/fake-ctx.ts +88 -0
- package/templates/imessage/tests/ingest.test.ts +141 -0
- package/templates/imessage/tests/lib.test.ts +99 -0
- package/templates/imessage/tests/review-fixes.test.ts +138 -0
- package/templates/imessage/tests/secrets.test.ts +36 -0
- package/templates/imessage/tests/sendblue.test.ts +143 -0
- package/templates/imessage/tests/setup.ts +5 -0
- package/templates/imessage/tsconfig.json +18 -0
- package/templates/inventory/README.md +21 -0
- package/templates/inventory/app/apple-icon.png +0 -0
- package/templates/inventory/app/globals.css +29 -14
- package/templates/inventory/app/icon.svg +1 -0
- package/templates/inventory/app/layout.tsx +7 -5
- package/templates/inventory/app/movements/movements-view.tsx +32 -10
- package/templates/inventory/app/movements/page.tsx +2 -1
- package/templates/inventory/app/page.tsx +4 -6
- package/templates/inventory/app/products/[id]/page.tsx +2 -1
- package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
- package/templates/inventory/app/products-view.tsx +74 -33
- package/templates/inventory/app/workspace.tsx +34 -16
- package/templates/inventory/app.ts +10 -2
- package/templates/inventory/components/app-shell.tsx +94 -0
- package/templates/inventory/components/auth-form.tsx +35 -5
- package/templates/inventory/components/avatar.tsx +5 -1
- package/templates/inventory/components/brand-mark.tsx +25 -0
- package/templates/inventory/components/category-tile.tsx +49 -0
- package/templates/inventory/components/command-palette.tsx +17 -17
- package/templates/inventory/components/data-table.tsx +97 -36
- package/templates/inventory/components/filter-tabs.tsx +101 -0
- package/templates/inventory/components/nav.tsx +14 -0
- package/templates/inventory/components/page-header.tsx +43 -7
- package/templates/inventory/components/require-auth.tsx +24 -26
- package/templates/inventory/components/sidebar.tsx +54 -48
- package/templates/inventory/components/stock-level.tsx +30 -14
- package/templates/inventory/components/summary-bar.tsx +8 -15
- package/templates/inventory/components/ui/button.tsx +5 -4
- package/templates/inventory/components/ui/dialog.tsx +1 -1
- package/templates/inventory/components/ui/input.tsx +1 -1
- package/templates/inventory/components/ui/select.tsx +1 -1
- package/templates/inventory/components/ui/textarea.tsx +1 -1
- package/templates/inventory/functions/seedWorkspace.ts +18 -8
- package/templates/inventory/lib/brand.ts +9 -0
- package/templates/inventory/lib/demo-team.ts +51 -0
- package/templates/inventory/lib/search.ts +4 -3
- package/templates/inventory/lib/seed.ts +29 -2
- package/templates/inventory/lib/stock.ts +32 -0
- package/templates/inventory/public/favicon.ico +0 -0
- package/templates/inventory/tests/demo-team.test.ts +18 -0
- package/templates/inventory/tests/stock.test.ts +29 -0
- package/templates/invoices/README.md +16 -0
- package/templates/invoices/app/apple-icon.png +0 -0
- package/templates/invoices/app/clients/clients-view.tsx +18 -6
- package/templates/invoices/app/clients/page.tsx +2 -1
- package/templates/invoices/app/globals.css +29 -14
- package/templates/invoices/app/icon.svg +1 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
- package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
- package/templates/invoices/app/invoices-view.tsx +81 -32
- package/templates/invoices/app/layout.tsx +7 -5
- package/templates/invoices/app/page.tsx +4 -6
- package/templates/invoices/app/workspace.tsx +26 -16
- package/templates/invoices/app.ts +2 -2
- package/templates/invoices/components/app-shell.tsx +94 -0
- package/templates/invoices/components/auth-form.tsx +35 -5
- package/templates/invoices/components/avatar.tsx +5 -1
- package/templates/invoices/components/brand-mark.tsx +26 -0
- package/templates/invoices/components/command-palette.tsx +17 -17
- package/templates/invoices/components/data-table.tsx +97 -36
- package/templates/invoices/components/filter-tabs.tsx +101 -0
- package/templates/invoices/components/line-items.tsx +19 -14
- package/templates/invoices/components/nav.tsx +14 -0
- package/templates/invoices/components/page-header.tsx +43 -7
- package/templates/invoices/components/require-auth.tsx +24 -26
- package/templates/invoices/components/sidebar.tsx +54 -48
- package/templates/invoices/components/status-badge.tsx +7 -7
- package/templates/invoices/components/summary-bar.tsx +5 -5
- package/templates/invoices/components/ui/button.tsx +5 -4
- package/templates/invoices/components/ui/dialog.tsx +1 -1
- package/templates/invoices/components/ui/input.tsx +1 -1
- package/templates/invoices/components/ui/select.tsx +1 -1
- package/templates/invoices/components/ui/textarea.tsx +1 -1
- package/templates/invoices/functions/recordPayment.ts +3 -0
- package/templates/invoices/lib/billing.ts +40 -0
- package/templates/invoices/lib/brand.ts +9 -0
- package/templates/invoices/lib/search.ts +4 -3
- package/templates/invoices/public/favicon.ico +0 -0
- package/templates/invoices/tests/billing.test.ts +27 -0
- package/templates/ios/consumer/apps/ios/Package.swift +4 -3
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
- package/templates/ios/consumer/apps/ios/project.yml +13 -9
- package/templates/local-service/.env.example +7 -0
- package/templates/local-service/README.md +26 -2
- package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
- package/templates/local-service/app/(marketing)/page.tsx +66 -42
- package/templates/local-service/app/auth-form.tsx +8 -8
- package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
- package/templates/local-service/app/dashboard/page.tsx +14 -14
- package/templates/local-service/app/icon.svg +2 -0
- package/templates/local-service/app/login/page.tsx +15 -15
- package/templates/local-service/functions/seedDemo.ts +49 -0
- package/templates/local-service/lib/demo.ts +135 -0
- package/templates/local-service/lib/hours.ts +44 -0
- package/templates/local-service/lib/site.config.ts +8 -3
- package/templates/local-service/lib/slots.ts +3 -0
- package/templates/local-service/public/favicon.ico +0 -0
- package/templates/local-service/tests/booking.test.ts +104 -0
- package/templates/marketplace/README.md +23 -15
- package/templates/marketplace/app/globals.css +172 -114
- package/templates/marketplace/app/layout.tsx +75 -30
- package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
- package/templates/marketplace/app/me/page.tsx +4 -4
- package/templates/marketplace/app/page.tsx +221 -298
- package/templates/marketplace/app/sell/page.tsx +8 -11
- package/templates/marketplace/app.ts +32 -7
- package/templates/marketplace/client/ActivityFeed.tsx +311 -0
- package/templates/marketplace/client/AuthNav.tsx +24 -16
- package/templates/marketplace/client/ListingCard.tsx +118 -0
- package/templates/marketplace/client/LiveGrid.tsx +92 -0
- package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
- package/templates/marketplace/client/LoginCard.tsx +114 -136
- package/templates/marketplace/client/MarketProvider.tsx +3 -5
- package/templates/marketplace/client/MyMarket.tsx +371 -185
- package/templates/marketplace/client/OfferPanel.tsx +421 -287
- package/templates/marketplace/client/SellForm.tsx +271 -192
- package/templates/marketplace/client/ThemeToggle.tsx +1 -0
- package/templates/marketplace/client/WatchButton.tsx +16 -16
- package/templates/marketplace/client/market.ts +61 -14
- package/templates/marketplace/functions/seedMarket.ts +143 -36
- package/templates/marketplace/functions/seedOffers.ts +101 -0
- package/templates/marketplace/lib/catalog.ts +72 -1
- package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
- package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
- package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
- package/templates/marketplace/public/images/listings/watch.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +64 -2
- package/templates/marketplace/tests/listing-card.test.tsx +61 -0
- package/templates/projects/README.md +24 -3
- package/templates/projects/app/apple-icon.png +0 -0
- package/templates/projects/app/clients/clients-view.tsx +20 -6
- package/templates/projects/app/clients/page.tsx +2 -1
- package/templates/projects/app/globals.css +29 -14
- package/templates/projects/app/icon.svg +1 -0
- package/templates/projects/app/layout.tsx +7 -5
- package/templates/projects/app/page.tsx +3 -5
- package/templates/projects/app/projects/[id]/page.tsx +2 -1
- package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
- package/templates/projects/app/projects-view.tsx +105 -52
- package/templates/projects/app/workspace.tsx +41 -20
- package/templates/projects/app.ts +2 -2
- package/templates/projects/components/app-shell.tsx +94 -0
- package/templates/projects/components/auth-form.tsx +35 -5
- package/templates/projects/components/avatar.tsx +5 -1
- package/templates/projects/components/board-skeleton.tsx +3 -3
- package/templates/projects/components/brand-mark.tsx +25 -0
- package/templates/projects/components/command-palette.tsx +17 -17
- package/templates/projects/components/data-table.tsx +97 -36
- package/templates/projects/components/filter-tabs.tsx +101 -0
- package/templates/projects/components/meter.tsx +48 -0
- package/templates/projects/components/nav.tsx +14 -0
- package/templates/projects/components/page-header.tsx +43 -7
- package/templates/projects/components/require-auth.tsx +24 -26
- package/templates/projects/components/sidebar.tsx +54 -48
- package/templates/projects/components/summary-bar.tsx +49 -0
- package/templates/projects/components/task-board.tsx +10 -9
- package/templates/projects/components/time-dialog.tsx +29 -5
- package/templates/projects/components/ui/button.tsx +5 -4
- package/templates/projects/components/ui/dialog.tsx +1 -1
- package/templates/projects/components/ui/input.tsx +1 -1
- package/templates/projects/components/ui/select.tsx +1 -1
- package/templates/projects/components/ui/textarea.tsx +1 -1
- package/templates/projects/functions/seedWorkspace.ts +26 -12
- package/templates/projects/lib/brand.ts +9 -0
- package/templates/projects/lib/demo-team.ts +51 -0
- package/templates/projects/lib/search.ts +4 -3
- package/templates/projects/lib/seed.ts +35 -1
- package/templates/projects/lib/work.ts +63 -0
- package/templates/projects/public/favicon.ico +0 -0
- package/templates/projects/tests/demo-team.test.ts +18 -0
- package/templates/projects/tests/work.test.ts +65 -0
- package/templates/restaurant/.env.example +7 -0
- package/templates/restaurant/README.md +29 -3
- package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
- package/templates/restaurant/app/(marketing)/page.tsx +47 -23
- package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
- package/templates/restaurant/app/auth-form.tsx +8 -8
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
- package/templates/restaurant/app/dashboard/page.tsx +14 -14
- package/templates/restaurant/app/icon.svg +4 -0
- package/templates/restaurant/app/login/page.tsx +15 -15
- package/templates/restaurant/functions/seedDemo.ts +44 -0
- package/templates/restaurant/lib/demo.ts +144 -0
- package/templates/restaurant/lib/hours.ts +45 -0
- package/templates/restaurant/lib/site.config.ts +3 -3
- package/templates/restaurant/public/favicon.ico +0 -0
- package/templates/restaurant/tests/reservations.test.ts +67 -0
- package/templates/saas/README.md +54 -35
- package/templates/saas/app/(auth)/auth-form.tsx +25 -23
- package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/layout.tsx +27 -33
- package/templates/saas/app/(auth)/login/page.tsx +1 -1
- package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/signup/page.tsx +1 -1
- package/templates/saas/app/(marketing)/layout.tsx +127 -253
- package/templates/saas/app/(marketing)/page.tsx +133 -372
- package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
- package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
- package/templates/saas/app/dashboard/board-client.tsx +416 -0
- package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
- package/templates/saas/app/dashboard/layout.tsx +27 -13
- package/templates/saas/app/dashboard/overview-client.tsx +332 -0
- package/templates/saas/app/dashboard/page.tsx +29 -35
- package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
- package/templates/saas/app/dashboard/projects/page.tsx +11 -7
- package/templates/saas/app/dashboard/projects-client.tsx +398 -0
- package/templates/saas/app/globals.css +5 -4
- package/templates/saas/app/icon.svg +1 -0
- package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
- package/templates/saas/app/onboarding/page.tsx +6 -2
- package/templates/saas/app.ts +68 -13
- package/templates/saas/components/dashboard-shell.tsx +212 -132
- package/templates/saas/components/marketing.tsx +137 -274
- package/templates/saas/components/pricing-table.tsx +12 -11
- package/templates/saas/components/setup-checklist.tsx +31 -29
- package/templates/saas/components/task-ui.tsx +199 -0
- package/templates/saas/functions/createProject.ts +7 -13
- package/templates/saas/functions/createTask.ts +67 -0
- package/templates/saas/functions/deleteProject.ts +17 -0
- package/templates/saas/functions/deleteTask.ts +14 -0
- package/templates/saas/functions/seedWorkspace.ts +46 -0
- package/templates/saas/functions/setProjectStatus.ts +41 -0
- package/templates/saas/functions/updateProfile.ts +13 -0
- package/templates/saas/functions/updateProject.ts +4 -5
- package/templates/saas/functions/updateTask.ts +88 -0
- package/templates/saas/lib/members.ts +53 -0
- package/templates/saas/lib/names.ts +53 -0
- package/templates/saas/lib/plans.ts +11 -0
- package/templates/saas/lib/sample-workspace.ts +117 -0
- package/templates/saas/lib/site.config.ts +200 -463
- package/templates/saas/lib/tasks.ts +101 -0
- package/templates/saas/lib/use-live.ts +25 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/public/screenshots/board.png +0 -0
- package/templates/saas/public/screenshots/members.png +0 -0
- package/templates/saas/public/screenshots/overview.png +0 -0
- package/templates/saas/public/screenshots/projects.png +0 -0
- package/templates/saas/scripts/capture-screenshots.mjs +156 -0
- package/templates/saas/tests/names.test.ts +35 -0
- package/templates/saas/tests/plans.test.ts +12 -0
- package/templates/saas/tests/sample-workspace.test.ts +46 -0
- package/templates/saas/tests/tasks.test.ts +81 -0
- package/templates/shop/README.md +25 -13
- package/templates/shop/app/(marketing)/layout.tsx +94 -67
- package/templates/shop/app/(marketing)/not-found.tsx +13 -12
- package/templates/shop/app/(marketing)/page.tsx +120 -59
- package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
- package/templates/shop/app/(marketing)/success/page.tsx +75 -22
- package/templates/shop/app/globals.css +94 -1
- package/templates/shop/app/layout.tsx +4 -4
- package/templates/shop/app/login/page.tsx +4 -4
- package/templates/shop/app.ts +18 -6
- package/templates/shop/components/cart-button.tsx +33 -0
- package/templates/shop/components/marketing.tsx +17 -87
- package/templates/shop/components/shelf-count.tsx +36 -0
- package/templates/shop/functions/checkout.ts +17 -1
- package/templates/shop/lib/cart-store.ts +57 -0
- package/templates/shop/lib/shelf-store.ts +35 -0
- package/templates/shop/lib/shop.ts +27 -0
- package/templates/shop/lib/site.config.ts +108 -98
- package/templates/shop/public/images/hero.jpg +0 -0
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/shop/public/images/products/sea-salt.jpg +0 -0
- package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
- package/templates/shop/tests/shop.test.ts +64 -0
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/README.md +29 -13
- package/templates/waitlist/app/(marketing)/page.tsx +9 -7
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
- package/templates/waitlist/app/auth-form.tsx +8 -8
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
- package/templates/waitlist/app/dashboard/page.tsx +17 -16
- package/templates/waitlist/app/icon.svg +2 -0
- package/templates/waitlist/app/login/page.tsx +14 -14
- package/templates/waitlist/app.ts +6 -6
- package/templates/waitlist/functions/joinWaitlist.ts +6 -18
- package/templates/waitlist/functions/seedDemo.ts +32 -0
- package/templates/waitlist/lib/demo.ts +94 -0
- package/templates/waitlist/lib/launch.ts +19 -0
- package/templates/waitlist/lib/site.config.ts +14 -9
- package/templates/waitlist/lib/stats.ts +35 -0
- package/templates/waitlist/public/favicon.ico +0 -0
- package/templates/waitlist/tests/counter.test.ts +107 -0
- package/templates/waitlist/tests/live-count.test.tsx +48 -0
- package/templates/ai-chat/tests/example.test.ts +0 -12
- package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
- package/templates/backend/consumer/apps/api/schema.ts +0 -143
- package/templates/chat/functions/_keep.ts +0 -13
- package/templates/chat/tests/example.test.ts +0 -12
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
- package/templates/marketplace/client/LiveTicker.tsx +0 -116
- package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
- package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// Shared client-side glue for
|
|
3
|
+
// Shared client-side glue for the marketplace: types, display helpers, and the
|
|
4
4
|
// email/password auth bootstrap. Same-origin under native SSR, so no baseUrl —
|
|
5
5
|
// init() resolves window.location.origin.
|
|
6
6
|
import { init, configureClient, callFn, setSessionToken, storageKey } from "@pylonsync/react";
|
|
@@ -10,13 +10,13 @@ import { init, configureClient, callFn, setSessionToken, storageKey } from "@pyl
|
|
|
10
10
|
// keyspace disagreement, no double token migration.
|
|
11
11
|
export const APP_NAME = "__APP_NAME__";
|
|
12
12
|
|
|
13
|
-
// Prefilled demo account
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// Prefilled demo account: the shopper you sign in as. Owns three listings (so
|
|
14
|
+
// the dashboard has offers to answer) but not the bulk of the catalog, so it
|
|
15
|
+
// can buy and bid on everything else.
|
|
16
16
|
export const DEMO = {
|
|
17
17
|
email: "demo@pylon.market",
|
|
18
18
|
password: "pylondemo123",
|
|
19
|
-
name: "
|
|
19
|
+
name: "Sam Rivera",
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
22
|
// The seed seller that owns most of the catalog. Nobody logs in as this; it
|
|
@@ -27,13 +27,23 @@ const BAZAAR = {
|
|
|
27
27
|
name: "Pylon Bazaar",
|
|
28
28
|
} as const;
|
|
29
29
|
|
|
30
|
+
// A seed buyer. Nobody logs in as this; it sends the demo offers (including
|
|
31
|
+
// offers on the demo shopper's own listings) so every listing page and the
|
|
32
|
+
// dashboard have live offer activity on first boot.
|
|
33
|
+
const COLLECTOR = {
|
|
34
|
+
email: "collector@pylon.market",
|
|
35
|
+
password: "collectorseed123",
|
|
36
|
+
name: "Collector",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
30
39
|
// How many of the seeded listings the bazaar owns; the rest go to the demo.
|
|
31
|
-
const BAZAAR_COUNT =
|
|
40
|
+
const BAZAAR_COUNT = 7;
|
|
32
41
|
|
|
33
42
|
export interface Listing {
|
|
34
43
|
id: string;
|
|
35
44
|
sellerId: string;
|
|
36
45
|
sellerName: string;
|
|
46
|
+
location?: string;
|
|
37
47
|
title: string;
|
|
38
48
|
slug: string;
|
|
39
49
|
description: string;
|
|
@@ -85,12 +95,18 @@ function hash(s: string): number {
|
|
|
85
95
|
return h >>> 0;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
/** Deterministic visual fallback for listings
|
|
98
|
+
/** Deterministic visual fallback for listings without a photo. */
|
|
89
99
|
export function gradient(seed: string): string {
|
|
90
100
|
const h = hash(seed);
|
|
91
|
-
const a = h %
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
const a = 20 + (h % 60);
|
|
102
|
+
return `linear-gradient(160deg, hsl(${a} 22% 82%), hsl(${a + 12} 18% 64%))`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A stable, muted avatar color for a person's name. */
|
|
106
|
+
export function avatarColor(name: string): string {
|
|
107
|
+
const hues = [18, 32, 96, 152, 196, 262, 340];
|
|
108
|
+
const hue = hues[hash(name) % hues.length];
|
|
109
|
+
return `hsl(${hue} 32% 42%)`;
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
export function initials(title: string): string {
|
|
@@ -305,15 +321,29 @@ async function ensureAccount(
|
|
|
305
321
|
* prefilled login is one click from a working session that can actually buy
|
|
306
322
|
* and sell. Idempotent and best-effort: any failure is swallowed.
|
|
307
323
|
*
|
|
308
|
-
*
|
|
324
|
+
* Three accounts are seeded:
|
|
309
325
|
* - **bazaar** owns the bulk of the catalog, so the demo shopper has other
|
|
310
326
|
* people's listings to buy + bid on (you can't buy your own);
|
|
311
|
-
* - **demo** (the prefilled login) owns
|
|
312
|
-
*
|
|
327
|
+
* - **demo** (the prefilled login) owns three listings so the dashboard has
|
|
328
|
+
* something in it;
|
|
329
|
+
* - **collector** sends the demo offers and buys one item, so listings show
|
|
330
|
+
* offer activity and the demo shopper has offers to accept.
|
|
313
331
|
*
|
|
314
|
-
*
|
|
332
|
+
* All tokens are used transiently to seed and then discarded — the visitor
|
|
315
333
|
* stays anonymous (read-only) until they choose to sign in.
|
|
316
334
|
*/
|
|
335
|
+
/** True when the public entity API returns at least one row. */
|
|
336
|
+
async function hasAnyRow(entity: string): Promise<boolean> {
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetch(`/api/entities/${entity}?limit=1`);
|
|
339
|
+
if (!res.ok) return false;
|
|
340
|
+
const body = (await res.json()) as { data?: unknown[] };
|
|
341
|
+
return (body.data?.length ?? 0) > 0;
|
|
342
|
+
} catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
317
347
|
// Shared in-flight promise so the many islands that mount MarketProvider on
|
|
318
348
|
// one page (ticker, header, SeedOnEmpty) all await the SAME seed run instead
|
|
319
349
|
// of each firing their own — that flood was tripping the login rate limit,
|
|
@@ -329,6 +359,14 @@ export function ensureDemoSeed(options: { force?: boolean } = {}): Promise<void>
|
|
|
329
359
|
if (seedPromise) return seedPromise;
|
|
330
360
|
|
|
331
361
|
seedPromise = (async () => {
|
|
362
|
+
// The collector's offers are the last seed step, so any Offer row means
|
|
363
|
+
// the accounts and catalog already exist. Skipping here keeps a fresh
|
|
364
|
+
// browser from spending the login rate limit on seed accounts.
|
|
365
|
+
if (!options.force && (await hasAnyRow("Offer"))) {
|
|
366
|
+
localStorage.setItem("market:demo-seeded", "1");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
332
370
|
const bazaarToken = await ensureAccount(
|
|
333
371
|
BAZAAR.email,
|
|
334
372
|
BAZAAR.password,
|
|
@@ -347,6 +385,15 @@ export function ensureDemoSeed(options: { force?: boolean } = {}): Promise<void>
|
|
|
347
385
|
await callFn("seedMarket", { start: BAZAAR_COUNT }, { token: demoToken });
|
|
348
386
|
}
|
|
349
387
|
|
|
388
|
+
const collectorToken = await ensureAccount(
|
|
389
|
+
COLLECTOR.email,
|
|
390
|
+
COLLECTOR.password,
|
|
391
|
+
COLLECTOR.name,
|
|
392
|
+
);
|
|
393
|
+
if (collectorToken) {
|
|
394
|
+
await callFn("seedOffers", {}, { token: collectorToken });
|
|
395
|
+
}
|
|
396
|
+
|
|
350
397
|
// Mark done only AFTER a successful pass, so a failed/partial run retries
|
|
351
398
|
// on the next load instead of being permanently skipped.
|
|
352
399
|
localStorage.setItem("market:demo-seeded", "1");
|
|
@@ -1,30 +1,141 @@
|
|
|
1
1
|
import { mutation, v } from "@pylonsync/functions";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Demo catalog for first boot. The client bootstrap calls this twice: once as
|
|
4
|
+
// the "bazaar" account for DEMO[0..7] and once as the demo shopper for the
|
|
5
|
+
// rest, so the shopper has other people's listings to buy and a few of its
|
|
6
|
+
// own to receive offers on. Idempotent per caller: a seller that already has
|
|
7
|
+
// listings is skipped.
|
|
6
8
|
const DEMO: Array<{
|
|
7
9
|
seller: string;
|
|
10
|
+
location: string;
|
|
8
11
|
title: string;
|
|
9
12
|
description: string;
|
|
10
13
|
price: number;
|
|
11
14
|
category: string;
|
|
12
15
|
condition: string;
|
|
13
|
-
|
|
16
|
+
photo: string;
|
|
14
17
|
seed: string;
|
|
15
18
|
}> = [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
{
|
|
20
|
+
seller: "Nora Lind",
|
|
21
|
+
location: "Brooklyn, NY",
|
|
22
|
+
title: "Danish teak lounge chair, oatmeal wool",
|
|
23
|
+
description:
|
|
24
|
+
"Solid teak frame with the original joinery, oiled last spring. Cushions were reupholstered in a heavy oatmeal wool bouclé. No wobble, no cracks. Seat height 16 in, width 27 in. Pickup in Greenpoint or I can help arrange a courier.",
|
|
25
|
+
price: 680,
|
|
26
|
+
category: "furniture",
|
|
27
|
+
condition: "good",
|
|
28
|
+
photo: "lounge-chair",
|
|
29
|
+
seed: "a1f3",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
seller: "Marcus Bell",
|
|
33
|
+
location: "Dallas, TX",
|
|
34
|
+
title: "1960s steel dress watch, cream dial",
|
|
35
|
+
description:
|
|
36
|
+
"34 mm stainless case, hand-wound movement serviced in March. Keeps about +6 s/day. Cream dial has light, even patina. Comes on a brown suede strap with a spare black leather strap.",
|
|
37
|
+
price: 890,
|
|
38
|
+
category: "watches",
|
|
39
|
+
condition: "good",
|
|
40
|
+
photo: "watch",
|
|
41
|
+
seed: "b7c2",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
seller: "Theo Park",
|
|
45
|
+
location: "Portland, OR",
|
|
46
|
+
title: "Chrome 35mm SLR with 50mm f/1.7",
|
|
47
|
+
description:
|
|
48
|
+
"Fully mechanical body, shutter fires at every speed and sounds right. Light meter reads within a third of a stop. Lens is clean with no haze or fungus. Original leather strap. Tested with a roll of Portra; sample scans on request.",
|
|
49
|
+
price: 240,
|
|
50
|
+
category: "cameras",
|
|
51
|
+
condition: "good",
|
|
52
|
+
photo: "film-camera",
|
|
53
|
+
seed: "c4e9",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
seller: "Ava Moreno",
|
|
57
|
+
location: "Austin, TX",
|
|
58
|
+
title: "Walnut direct-drive turntable",
|
|
59
|
+
description:
|
|
60
|
+
"Direct-drive deck in a solid walnut plinth. Speed is stable at 33 and 45, pitch control works. New belt-free motor service, new cartridge with about 40 hours on it. Dust cover not included.",
|
|
61
|
+
price: 410,
|
|
62
|
+
category: "audio",
|
|
63
|
+
condition: "like-new",
|
|
64
|
+
photo: "turntable",
|
|
65
|
+
seed: "d8a1",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
seller: "Nora Lind",
|
|
69
|
+
location: "Brooklyn, NY",
|
|
70
|
+
title: "Cognac leather weekender",
|
|
71
|
+
description:
|
|
72
|
+
"Full-grain leather duffel with solid brass hardware and a detachable shoulder strap. Carried on about ten trips; the leather has softened and darkened evenly. Interior lining is clean. 20 x 11 x 10 in.",
|
|
73
|
+
price: 295,
|
|
74
|
+
category: "bags",
|
|
75
|
+
condition: "good",
|
|
76
|
+
photo: "weekender-bag",
|
|
77
|
+
seed: "a9c4",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
seller: "Elise Carter",
|
|
81
|
+
location: "Asheville, NC",
|
|
82
|
+
title: "Speckled stoneware table lamp",
|
|
83
|
+
description:
|
|
84
|
+
"Hand-thrown stoneware base in a sand glaze, pleated linen shade. Rewired with a cloth cord and inline switch. 22 in tall with the shade. One tiny glaze pop near the foot, shown in photos.",
|
|
85
|
+
price: 165,
|
|
86
|
+
category: "lighting",
|
|
87
|
+
condition: "like-new",
|
|
88
|
+
photo: "ceramic-lamp",
|
|
89
|
+
seed: "b3f8",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
seller: "Elise Carter",
|
|
93
|
+
location: "Asheville, NC",
|
|
94
|
+
title: "Tufted slipper chair, ivory",
|
|
95
|
+
description:
|
|
96
|
+
"Small button-tufted slipper chair in ivory cotton velvet on turned white legs. Frame is tight, fabric has no stains or pulls. Seat height 17 in, width 24 in. Works in a bedroom or reading corner.",
|
|
97
|
+
price: 240,
|
|
98
|
+
category: "furniture",
|
|
99
|
+
condition: "like-new",
|
|
100
|
+
photo: "slipper-chair",
|
|
101
|
+
seed: "m5t1",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
seller: "Sam Rivera",
|
|
105
|
+
location: "Denver, CO",
|
|
106
|
+
title: "Two-door oak wardrobe",
|
|
107
|
+
description:
|
|
108
|
+
"Solid oak wardrobe with two doors over two deep drawers. Hanging rail and one shelf inside. Doors close flush, drawers run smoothly. 40 in wide, 22 in deep, 74 in tall. Comes apart at the top for moving.",
|
|
109
|
+
price: 450,
|
|
110
|
+
category: "furniture",
|
|
111
|
+
condition: "good",
|
|
112
|
+
photo: "wardrobe",
|
|
113
|
+
seed: "n8w3",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
seller: "Sam Rivera",
|
|
117
|
+
location: "Denver, CO",
|
|
118
|
+
title: "Carbon road bike, 54 cm, matte black",
|
|
119
|
+
description:
|
|
120
|
+
"Carbon frame and fork, 54 cm, matte black. 2x11 drivetrain shifts cleanly, new chain and brake pads this season. No cracks or impact damage. About 8.4 kg as pictured.",
|
|
121
|
+
price: 1250,
|
|
122
|
+
category: "bikes",
|
|
123
|
+
condition: "good",
|
|
124
|
+
photo: "road-bike",
|
|
125
|
+
seed: "e2b6",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
seller: "Sam Rivera",
|
|
129
|
+
location: "Denver, CO",
|
|
130
|
+
title: "Black travel duffel, 55 L",
|
|
131
|
+
description:
|
|
132
|
+
"Water-resistant nylon duffel with padded shoulder straps and a separate shoe pocket. Used on two trips. All zips and buckles work. Packs flat into its own pocket.",
|
|
133
|
+
price: 70,
|
|
134
|
+
category: "bags",
|
|
135
|
+
condition: "like-new",
|
|
136
|
+
photo: "duffel",
|
|
137
|
+
seed: "p2d6",
|
|
138
|
+
},
|
|
28
139
|
];
|
|
29
140
|
|
|
30
141
|
interface SeedMarketArgs {
|
|
@@ -36,11 +147,17 @@ interface SeedMarketResult {
|
|
|
36
147
|
seeded: number;
|
|
37
148
|
}
|
|
38
149
|
|
|
150
|
+
function slugify(s: string): string {
|
|
151
|
+
return s
|
|
152
|
+
.toLowerCase()
|
|
153
|
+
.trim()
|
|
154
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
155
|
+
.replace(/^-+|-+$/g, "")
|
|
156
|
+
.slice(0, 60);
|
|
157
|
+
}
|
|
158
|
+
|
|
39
159
|
export default mutation<SeedMarketArgs, SeedMarketResult>({
|
|
40
|
-
//
|
|
41
|
-
// caller. The bootstrap calls this twice: once as a "bazaar" seller for the
|
|
42
|
-
// bulk of the catalog (so the demo buyer can bid on it), and once as the
|
|
43
|
-
// demo account for a couple of its own listings.
|
|
160
|
+
// auth defaults to "user": every seeded listing is owned by the caller.
|
|
44
161
|
args: {
|
|
45
162
|
start: v.optional(v.number()),
|
|
46
163
|
end: v.optional(v.number()),
|
|
@@ -48,32 +165,22 @@ export default mutation<SeedMarketArgs, SeedMarketResult>({
|
|
|
48
165
|
async handler(ctx, args) {
|
|
49
166
|
if (!ctx.auth.userId) throw ctx.error("UNAUTHENTICATED", "sign in first");
|
|
50
167
|
|
|
51
|
-
|
|
52
|
-
// two seed calls (and any reloads) don't duplicate.
|
|
53
|
-
const all = await ctx.db.list("Listing") as Array<{ sellerId: string }>;
|
|
168
|
+
const all = (await ctx.db.list("Listing")) as Array<{ sellerId: string }>;
|
|
54
169
|
if (all.some((l) => l.sellerId === ctx.auth.userId)) return { seeded: 0 };
|
|
55
170
|
|
|
56
171
|
const start = args.start ?? 0;
|
|
57
172
|
const end = args.end ?? DEMO.length;
|
|
58
173
|
const slice = DEMO.slice(start, end);
|
|
59
174
|
|
|
60
|
-
// Stagger createdAt so
|
|
61
|
-
// seller id is the caller
|
|
62
|
-
// framework would reject any other value. `seller` stays a display name.
|
|
63
|
-
const slugify = (s: string) =>
|
|
64
|
-
s
|
|
65
|
-
.toLowerCase()
|
|
66
|
-
.trim()
|
|
67
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
68
|
-
.replace(/^-+|-+$/g, "")
|
|
69
|
-
.slice(0, 60);
|
|
70
|
-
|
|
175
|
+
// Stagger createdAt so "newest first" follows the array order. The
|
|
176
|
+
// seller id is the caller; `sellerName` is a display name only.
|
|
71
177
|
const now = Date.now();
|
|
72
178
|
let n = 0;
|
|
73
179
|
for (const d of slice) {
|
|
74
180
|
await ctx.db.insert("Listing", {
|
|
75
181
|
sellerId: ctx.auth.userId,
|
|
76
182
|
sellerName: d.seller,
|
|
183
|
+
location: d.location,
|
|
77
184
|
title: d.title,
|
|
78
185
|
slug: `${slugify(d.title) || "item"}-${d.seed}`,
|
|
79
186
|
description: d.description,
|
|
@@ -81,9 +188,9 @@ export default mutation<SeedMarketArgs, SeedMarketResult>({
|
|
|
81
188
|
category: d.category,
|
|
82
189
|
condition: d.condition,
|
|
83
190
|
status: "active",
|
|
84
|
-
imageUrl: d.
|
|
191
|
+
imageUrl: `/images/listings/${d.photo}.webp`,
|
|
85
192
|
seed: d.seed,
|
|
86
|
-
createdAt: new Date(now - (start + n) *
|
|
193
|
+
createdAt: new Date(now - ((start + n) * 23 + 4) * 60_000).toISOString(),
|
|
87
194
|
});
|
|
88
195
|
n++;
|
|
89
196
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mutation } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// Demo offers for first boot, made by the caller (the "collector" account in
|
|
4
|
+
// client/market.ts). Each entry is an ordinary offer the caller could send
|
|
5
|
+
// through makeOffer, or a purchase it could make through buyNow, so this
|
|
6
|
+
// grants no extra power: it skips listings the caller owns and listings that
|
|
7
|
+
// are no longer active. Idempotent per caller: a buyer that already has
|
|
8
|
+
// offers is skipped.
|
|
9
|
+
const PLAN: Array<{
|
|
10
|
+
seed: string;
|
|
11
|
+
amount?: number;
|
|
12
|
+
buyerName: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
minutesAgo: number;
|
|
15
|
+
buy?: boolean;
|
|
16
|
+
}> = [
|
|
17
|
+
{ seed: "a1f3", amount: 600, buyerName: "Leo T.", minutesAgo: 31 },
|
|
18
|
+
{ seed: "a1f3", amount: 620, buyerName: "Ines M.", message: "Could pick up this weekend.", minutesAgo: 6 },
|
|
19
|
+
{ seed: "b7c2", amount: 800, buyerName: "Kenji A.", minutesAgo: 14 },
|
|
20
|
+
{ seed: "c4e9", amount: 215, buyerName: "Dana R.", message: "Does the meter need a battery?", minutesAgo: 26 },
|
|
21
|
+
{ seed: "d8a1", amount: 380, buyerName: "Ines M.", minutesAgo: 22 },
|
|
22
|
+
{ seed: "a9c4", amount: 260, buyerName: "Leo T.", minutesAgo: 44 },
|
|
23
|
+
{ seed: "e2b6", buyerName: "Kenji A.", minutesAgo: 11, buy: true },
|
|
24
|
+
{ seed: "n8w3", amount: 400, buyerName: "Dana R.", message: "I can pick up Saturday morning.", minutesAgo: 3 },
|
|
25
|
+
{ seed: "p2d6", amount: 60, buyerName: "Ines M.", minutesAgo: 17 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
interface SeedOffersResult {
|
|
29
|
+
seeded: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Row = {
|
|
33
|
+
id: string;
|
|
34
|
+
seed: string;
|
|
35
|
+
title: string;
|
|
36
|
+
price: number;
|
|
37
|
+
sellerId: string;
|
|
38
|
+
status: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default mutation<Record<string, never>, SeedOffersResult>({
|
|
42
|
+
args: {},
|
|
43
|
+
async handler(ctx) {
|
|
44
|
+
const buyerId = ctx.auth.userId;
|
|
45
|
+
if (!buyerId) throw ctx.error("UNAUTHENTICATED", "sign in first");
|
|
46
|
+
|
|
47
|
+
const existing = (await ctx.db.list("Offer")) as Array<{ buyerId: string }>;
|
|
48
|
+
if (existing.some((o) => o.buyerId === buyerId)) return { seeded: 0 };
|
|
49
|
+
|
|
50
|
+
const listings = (await ctx.db.list("Listing")) as Row[];
|
|
51
|
+
const bySeed = new Map(listings.map((l) => [l.seed, l]));
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
let seeded = 0;
|
|
54
|
+
|
|
55
|
+
for (const entry of PLAN) {
|
|
56
|
+
const listing = bySeed.get(entry.seed);
|
|
57
|
+
if (!listing || listing.sellerId === buyerId || listing.status !== "active") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const createdAt = new Date(now - entry.minutesAgo * 60_000).toISOString();
|
|
61
|
+
|
|
62
|
+
if (entry.buy) {
|
|
63
|
+
const offerId = await ctx.db.insert("Offer", {
|
|
64
|
+
listingId: listing.id,
|
|
65
|
+
listingTitle: listing.title,
|
|
66
|
+
sellerId: listing.sellerId,
|
|
67
|
+
buyerId,
|
|
68
|
+
buyerName: entry.buyerName,
|
|
69
|
+
amount: listing.price,
|
|
70
|
+
message: "Bought at list price",
|
|
71
|
+
status: "accepted",
|
|
72
|
+
createdAt,
|
|
73
|
+
});
|
|
74
|
+
await ctx.db.update("Listing", listing.id, { status: "sold" });
|
|
75
|
+
listing.status = "sold";
|
|
76
|
+
const siblings = (await ctx.db.query("Offer", {
|
|
77
|
+
listingId: listing.id,
|
|
78
|
+
})) as Array<{ id: string; status: string }>;
|
|
79
|
+
for (const o of siblings) {
|
|
80
|
+
if (o.id !== offerId && o.status === "pending") {
|
|
81
|
+
await ctx.db.update("Offer", o.id, { status: "declined" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
await ctx.db.insert("Offer", {
|
|
86
|
+
listingId: listing.id,
|
|
87
|
+
listingTitle: listing.title,
|
|
88
|
+
sellerId: listing.sellerId,
|
|
89
|
+
buyerId,
|
|
90
|
+
buyerName: entry.buyerName,
|
|
91
|
+
amount: entry.amount ?? listing.price,
|
|
92
|
+
message: entry.message ?? "",
|
|
93
|
+
status: "pending",
|
|
94
|
+
createdAt,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
seeded++;
|
|
98
|
+
}
|
|
99
|
+
return { seeded };
|
|
100
|
+
},
|
|
101
|
+
});
|
|
@@ -1,7 +1,39 @@
|
|
|
1
|
-
import type { Listing } from "../client/market";
|
|
1
|
+
import type { Listing, Offer } from "../client/market";
|
|
2
2
|
|
|
3
3
|
export type CatalogSort = "latest" | "price-low" | "price-high";
|
|
4
4
|
|
|
5
|
+
export interface Category {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Browse + sell categories. The id is what a Listing stores; the label is
|
|
11
|
+
// what the UI shows.
|
|
12
|
+
export const CATEGORIES: Category[] = [
|
|
13
|
+
{ id: "furniture", label: "Furniture" },
|
|
14
|
+
{ id: "lighting", label: "Lighting" },
|
|
15
|
+
{ id: "audio", label: "Audio" },
|
|
16
|
+
{ id: "cameras", label: "Cameras" },
|
|
17
|
+
{ id: "watches", label: "Watches" },
|
|
18
|
+
{ id: "clothing", label: "Clothing" },
|
|
19
|
+
{ id: "shoes", label: "Shoes" },
|
|
20
|
+
{ id: "bags", label: "Bags" },
|
|
21
|
+
{ id: "bikes", label: "Bikes" },
|
|
22
|
+
{ id: "instruments", label: "Instruments" },
|
|
23
|
+
{ id: "kitchen", label: "Kitchen" },
|
|
24
|
+
{ id: "other", label: "Other" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const CONDITIONS = ["new", "like-new", "good", "fair"] as const;
|
|
28
|
+
|
|
29
|
+
export function categoryLabel(id: string): string {
|
|
30
|
+
return CATEGORIES.find((c) => c.id === id)?.label ?? id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isCategory(id: string): boolean {
|
|
34
|
+
return CATEGORIES.some((c) => c.id === id);
|
|
35
|
+
}
|
|
36
|
+
|
|
5
37
|
export function browseListings(
|
|
6
38
|
listings: Listing[],
|
|
7
39
|
options: { category?: string; query?: string; sort?: string },
|
|
@@ -21,6 +53,7 @@ export function browseListings(
|
|
|
21
53
|
listing.description,
|
|
22
54
|
listing.category,
|
|
23
55
|
listing.sellerName,
|
|
56
|
+
listing.location ?? "",
|
|
24
57
|
].some((value) => value.toLocaleLowerCase().includes(query));
|
|
25
58
|
});
|
|
26
59
|
|
|
@@ -30,3 +63,41 @@ export function browseListings(
|
|
|
30
63
|
return b.createdAt.localeCompare(a.createdAt);
|
|
31
64
|
});
|
|
32
65
|
}
|
|
66
|
+
|
|
67
|
+
/** Pending offers per listing id. */
|
|
68
|
+
export function pendingOfferCounts(offers: Offer[]): Map<string, number> {
|
|
69
|
+
const counts = new Map<string, number>();
|
|
70
|
+
for (const offer of offers) {
|
|
71
|
+
if (offer.status !== "pending") continue;
|
|
72
|
+
counts.set(offer.listingId, (counts.get(offer.listingId) ?? 0) + 1);
|
|
73
|
+
}
|
|
74
|
+
return counts;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** An offer as a whole-number percentage of the asking price. */
|
|
78
|
+
export function percentOfAsk(amount: number, price: number): number {
|
|
79
|
+
if (!(price > 0)) return 0;
|
|
80
|
+
return Math.round((amount / price) * 100);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Suggested offer amounts: 95%, 90%, and 85% of the asking price, rounded to
|
|
85
|
+
* a clean step, deduplicated, and always below the asking price.
|
|
86
|
+
*/
|
|
87
|
+
export function suggestedOffers(price: number): number[] {
|
|
88
|
+
if (!(price > 0)) return [];
|
|
89
|
+
const step = price >= 500 ? 10 : price >= 100 ? 5 : 1;
|
|
90
|
+
const amounts = [0.95, 0.9, 0.85].map((ratio) =>
|
|
91
|
+
Math.max(step, Math.round((price * ratio) / step) * step),
|
|
92
|
+
);
|
|
93
|
+
return Array.from(new Set(amounts)).filter((amount) => amount < price);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const BUNDLED_PHOTO = /^(\/images\/listings\/[a-z0-9-]+)\.webp$/;
|
|
97
|
+
|
|
98
|
+
/** Responsive `srcset` for a bundled listing photo; undefined for others. */
|
|
99
|
+
export function listingSrcSet(imageUrl: string | undefined): string | undefined {
|
|
100
|
+
const match = imageUrl?.match(BUNDLED_PHOTO);
|
|
101
|
+
if (!match) return undefined;
|
|
102
|
+
return `${match[1]}-640.webp 640w, ${imageUrl} 1120w`;
|
|
103
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|