@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,48 +1,31 @@
|
|
|
1
1
|
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { CAPTION_MAX, isImageUrl, requireProfile } from "../lib/social";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
* with
|
|
6
|
-
*
|
|
5
|
+
* Publish a photo. The client uploads the file first (`/api/files/init`
|
|
6
|
+
* with visibility "public", so every viewer can load it) and sends the
|
|
7
|
+
* file URL here with the caption.
|
|
7
8
|
*/
|
|
8
9
|
export default mutation({
|
|
9
|
-
args: {
|
|
10
|
-
async handler(ctx, args: {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
args: { imageUrl: v.string(), caption: v.optional(v.string()) },
|
|
11
|
+
async handler(ctx, args: { imageUrl: string; caption?: string }) {
|
|
12
|
+
const profile = await requireProfile(ctx, ctx.auth.userId);
|
|
13
|
+
const imageUrl = args.imageUrl.trim();
|
|
14
|
+
if (!isImageUrl(imageUrl)) {
|
|
15
|
+
throw ctx.error("INVALID_IMAGE", "Upload the photo again.");
|
|
13
16
|
}
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
throw ctx.error("TOO_LONG", "post body capped at 1000 chars");
|
|
17
|
+
const caption = (args.caption ?? "").trim();
|
|
18
|
+
if (caption.length > CAPTION_MAX) {
|
|
19
|
+
throw ctx.error("CAPTION_TOO_LONG", `Use at most ${CAPTION_MAX} characters.`);
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (profiles.length === 0) {
|
|
23
|
-
throw ctx.error(
|
|
24
|
-
"NO_PROFILE",
|
|
25
|
-
"create a profile first via upsertProfile",
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
const profile = profiles[0];
|
|
29
|
-
const id = await ctx.db.insert("Post", {
|
|
21
|
+
// The Post policy refuses client writes. This function is the checked
|
|
22
|
+
// write path, so it writes past the policy.
|
|
23
|
+
const id = await ctx.db.unsafe.insert("Post", {
|
|
30
24
|
authorId: profile.id,
|
|
31
|
-
|
|
25
|
+
imageUrl,
|
|
26
|
+
caption: caption || null,
|
|
32
27
|
createdAt: new Date().toISOString(),
|
|
33
28
|
});
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
id: post.id,
|
|
37
|
-
body: post.body,
|
|
38
|
-
createdAt: post.createdAt,
|
|
39
|
-
author: {
|
|
40
|
-
id: profile.id,
|
|
41
|
-
handle: profile.handle,
|
|
42
|
-
displayName: profile.displayName,
|
|
43
|
-
},
|
|
44
|
-
likeCount: 0,
|
|
45
|
-
likedByMe: false,
|
|
46
|
-
};
|
|
29
|
+
return { id };
|
|
47
30
|
},
|
|
48
31
|
});
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { requireProfile } from "../lib/social";
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Delete a Post and any associated Likes. Post policy already gates
|
|
5
|
-
* deletes to the author's Profile, so a non-author can't reach the
|
|
6
|
-
* actual delete call — but we double-check so a future policy
|
|
7
|
-
* loosening doesn't accidentally orphan Like rows.
|
|
8
|
-
*/
|
|
4
|
+
/** Delete one of the caller's posts with its likes and comments. */
|
|
9
5
|
export default mutation({
|
|
10
6
|
args: { id: v.id("Post") },
|
|
11
7
|
async handler(ctx, args: { id: string }) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const profile = await requireProfile(ctx, ctx.auth.userId);
|
|
9
|
+
const post = await ctx.db.get("Post", args.id);
|
|
10
|
+
if (!post) throw ctx.error("NOT_FOUND", "This post no longer exists.");
|
|
11
|
+
if (post.authorId !== profile.id) {
|
|
12
|
+
throw ctx.error("FORBIDDEN", "You can only delete your own posts.");
|
|
17
13
|
}
|
|
18
|
-
await ctx.db.
|
|
19
|
-
|
|
14
|
+
for (const like of await ctx.db.query("Like", { postId: args.id })) {
|
|
15
|
+
await ctx.db.unsafe.delete("Like", String(like.id));
|
|
16
|
+
}
|
|
17
|
+
for (const comment of await ctx.db.query("Comment", { postId: args.id })) {
|
|
18
|
+
await ctx.db.unsafe.delete("Comment", String(comment.id));
|
|
19
|
+
}
|
|
20
|
+
await ctx.db.unsafe.delete("Post", args.id);
|
|
21
|
+
return { id: args.id };
|
|
20
22
|
},
|
|
21
23
|
});
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { query } from "@pylonsync/functions";
|
|
2
|
+
import { findProfile } from "../lib/social";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The caller's Profile, or null when they have not made one yet. The
|
|
6
|
+
* clients call this after sign-in to decide between the profile setup
|
|
7
|
+
* screen and the feed.
|
|
7
8
|
*/
|
|
8
9
|
export default query({
|
|
9
10
|
args: {},
|
|
10
11
|
async handler(ctx) {
|
|
11
|
-
|
|
12
|
-
const rows = (await ctx.db.query("Profile", {
|
|
13
|
-
userId: ctx.auth.userId,
|
|
14
|
-
})) as any[];
|
|
15
|
-
return rows[0] ?? null;
|
|
12
|
+
return await findProfile(ctx, ctx.auth.userId);
|
|
16
13
|
},
|
|
17
14
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mutation } from "@pylonsync/functions";
|
|
2
|
+
import {
|
|
3
|
+
SEED_COMMENTS,
|
|
4
|
+
SEED_PEOPLE,
|
|
5
|
+
SEED_POSTS,
|
|
6
|
+
photoPath,
|
|
7
|
+
seedFollows,
|
|
8
|
+
seedLikers,
|
|
9
|
+
} from "../lib/seed";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Write the demo people, posts, likes, comments, and follows once. The
|
|
13
|
+
* clients call this on launch. It returns at once when the demo data
|
|
14
|
+
* exists, and a lock keeps two first launches from seeding twice.
|
|
15
|
+
*
|
|
16
|
+
* Public so the first launch can seed before anyone signs in. It writes
|
|
17
|
+
* only the fixed data in lib/seed.ts. When you launch, delete this function,
|
|
18
|
+
* lib/seed.ts, public/images, the reserved-handle check in upsertProfile.ts,
|
|
19
|
+
* and the client calls to `seedDemo`.
|
|
20
|
+
*/
|
|
21
|
+
export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
22
|
+
auth: "public",
|
|
23
|
+
args: {},
|
|
24
|
+
async handler(ctx) {
|
|
25
|
+
// The first demo account's email marks a seeded database. Check before
|
|
26
|
+
// the lock so launches after the first skip it, then again under it.
|
|
27
|
+
const marker = { email: `${SEED_PEOPLE[0].handle}@demo.invalid`, $limit: 1 };
|
|
28
|
+
if ((await ctx.db.unsafe.query("User", marker)).length > 0) return { seeded: false };
|
|
29
|
+
await ctx.db.advisoryLock("consumer_seed_demo");
|
|
30
|
+
if ((await ctx.db.unsafe.query("User", marker)).length > 0) return { seeded: false };
|
|
31
|
+
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const hoursAgo = (h: number) => new Date(now - h * 3_600_000).toISOString();
|
|
34
|
+
|
|
35
|
+
const profileIds = new Map<string, string>();
|
|
36
|
+
for (const person of SEED_PEOPLE) {
|
|
37
|
+
// A demo account has no password, so nobody can sign in as it.
|
|
38
|
+
const userId = await ctx.db.unsafe.insert("User", {
|
|
39
|
+
email: `${person.handle}@demo.invalid`,
|
|
40
|
+
displayName: person.displayName,
|
|
41
|
+
createdAt: hoursAgo(24 * 60),
|
|
42
|
+
});
|
|
43
|
+
const profileId = await ctx.db.unsafe.insert("Profile", {
|
|
44
|
+
userId,
|
|
45
|
+
handle: person.handle,
|
|
46
|
+
displayName: person.displayName,
|
|
47
|
+
bio: person.bio,
|
|
48
|
+
createdAt: hoursAgo(24 * 60),
|
|
49
|
+
});
|
|
50
|
+
profileIds.set(person.handle, profileId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const postIds = new Map<string, string>();
|
|
54
|
+
const postAges = new Map<string, number>();
|
|
55
|
+
for (const post of SEED_POSTS) {
|
|
56
|
+
const id = await ctx.db.unsafe.insert("Post", {
|
|
57
|
+
authorId: profileIds.get(post.author),
|
|
58
|
+
imageUrl: photoPath(post.photo),
|
|
59
|
+
caption: post.caption,
|
|
60
|
+
createdAt: hoursAgo(post.age),
|
|
61
|
+
});
|
|
62
|
+
postIds.set(post.photo, id);
|
|
63
|
+
postAges.set(post.photo, post.age);
|
|
64
|
+
for (const [i, liker] of seedLikers(post.photo).entries()) {
|
|
65
|
+
if (liker === post.author) continue;
|
|
66
|
+
await ctx.db.unsafe.insert("Like", {
|
|
67
|
+
postId: id,
|
|
68
|
+
profileId: profileIds.get(liker),
|
|
69
|
+
createdAt: hoursAgo(Math.max(post.age - 0.2 * (i + 1), 0.05)),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const comment of SEED_COMMENTS) {
|
|
75
|
+
const postId = postIds.get(comment.photo);
|
|
76
|
+
const age = postAges.get(comment.photo);
|
|
77
|
+
if (!postId || age === undefined) continue;
|
|
78
|
+
await ctx.db.unsafe.insert("Comment", {
|
|
79
|
+
postId,
|
|
80
|
+
profileId: profileIds.get(comment.author),
|
|
81
|
+
text: comment.text,
|
|
82
|
+
createdAt: hoursAgo(Math.max(age - comment.after, 0.05)),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const [follower, following] of seedFollows()) {
|
|
87
|
+
await ctx.db.unsafe.insert("Follow", {
|
|
88
|
+
followerId: profileIds.get(follower),
|
|
89
|
+
followingId: profileIds.get(following),
|
|
90
|
+
createdAt: hoursAgo(24 * 30),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return { seeded: true };
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { requireProfile } from "../lib/social";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Follow or unfollow a profile as the caller. Idempotent, like setLike.
|
|
6
|
+
*/
|
|
7
|
+
export default mutation({
|
|
8
|
+
args: { profileId: v.id("Profile"), following: v.boolean() },
|
|
9
|
+
async handler(ctx, args: { profileId: string; following: boolean }) {
|
|
10
|
+
const me = await requireProfile(ctx, ctx.auth.userId);
|
|
11
|
+
if (me.id === args.profileId) {
|
|
12
|
+
throw ctx.error("SELF_FOLLOW", "You cannot follow yourself.");
|
|
13
|
+
}
|
|
14
|
+
const target = await ctx.db.get("Profile", args.profileId);
|
|
15
|
+
if (!target) throw ctx.error("NOT_FOUND", "This profile no longer exists.");
|
|
16
|
+
|
|
17
|
+
await ctx.db.advisoryLock(`follow:${me.id}:${args.profileId}`);
|
|
18
|
+
const existing = await ctx.db.query("Follow", {
|
|
19
|
+
followerId: me.id,
|
|
20
|
+
followingId: args.profileId,
|
|
21
|
+
});
|
|
22
|
+
if (args.following && existing.length === 0) {
|
|
23
|
+
await ctx.db.unsafe.insert("Follow", {
|
|
24
|
+
followerId: me.id,
|
|
25
|
+
followingId: args.profileId,
|
|
26
|
+
createdAt: new Date().toISOString(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (!args.following) {
|
|
30
|
+
for (const row of existing) {
|
|
31
|
+
await ctx.db.unsafe.delete("Follow", String(row.id));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { following: args.following };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { requireProfile } from "../lib/social";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Like or unlike a post as the caller. Idempotent: `liked: true` twice
|
|
6
|
+
* leaves one like, so taps that reach the server out of order still end
|
|
7
|
+
* in the state of the last tap. Returns the post's like count.
|
|
8
|
+
*/
|
|
9
|
+
export default mutation({
|
|
10
|
+
args: { postId: v.id("Post"), liked: v.boolean() },
|
|
11
|
+
async handler(ctx, args: { postId: string; liked: boolean }) {
|
|
12
|
+
const profile = await requireProfile(ctx, ctx.auth.userId);
|
|
13
|
+
const post = await ctx.db.get("Post", args.postId);
|
|
14
|
+
if (!post) throw ctx.error("NOT_FOUND", "This post no longer exists.");
|
|
15
|
+
|
|
16
|
+
await ctx.db.advisoryLock(`like:${profile.id}:${args.postId}`);
|
|
17
|
+
const existing = await ctx.db.query("Like", {
|
|
18
|
+
postId: args.postId,
|
|
19
|
+
profileId: profile.id,
|
|
20
|
+
});
|
|
21
|
+
if (args.liked && existing.length === 0) {
|
|
22
|
+
await ctx.db.unsafe.insert("Like", {
|
|
23
|
+
postId: args.postId,
|
|
24
|
+
profileId: profile.id,
|
|
25
|
+
createdAt: new Date().toISOString(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!args.liked) {
|
|
29
|
+
for (const like of existing) {
|
|
30
|
+
await ctx.db.unsafe.delete("Like", String(like.id));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const likes = await ctx.db.query("Like", { postId: args.postId });
|
|
34
|
+
return { liked: args.liked, likeCount: likes.length };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -1,70 +1,76 @@
|
|
|
1
1
|
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { SEED_PEOPLE } from "../lib/seed";
|
|
3
|
+
import { BIO_MAX, HANDLE_PATTERN, findProfile, isImageUrl } from "../lib/social";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
* Create
|
|
5
|
-
*
|
|
6
|
-
* we don't re-check the handle if it didn't change.
|
|
6
|
+
* Create or update the caller's Profile. Handles are lowercase and unique.
|
|
7
|
+
* `avatarUrl` is optional; an empty string clears it.
|
|
7
8
|
*/
|
|
8
9
|
export default mutation({
|
|
9
10
|
args: {
|
|
10
11
|
handle: v.string(),
|
|
11
12
|
displayName: v.string(),
|
|
12
13
|
bio: v.string(),
|
|
14
|
+
avatarUrl: v.optional(v.string()),
|
|
13
15
|
},
|
|
14
16
|
async handler(
|
|
15
17
|
ctx,
|
|
16
|
-
args: { handle: string; displayName: string; bio: string },
|
|
18
|
+
args: { handle: string; displayName: string; bio: string; avatarUrl?: string },
|
|
17
19
|
) {
|
|
18
|
-
|
|
19
|
-
throw ctx.error("UNAUTHENTICATED", "log in first");
|
|
20
|
-
}
|
|
20
|
+
const userId = ctx.auth.userId;
|
|
21
21
|
const handle = args.handle.trim().toLowerCase();
|
|
22
|
-
if (
|
|
22
|
+
if (!HANDLE_PATTERN.test(handle)) {
|
|
23
23
|
throw ctx.error(
|
|
24
24
|
"INVALID_HANDLE",
|
|
25
|
-
"
|
|
25
|
+
"Use 2 to 24 characters: lowercase letters, digits, periods, and underscores.",
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
|
+
// The demo people in lib/seed.ts own these handles, even before the
|
|
29
|
+
// seed runs. Delete this check with lib/seed.ts.
|
|
30
|
+
if (SEED_PEOPLE.some((p) => p.handle === handle)) {
|
|
31
|
+
throw ctx.error("HANDLE_TAKEN", `@${handle} is taken.`);
|
|
32
|
+
}
|
|
28
33
|
const displayName = args.displayName.trim();
|
|
29
|
-
if (!displayName)
|
|
30
|
-
|
|
34
|
+
if (!displayName) throw ctx.error("EMPTY_NAME", "Enter your name.");
|
|
35
|
+
if (displayName.length > 40) {
|
|
36
|
+
throw ctx.error("NAME_TOO_LONG", "Use at most 40 characters for your name.");
|
|
37
|
+
}
|
|
38
|
+
const bio = args.bio.trim();
|
|
39
|
+
if (bio.length > BIO_MAX) {
|
|
40
|
+
throw ctx.error("BIO_TOO_LONG", `Use at most ${BIO_MAX} characters for your bio.`);
|
|
41
|
+
}
|
|
42
|
+
const avatarUrl = args.avatarUrl?.trim();
|
|
43
|
+
if (avatarUrl && !isImageUrl(avatarUrl)) {
|
|
44
|
+
throw ctx.error("INVALID_IMAGE", "Upload the photo again.");
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
// Two devices saving at once must not create two profiles.
|
|
48
|
+
await ctx.db.advisoryLock(`profile:${userId}`);
|
|
49
|
+
const existing = await findProfile(ctx, userId);
|
|
50
|
+
const [taken] = await ctx.db.query("Profile", { handle, $limit: 1 });
|
|
51
|
+
if (taken && taken.id !== existing?.id) {
|
|
52
|
+
throw ctx.error("HANDLE_TAKEN", `@${handle} is taken.`);
|
|
53
|
+
}
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
const id = await ctx.db.insert("Profile", {
|
|
44
|
-
userId: ctx.auth.userId,
|
|
55
|
+
// The Profile policy refuses client writes. This function is the
|
|
56
|
+
// checked write path, so it writes past the policy.
|
|
57
|
+
if (!existing) {
|
|
58
|
+
const id = await ctx.db.unsafe.insert("Profile", {
|
|
59
|
+
userId,
|
|
45
60
|
handle,
|
|
46
61
|
displayName,
|
|
47
|
-
bio:
|
|
62
|
+
bio: bio || null,
|
|
63
|
+
avatarUrl: avatarUrl || null,
|
|
48
64
|
createdAt: new Date().toISOString(),
|
|
49
65
|
});
|
|
50
66
|
return await ctx.db.get("Profile", id);
|
|
51
67
|
}
|
|
52
|
-
|
|
53
|
-
const profile = existing[0];
|
|
54
|
-
// Update — re-check handle uniqueness only if changing
|
|
55
|
-
if (profile.handle !== handle) {
|
|
56
|
-
const dup = ((await ctx.db.query("Profile", { handle })) as any[]).filter(
|
|
57
|
-
(p) => p.id !== profile.id,
|
|
58
|
-
);
|
|
59
|
-
if (dup.length > 0) {
|
|
60
|
-
throw ctx.error("HANDLE_TAKEN", `@${handle} is taken`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
await ctx.db.update("Profile", profile.id, {
|
|
68
|
+
await ctx.db.unsafe.update("Profile", existing.id, {
|
|
64
69
|
handle,
|
|
65
70
|
displayName,
|
|
66
|
-
bio:
|
|
71
|
+
bio: bio || null,
|
|
72
|
+
...(args.avatarUrl !== undefined ? { avatarUrl: avatarUrl || null } : {}),
|
|
67
73
|
});
|
|
68
|
-
return await ctx.db.get("Profile",
|
|
74
|
+
return await ctx.db.get("Profile", existing.id);
|
|
69
75
|
},
|
|
70
76
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Demo data for a new app: eight people who post, follow each other, like,
|
|
2
|
+
// and comment, so the first launch opens on a feed with content in it.
|
|
3
|
+
//
|
|
4
|
+
// Photos live in public/images/posts and the server serves them at
|
|
5
|
+
// /images/posts/... Demo people have no avatar photo, so the clients draw
|
|
6
|
+
// their initial. Demo accounts have no password, so nobody can sign in as
|
|
7
|
+
// them. functions/seedDemo.ts writes this data once.
|
|
8
|
+
|
|
9
|
+
export interface SeedPerson {
|
|
10
|
+
handle: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
bio: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SeedPost {
|
|
16
|
+
/** File name under public/images/posts, without `.jpg`. */
|
|
17
|
+
photo: string;
|
|
18
|
+
author: string;
|
|
19
|
+
caption: string;
|
|
20
|
+
/** Hours before the seed runs. */
|
|
21
|
+
age: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SeedComment {
|
|
25
|
+
photo: string;
|
|
26
|
+
author: string;
|
|
27
|
+
text: string;
|
|
28
|
+
/** Hours after the post. */
|
|
29
|
+
after: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SEED_PEOPLE: SeedPerson[] = [
|
|
33
|
+
{ handle: "maya.rivera", displayName: "Maya Rivera", bio: "Travel photographer. Currently in Lisbon." },
|
|
34
|
+
{ handle: "jordan.eats", displayName: "Jordan Brooks", bio: "Cooking at home, eating out, writing it all down." },
|
|
35
|
+
{ handle: "priya.home", displayName: "Priya Shah", bio: "Interior designer. Small rooms, good light." },
|
|
36
|
+
{ handle: "kenji.mori", displayName: "Kenji Mori", bio: "Architecture and night walks. Tokyo." },
|
|
37
|
+
{ handle: "elena.runs", displayName: "Elena Berg", bio: "Trail runner. Mountains most weekends." },
|
|
38
|
+
{ handle: "sam.and.juno", displayName: "Sam Whitaker", bio: "Juno is the golden one." },
|
|
39
|
+
{ handle: "amara.o", displayName: "Amara Okafor", bio: "Coffee, markets, and early mornings." },
|
|
40
|
+
{ handle: "leo.haddad", displayName: "Leo Haddad", bio: "Road trips and sunsets. Film when I can." },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const SEED_POSTS: SeedPost[] = [
|
|
44
|
+
{ photo: "lisbon", author: "maya.rivera", caption: "Tram 28 at golden hour. Waited twenty minutes for this one and it was worth it.", age: 1.5 },
|
|
45
|
+
{ photo: "coffee", author: "amara.o", caption: "Flat white and a croissant before the market opens.", age: 3 },
|
|
46
|
+
{ photo: "dog", author: "sam.and.juno", caption: "Juno found the tall grass again.", age: 5 },
|
|
47
|
+
{ photo: "run", author: "elena.runs", caption: "Sunrise on the ridge. 18 km, 900 m up, legs gone.", age: 8 },
|
|
48
|
+
{ photo: "ramen", author: "jordan.eats", caption: "Tonkotsu from the new place on 5th. Broth cooked for 14 hours and you can tell.", age: 11 },
|
|
49
|
+
{ photo: "arch", author: "kenji.mori", caption: "Curves and shadows. The museum extension at noon.", age: 14 },
|
|
50
|
+
{ photo: "sunset", author: "leo.haddad", caption: "Big Sur, last light. 30 second exposure.", age: 20 },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const SEED_COMMENTS: SeedComment[] = [
|
|
54
|
+
{ photo: "lisbon", author: "leo.haddad", text: "The light on those tiles. Which street is this?", after: 0.3 },
|
|
55
|
+
{ photo: "lisbon", author: "maya.rivera", text: "Rua da Conceição, just before it turns up to the cathedral.", after: 0.6 },
|
|
56
|
+
{ photo: "lisbon", author: "amara.o", text: "Adding this to the list for May.", after: 0.9 },
|
|
57
|
+
{ photo: "coffee", author: "jordan.eats", text: "That rosetta is perfect.", after: 0.5 },
|
|
58
|
+
{ photo: "coffee", author: "priya.home", text: "Where is this? I need that table.", after: 1 },
|
|
59
|
+
{ photo: "dog", author: "elena.runs", text: "Juno looks so happy.", after: 0.4 },
|
|
60
|
+
{ photo: "dog", author: "amara.o", text: "Best dog on this app.", after: 1.2 },
|
|
61
|
+
{ photo: "run", author: "kenji.mori", text: "Incredible view. How early did you start?", after: 1 },
|
|
62
|
+
{ photo: "run", author: "elena.runs", text: "Headlamps on at 4:30.", after: 1.5 },
|
|
63
|
+
{ photo: "ramen", author: "maya.rivera", text: "Going this week.", after: 2 },
|
|
64
|
+
{ photo: "arch", author: "priya.home", text: "Those shadows are unreal.", after: 1 },
|
|
65
|
+
{ photo: "sunset", author: "sam.and.juno", text: "Colors like this make me want to drive up the coast tonight.", after: 2 },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** Which demo people like which photo. Deterministic, so every seed looks the same. */
|
|
69
|
+
export function seedLikers(photo: string): string[] {
|
|
70
|
+
const people = SEED_PEOPLE.map((p) => p.handle);
|
|
71
|
+
let hash = 0;
|
|
72
|
+
for (const ch of photo) hash = (hash * 31 + ch.charCodeAt(0)) >>> 0;
|
|
73
|
+
return people.filter((_, i) => ((hash >> i) & 3) !== 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Everyone follows everyone else, except a few pairs. */
|
|
77
|
+
export function seedFollows(): Array<[string, string]> {
|
|
78
|
+
const out: Array<[string, string]> = [];
|
|
79
|
+
for (const a of SEED_PEOPLE) {
|
|
80
|
+
for (const b of SEED_PEOPLE) {
|
|
81
|
+
if (a.handle === b.handle) continue;
|
|
82
|
+
if ((a.handle.length + b.handle.length) % 5 === 0) continue;
|
|
83
|
+
out.push([a.handle, b.handle]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const photoPath = (photo: string) => `/images/posts/${photo}.jpg`;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MutationCtx, QueryCtx } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// Shared checks for the functions in functions/.
|
|
4
|
+
|
|
5
|
+
export const CAPTION_MAX = 2200;
|
|
6
|
+
export const COMMENT_MAX = 500;
|
|
7
|
+
export const BIO_MAX = 150;
|
|
8
|
+
export const HANDLE_PATTERN = /^[a-z0-9._]{2,24}$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A post or avatar image must be a demo photo served from public/images or
|
|
12
|
+
* a file uploaded to this server (`/api/files/<id>`). Anything else is
|
|
13
|
+
* refused, so a client cannot point the feed at an arbitrary host.
|
|
14
|
+
*/
|
|
15
|
+
export function isImageUrl(url: string): boolean {
|
|
16
|
+
if (url.includes("..") || url.includes("//")) return false;
|
|
17
|
+
return /^\/images\/[a-z0-9/_.-]+\.(jpg|jpeg|png|webp)$/i.test(url) ||
|
|
18
|
+
/^\/api\/files\/[A-Za-z0-9_.-]+$/.test(url);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The caller's Profile row, or null when they have not made one yet. */
|
|
22
|
+
export async function findProfile(
|
|
23
|
+
ctx: Pick<QueryCtx, "db"> | Pick<MutationCtx, "db">,
|
|
24
|
+
userId: string,
|
|
25
|
+
): Promise<ProfileRow | null> {
|
|
26
|
+
const rows = await ctx.db.query("Profile", { userId, $limit: 1 });
|
|
27
|
+
return (rows[0] as unknown as ProfileRow | undefined) ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The caller's Profile row. Throws NO_PROFILE when there is none. */
|
|
31
|
+
export async function requireProfile(
|
|
32
|
+
ctx: Pick<MutationCtx, "db" | "error">,
|
|
33
|
+
userId: string,
|
|
34
|
+
): Promise<ProfileRow> {
|
|
35
|
+
const profile = await findProfile(ctx, userId);
|
|
36
|
+
if (!profile) {
|
|
37
|
+
throw ctx.error("NO_PROFILE", "Create your profile first.");
|
|
38
|
+
}
|
|
39
|
+
return profile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProfileRow {
|
|
43
|
+
id: string;
|
|
44
|
+
userId: string;
|
|
45
|
+
handle: string;
|
|
46
|
+
displayName: string;
|
|
47
|
+
bio?: string | null;
|
|
48
|
+
avatarUrl?: string | null;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
}
|
|
@@ -6,15 +6,21 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "pylon dev",
|
|
8
8
|
"build": "pylon build",
|
|
9
|
+
"check": "tsc --noEmit",
|
|
9
10
|
"schema:push": "pylon schema push --sqlite .pylon/dev.db",
|
|
10
11
|
"schema:inspect": "pylon schema inspect --sqlite .pylon/dev.db"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
|
-
"@pylonsync/functions": "^__PYLON_VERSION__"
|
|
15
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
16
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
17
|
+
"react": "^19.0.0",
|
|
18
|
+
"react-dom": "^19.0.0"
|
|
15
19
|
},
|
|
16
20
|
"devDependencies": {
|
|
17
21
|
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
18
|
-
"typescript": "^5.5.0"
|
|
22
|
+
"typescript": "^5.5.0",
|
|
23
|
+
"@types/react": "^19.0.0",
|
|
24
|
+
"@types/react-dom": "^19.0.0"
|
|
19
25
|
}
|
|
20
26
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -7,7 +7,17 @@
|
|
|
7
7
|
"skipLibCheck": true,
|
|
8
8
|
"noEmit": true,
|
|
9
9
|
"esModuleInterop": true,
|
|
10
|
-
"allowSyntheticDefaultImports": true
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"jsx": "react",
|
|
12
|
+
"lib": [
|
|
13
|
+
"ES2022",
|
|
14
|
+
"DOM"
|
|
15
|
+
]
|
|
11
16
|
},
|
|
12
|
-
"include": [
|
|
17
|
+
"include": [
|
|
18
|
+
"app.ts",
|
|
19
|
+
"app/**/*",
|
|
20
|
+
"functions/**/*",
|
|
21
|
+
"lib/**/*"
|
|
22
|
+
]
|
|
13
23
|
}
|