@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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import Combine
|
|
2
|
+
import Foundation
|
|
3
|
+
import PylonClient
|
|
4
|
+
import PylonSync
|
|
5
|
+
import PylonSwiftUI
|
|
6
|
+
|
|
7
|
+
/// The live social graph. Five subscriptions (Profile, Post, Like, Comment,
|
|
8
|
+
/// Follow) on the sync engine, joined in memory. A row written by any
|
|
9
|
+
/// client reaches every open app over the engine's WebSocket, and every
|
|
10
|
+
/// view that reads this store redraws.
|
|
11
|
+
///
|
|
12
|
+
/// Writes go through server functions. The server writes the row, the
|
|
13
|
+
/// change comes back through sync, and the view updates. Likes and follows
|
|
14
|
+
/// also show at once through a pending state that clears when the synced
|
|
15
|
+
/// row arrives.
|
|
16
|
+
@MainActor
|
|
17
|
+
final class SocialStore: ObservableObject {
|
|
18
|
+
let engine: SyncEngine
|
|
19
|
+
let client: PylonClient
|
|
20
|
+
let media: MediaService
|
|
21
|
+
|
|
22
|
+
@Published var myProfileId: String?
|
|
23
|
+
@Published private(set) var profiles: [String: Profile] = [:]
|
|
24
|
+
/// Newest first.
|
|
25
|
+
@Published private(set) var posts: [Post] = []
|
|
26
|
+
@Published private(set) var likesByPost: [String: [Like]] = [:]
|
|
27
|
+
/// Oldest first within a post.
|
|
28
|
+
@Published private(set) var commentsByPost: [String: [Comment]] = [:]
|
|
29
|
+
@Published private(set) var follows: [Follow] = []
|
|
30
|
+
@Published private(set) var isLoading = true
|
|
31
|
+
/// Post ids the user liked or unliked whose synced row has not arrived yet.
|
|
32
|
+
@Published private(set) var pendingLikes: [String: Bool] = [:]
|
|
33
|
+
@Published private(set) var pendingFollows: [String: Bool] = [:]
|
|
34
|
+
|
|
35
|
+
private let profileQuery: PylonQuery<Profile>
|
|
36
|
+
private let postQuery: PylonQuery<Post>
|
|
37
|
+
private let likeQuery: PylonQuery<Like>
|
|
38
|
+
private let commentQuery: PylonQuery<Comment>
|
|
39
|
+
private let followQuery: PylonQuery<Follow>
|
|
40
|
+
private var subscriptions: Set<AnyCancellable> = []
|
|
41
|
+
|
|
42
|
+
init(engine: SyncEngine, client: PylonClient, media: MediaService) {
|
|
43
|
+
self.engine = engine
|
|
44
|
+
self.client = client
|
|
45
|
+
self.media = media
|
|
46
|
+
profileQuery = PylonQuery(engine: engine, entity: "Profile")
|
|
47
|
+
postQuery = PylonQuery(engine: engine, entity: "Post")
|
|
48
|
+
likeQuery = PylonQuery(engine: engine, entity: "Like")
|
|
49
|
+
commentQuery = PylonQuery(engine: engine, entity: "Comment")
|
|
50
|
+
followQuery = PylonQuery(engine: engine, entity: "Follow")
|
|
51
|
+
|
|
52
|
+
profileQuery.$rows
|
|
53
|
+
.sink { [weak self] rows in
|
|
54
|
+
self?.profiles = Dictionary(rows.map { ($0.id, $0) }, uniquingKeysWith: { _, new in new })
|
|
55
|
+
}
|
|
56
|
+
.store(in: &subscriptions)
|
|
57
|
+
postQuery.$rows
|
|
58
|
+
.sink { [weak self] rows in
|
|
59
|
+
self?.posts = rows.sorted { $0.createdAt > $1.createdAt }
|
|
60
|
+
}
|
|
61
|
+
.store(in: &subscriptions)
|
|
62
|
+
likeQuery.$rows
|
|
63
|
+
.sink { [weak self] rows in
|
|
64
|
+
guard let self else { return }
|
|
65
|
+
likesByPost = Dictionary(grouping: rows, by: \.postId)
|
|
66
|
+
pendingLikes = pendingLikes.filter { postId, liked in
|
|
67
|
+
self.syncedLike(postId) != liked
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
.store(in: &subscriptions)
|
|
71
|
+
commentQuery.$rows
|
|
72
|
+
.sink { [weak self] rows in
|
|
73
|
+
self?.commentsByPost = Dictionary(grouping: rows, by: \.postId)
|
|
74
|
+
.mapValues { $0.sorted { $0.createdAt < $1.createdAt } }
|
|
75
|
+
}
|
|
76
|
+
.store(in: &subscriptions)
|
|
77
|
+
followQuery.$rows
|
|
78
|
+
.sink { [weak self] rows in
|
|
79
|
+
guard let self else { return }
|
|
80
|
+
follows = rows
|
|
81
|
+
pendingFollows = pendingFollows.filter { profileId, following in
|
|
82
|
+
self.syncedFollow(profileId) != following
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
.store(in: &subscriptions)
|
|
86
|
+
postQuery.$loading
|
|
87
|
+
.sink { [weak self] loading in self?.isLoading = loading }
|
|
88
|
+
.store(in: &subscriptions)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// MARK: - Reads
|
|
92
|
+
|
|
93
|
+
var me: Profile? { myProfileId.flatMap { profiles[$0] } }
|
|
94
|
+
|
|
95
|
+
func profile(_ id: String) -> Profile? { profiles[id] }
|
|
96
|
+
|
|
97
|
+
func posts(by profileId: String) -> [Post] {
|
|
98
|
+
posts.filter { $0.authorId == profileId }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func post(_ id: String) -> Post? { posts.first { $0.id == id } }
|
|
102
|
+
|
|
103
|
+
func isLiked(_ postId: String) -> Bool {
|
|
104
|
+
pendingLikes[postId] ?? syncedLike(postId)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func likeCount(_ postId: String) -> Int {
|
|
108
|
+
let synced = likesByPost[postId]?.count ?? 0
|
|
109
|
+
guard let pending = pendingLikes[postId], pending != syncedLike(postId) else { return synced }
|
|
110
|
+
return synced + (pending ? 1 : -1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// A liker to name under the photo: someone the user follows first, else the latest.
|
|
114
|
+
func featuredLiker(_ postId: String) -> Profile? {
|
|
115
|
+
let likes = (likesByPost[postId] ?? []).filter { $0.profileId != myProfileId }
|
|
116
|
+
let following = Set(followingIds(of: myProfileId))
|
|
117
|
+
let pick = likes.first { following.contains($0.profileId) } ?? likes.max { $0.createdAt < $1.createdAt }
|
|
118
|
+
return pick.flatMap { profiles[$0.profileId] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func comments(_ postId: String) -> [Comment] { commentsByPost[postId] ?? [] }
|
|
122
|
+
|
|
123
|
+
func followerIds(of profileId: String) -> [String] {
|
|
124
|
+
follows.filter { $0.followingId == profileId }.map(\.followerId)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func followingIds(of profileId: String?) -> [String] {
|
|
128
|
+
guard let profileId else { return [] }
|
|
129
|
+
return follows.filter { $0.followerId == profileId }.map(\.followingId)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func isFollowing(_ profileId: String) -> Bool {
|
|
133
|
+
pendingFollows[profileId] ?? syncedFollow(profileId)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func followerCount(_ profileId: String) -> Int {
|
|
137
|
+
let synced = followerIds(of: profileId).count
|
|
138
|
+
guard let pending = pendingFollows[profileId], pending != syncedFollow(profileId) else { return synced }
|
|
139
|
+
return synced + (pending ? 1 : -1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// People with a post in the last 24 hours, the user first, then newest.
|
|
143
|
+
var storyAuthors: [Profile] {
|
|
144
|
+
let cutoff = Date.now.addingTimeInterval(-86_400)
|
|
145
|
+
var seen = Set<String>()
|
|
146
|
+
var out: [Profile] = []
|
|
147
|
+
for post in posts where post.date > cutoff {
|
|
148
|
+
guard !seen.contains(post.authorId), post.authorId != myProfileId,
|
|
149
|
+
let author = profiles[post.authorId] else { continue }
|
|
150
|
+
seen.insert(post.authorId)
|
|
151
|
+
out.append(author)
|
|
152
|
+
}
|
|
153
|
+
return out
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func hasRecentPost(_ profileId: String) -> Bool {
|
|
157
|
+
let cutoff = Date.now.addingTimeInterval(-86_400)
|
|
158
|
+
return posts.contains { $0.authorId == profileId && $0.date > cutoff }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func recentPosts(by profileId: String) -> [Post] {
|
|
162
|
+
let cutoff = Date.now.addingTimeInterval(-86_400)
|
|
163
|
+
return posts.filter { $0.authorId == profileId && $0.date > cutoff }.reversed()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Likes, comments, and follows aimed at the user, newest first.
|
|
167
|
+
var activity: [ActivityItem] {
|
|
168
|
+
guard let me = myProfileId else { return [] }
|
|
169
|
+
let mine = Set(posts.filter { $0.authorId == me }.map(\.id))
|
|
170
|
+
var items: [ActivityItem] = []
|
|
171
|
+
for postId in mine {
|
|
172
|
+
for like in likesByPost[postId] ?? [] where like.profileId != me {
|
|
173
|
+
items.append(ActivityItem(id: "like-\(like.id)", kind: .like, actorId: like.profileId, postId: postId, text: nil, createdAt: like.createdAt))
|
|
174
|
+
}
|
|
175
|
+
for comment in commentsByPost[postId] ?? [] where comment.profileId != me {
|
|
176
|
+
items.append(ActivityItem(id: "comment-\(comment.id)", kind: .comment, actorId: comment.profileId, postId: postId, text: comment.text, createdAt: comment.createdAt))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for follow in follows where follow.followingId == me {
|
|
180
|
+
items.append(ActivityItem(id: "follow-\(follow.id)", kind: .follow, actorId: follow.followerId, postId: nil, text: nil, createdAt: follow.createdAt))
|
|
181
|
+
}
|
|
182
|
+
return items.sorted { $0.createdAt > $1.createdAt }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func syncedLike(_ postId: String) -> Bool {
|
|
186
|
+
guard let me = myProfileId else { return false }
|
|
187
|
+
return likesByPost[postId]?.contains { $0.profileId == me } ?? false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private func syncedFollow(_ profileId: String) -> Bool {
|
|
191
|
+
guard let me = myProfileId else { return false }
|
|
192
|
+
return follows.contains { $0.followerId == me && $0.followingId == profileId }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MARK: - Writes
|
|
196
|
+
|
|
197
|
+
func toggleLike(_ postId: String) {
|
|
198
|
+
let next = !isLiked(postId)
|
|
199
|
+
setLike(postId, liked: next)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Double tap only ever likes.
|
|
203
|
+
func like(_ postId: String) {
|
|
204
|
+
if !isLiked(postId) { setLike(postId, liked: true) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Records the tap at once, then sends the wanted state. The server call
|
|
208
|
+
/// sets the state rather than flipping it, so taps that arrive out of
|
|
209
|
+
/// order still end in the last tap's state.
|
|
210
|
+
private func setLike(_ postId: String, liked: Bool) {
|
|
211
|
+
pendingLikes[postId] = liked
|
|
212
|
+
Task {
|
|
213
|
+
do {
|
|
214
|
+
_ = try await client.callFn("setLike", args: SetLikeArgs(postId: postId, liked: liked), as: SetLikeResult.self)
|
|
215
|
+
} catch {
|
|
216
|
+
if pendingLikes[postId] == liked { pendingLikes[postId] = nil }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func toggleFollow(_ profileId: String) {
|
|
222
|
+
let following = !isFollowing(profileId)
|
|
223
|
+
pendingFollows[profileId] = following
|
|
224
|
+
Task {
|
|
225
|
+
do {
|
|
226
|
+
_ = try await client.callFn("setFollow", args: SetFollowArgs(profileId: profileId, following: following), as: SetFollowResult.self)
|
|
227
|
+
} catch {
|
|
228
|
+
if pendingFollows[profileId] == following { pendingFollows[profileId] = nil }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func addComment(_ postId: String, text: String) async throws {
|
|
234
|
+
_ = try await client.callFn("addComment", args: AddCommentArgs(postId: postId, text: text), as: IdResult.self)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func deletePost(_ postId: String) async throws {
|
|
238
|
+
_ = try await client.callFn("deletePost", args: DeletePostArgs(id: postId), as: IdResult.self)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func createPost(jpeg: Data, caption: String) async throws {
|
|
242
|
+
let imageUrl = try await media.uploadPublicJPEG(jpeg)
|
|
243
|
+
_ = try await client.callFn("createPost", args: CreatePostArgs(imageUrl: imageUrl, caption: caption), as: IdResult.self)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func saveProfile(handle: String, displayName: String, bio: String, avatarJPEG: Data?) async throws -> Profile {
|
|
247
|
+
var avatarUrl: String?
|
|
248
|
+
if let avatarJPEG {
|
|
249
|
+
avatarUrl = try await media.uploadPublicJPEG(avatarJPEG)
|
|
250
|
+
}
|
|
251
|
+
return try await client.callFn(
|
|
252
|
+
"upsertProfile",
|
|
253
|
+
args: UpsertProfileArgs(handle: handle, displayName: displayName, bio: bio, avatarUrl: avatarUrl),
|
|
254
|
+
as: Profile.self
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
struct ActivityItem: Identifiable, Hashable {
|
|
260
|
+
enum Kind { case like, comment, follow }
|
|
261
|
+
let id: String
|
|
262
|
+
let kind: Kind
|
|
263
|
+
let actorId: String
|
|
264
|
+
let postId: String?
|
|
265
|
+
let text: String?
|
|
266
|
+
let createdAt: String
|
|
267
|
+
|
|
268
|
+
var date: Date { Timestamp.parse(createdAt) }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// A readable sentence for an error from a server function or the network.
|
|
272
|
+
func friendlyMessage(_ error: Error) -> String {
|
|
273
|
+
if let pylon = error as? PylonError {
|
|
274
|
+
switch pylon {
|
|
275
|
+
case .http(_, _, let message?) where !message.isEmpty:
|
|
276
|
+
return message.prefix(1).uppercased() + message.dropFirst()
|
|
277
|
+
case .transport:
|
|
278
|
+
return "Cannot reach the server. Check your connection and try again."
|
|
279
|
+
default:
|
|
280
|
+
return "Something went wrong. Try again."
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if let local = error as? LocalizedError, let text = local.errorDescription { return text }
|
|
284
|
+
return "Something went wrong. Try again."
|
|
285
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Full-screen playback of each person's posts from the last 24 hours.
|
|
4
|
+
/// Every photo shows for five seconds. Tap the right side for the next
|
|
5
|
+
/// photo, the left side for the previous one, or swipe down to close.
|
|
6
|
+
struct StoryViewer: View {
|
|
7
|
+
@EnvironmentObject private var social: SocialStore
|
|
8
|
+
@Environment(\.dismiss) private var dismiss
|
|
9
|
+
|
|
10
|
+
let authors: [Profile]
|
|
11
|
+
@State var authorIndex: Int
|
|
12
|
+
@State private var postIndex = 0
|
|
13
|
+
@State private var progress: Double = 0
|
|
14
|
+
@State private var dragOffset: CGFloat = 0
|
|
15
|
+
/// Bumped to replay the current photo from the start.
|
|
16
|
+
@State private var restarts = 0
|
|
17
|
+
|
|
18
|
+
private let duration: Double = 5
|
|
19
|
+
private let tick: Double = 0.05
|
|
20
|
+
|
|
21
|
+
init(authors: [Profile], startIndex: Int) {
|
|
22
|
+
self.authors = authors
|
|
23
|
+
_authorIndex = State(initialValue: startIndex)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private var author: Profile? { authors.indices.contains(authorIndex) ? authors[authorIndex] : nil }
|
|
27
|
+
private var posts: [Post] { author.map { social.recentPosts(by: $0.id) } ?? [] }
|
|
28
|
+
private var post: Post? { posts.indices.contains(postIndex) ? posts[postIndex] : nil }
|
|
29
|
+
|
|
30
|
+
var body: some View {
|
|
31
|
+
ZStack {
|
|
32
|
+
Color.black.ignoresSafeArea()
|
|
33
|
+
if let post {
|
|
34
|
+
let url = social.media.url(for: post.imageUrl)
|
|
35
|
+
RemoteImage(url: url)
|
|
36
|
+
.blur(radius: 40)
|
|
37
|
+
.opacity(0.6)
|
|
38
|
+
.ignoresSafeArea()
|
|
39
|
+
Color.clear
|
|
40
|
+
.aspectRatio(4 / 5, contentMode: .fit)
|
|
41
|
+
.overlay { RemoteImage(url: url) }
|
|
42
|
+
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
43
|
+
.id(post.id)
|
|
44
|
+
}
|
|
45
|
+
HStack(spacing: 0) {
|
|
46
|
+
Color.clear.contentShape(Rectangle()).onTapGesture(perform: previous)
|
|
47
|
+
Color.clear.contentShape(Rectangle()).onTapGesture(perform: next)
|
|
48
|
+
}
|
|
49
|
+
VStack(spacing: 10) {
|
|
50
|
+
progressBars
|
|
51
|
+
topBar
|
|
52
|
+
Spacer()
|
|
53
|
+
if let caption = post?.caption, !caption.isEmpty {
|
|
54
|
+
Text(caption)
|
|
55
|
+
.font(.subheadline)
|
|
56
|
+
.foregroundStyle(.white)
|
|
57
|
+
.multilineTextAlignment(.leading)
|
|
58
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
59
|
+
.padding(14)
|
|
60
|
+
.background(.black.opacity(0.35), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
.padding(.horizontal, 12)
|
|
64
|
+
.padding(.top, 8)
|
|
65
|
+
}
|
|
66
|
+
.offset(y: dragOffset)
|
|
67
|
+
.gesture(
|
|
68
|
+
DragGesture()
|
|
69
|
+
.onChanged { value in dragOffset = max(0, value.translation.height) }
|
|
70
|
+
.onEnded { value in
|
|
71
|
+
if value.translation.height > 120 { dismiss() } else {
|
|
72
|
+
withAnimation(.spring) { dragOffset = 0 }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
.statusBarHidden()
|
|
77
|
+
.task(id: "\(authorIndex)-\(postIndex)-\(restarts)") { await play() }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private var progressBars: some View {
|
|
81
|
+
HStack(spacing: 4) {
|
|
82
|
+
ForEach(posts.indices, id: \.self) { index in
|
|
83
|
+
GeometryReader { geo in
|
|
84
|
+
Capsule().fill(.white.opacity(0.35))
|
|
85
|
+
.overlay(alignment: .leading) {
|
|
86
|
+
Capsule().fill(.white)
|
|
87
|
+
.frame(width: geo.size.width * fill(for: index))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
.frame(height: 2.5)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private var topBar: some View {
|
|
96
|
+
HStack(spacing: 10) {
|
|
97
|
+
AvatarView(profile: author, size: 32)
|
|
98
|
+
Text(author?.handle ?? "")
|
|
99
|
+
.font(.subheadline.weight(.semibold))
|
|
100
|
+
if let post {
|
|
101
|
+
Text(Timestamp.short(post.date))
|
|
102
|
+
.font(.subheadline)
|
|
103
|
+
.opacity(0.7)
|
|
104
|
+
}
|
|
105
|
+
Spacer()
|
|
106
|
+
Button { dismiss() } label: {
|
|
107
|
+
Image(systemName: "xmark")
|
|
108
|
+
.font(.system(size: 20, weight: .semibold))
|
|
109
|
+
.frame(width: 40, height: 40)
|
|
110
|
+
.contentShape(Rectangle())
|
|
111
|
+
}
|
|
112
|
+
.accessibilityLabel("Close")
|
|
113
|
+
}
|
|
114
|
+
.foregroundStyle(.white)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func fill(for index: Int) -> Double {
|
|
118
|
+
if index < postIndex { return 1 }
|
|
119
|
+
if index == postIndex { return progress }
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func play() async {
|
|
124
|
+
progress = 0
|
|
125
|
+
let steps = Int(duration / tick)
|
|
126
|
+
for _ in 0..<steps {
|
|
127
|
+
try? await Task.sleep(for: .seconds(tick))
|
|
128
|
+
if Task.isCancelled { return }
|
|
129
|
+
progress += tick / duration
|
|
130
|
+
}
|
|
131
|
+
next()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func next() {
|
|
135
|
+
if postIndex + 1 < posts.count {
|
|
136
|
+
postIndex += 1
|
|
137
|
+
} else if authorIndex + 1 < authors.count {
|
|
138
|
+
authorIndex += 1
|
|
139
|
+
postIndex = 0
|
|
140
|
+
} else {
|
|
141
|
+
dismiss()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func previous() {
|
|
146
|
+
if postIndex > 0 {
|
|
147
|
+
postIndex -= 1
|
|
148
|
+
} else if authorIndex > 0 {
|
|
149
|
+
authorIndex -= 1
|
|
150
|
+
postIndex = 0
|
|
151
|
+
} else {
|
|
152
|
+
restarts += 1
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
enum Theme {
|
|
4
|
+
/// Filled buttons, links, and badges. Fixed so the black tab bar tint
|
|
5
|
+
/// does not change it.
|
|
6
|
+
static let accent = Color(red: 0.0, green: 0.584, blue: 0.965)
|
|
7
|
+
/// The like color.
|
|
8
|
+
static let heart = Color(red: 1.0, green: 0.19, blue: 0.25)
|
|
9
|
+
/// The ring around an avatar with a post from the last 24 hours.
|
|
10
|
+
static let storyRing = AngularGradient(
|
|
11
|
+
colors: [
|
|
12
|
+
Color(red: 0.99, green: 0.73, blue: 0.27),
|
|
13
|
+
Color(red: 0.98, green: 0.38, blue: 0.20),
|
|
14
|
+
Color(red: 0.87, green: 0.15, blue: 0.47),
|
|
15
|
+
Color(red: 0.55, green: 0.22, blue: 0.85),
|
|
16
|
+
Color(red: 0.99, green: 0.73, blue: 0.27),
|
|
17
|
+
],
|
|
18
|
+
center: .center
|
|
19
|
+
)
|
|
20
|
+
static let hairline = Color(uiColor: .separator)
|
|
21
|
+
static let fieldFill = Color(uiColor: .secondarySystemBackground)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The app name in the feed header and on the sign-in screen.
|
|
25
|
+
struct Wordmark: View {
|
|
26
|
+
var size: CGFloat = 30
|
|
27
|
+
|
|
28
|
+
var body: some View {
|
|
29
|
+
Text("__APP_NAME__")
|
|
30
|
+
.font(.system(size: size, weight: .semibold, design: .serif))
|
|
31
|
+
.italic()
|
|
32
|
+
.tracking(-0.5)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// A full-width filled button for the main action on a screen.
|
|
37
|
+
struct PrimaryButtonStyle: ButtonStyle {
|
|
38
|
+
var isLoading = false
|
|
39
|
+
|
|
40
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
41
|
+
ZStack {
|
|
42
|
+
configuration.label.opacity(isLoading ? 0 : 1)
|
|
43
|
+
if isLoading { ProgressView().tint(.white) }
|
|
44
|
+
}
|
|
45
|
+
.font(.body.weight(.semibold))
|
|
46
|
+
.foregroundStyle(.white)
|
|
47
|
+
.frame(maxWidth: .infinity, minHeight: 50)
|
|
48
|
+
.background(Theme.accent, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
49
|
+
.opacity(configuration.isPressed ? 0.8 : 1)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// A compact button for profile actions: Edit profile, Follow, Following.
|
|
54
|
+
struct ProfileButtonStyle: ButtonStyle {
|
|
55
|
+
var prominent = false
|
|
56
|
+
|
|
57
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
58
|
+
configuration.label
|
|
59
|
+
.font(.subheadline.weight(.semibold))
|
|
60
|
+
.foregroundStyle(prominent ? Color.white : Color.primary)
|
|
61
|
+
.frame(maxWidth: .infinity, minHeight: 34)
|
|
62
|
+
.background(
|
|
63
|
+
prominent ? Theme.accent : Theme.fieldFill,
|
|
64
|
+
in: RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
65
|
+
)
|
|
66
|
+
.opacity(configuration.isPressed ? 0.7 : 1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// A rounded text field on the sign-in and profile screens.
|
|
71
|
+
struct FieldStyle: ViewModifier {
|
|
72
|
+
func body(content: Content) -> some View {
|
|
73
|
+
content
|
|
74
|
+
.padding(.horizontal, 14)
|
|
75
|
+
.frame(minHeight: 50)
|
|
76
|
+
.background(Theme.fieldFill, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
extension View {
|
|
81
|
+
func fieldStyle() -> some View { modifier(FieldStyle()) }
|
|
82
|
+
}
|
package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift
CHANGED
|
@@ -1,57 +1,14 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
-
import PylonClient
|
|
3
|
-
import PylonSync
|
|
4
2
|
|
|
5
3
|
@main
|
|
6
4
|
struct __APP_NAME_PASCAL__App: App {
|
|
7
|
-
@StateObject private var
|
|
5
|
+
@StateObject private var app = AppModel()
|
|
8
6
|
|
|
9
7
|
var body: some Scene {
|
|
10
8
|
WindowGroup {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.environmentObject(session)
|
|
15
|
-
} else {
|
|
16
|
-
ProgressView()
|
|
17
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
.task { await session.bootIfNeeded() }
|
|
9
|
+
RootView()
|
|
10
|
+
.environmentObject(app)
|
|
11
|
+
.task { await app.boot() }
|
|
21
12
|
}
|
|
22
13
|
}
|
|
23
14
|
}
|
|
24
|
-
|
|
25
|
-
@MainActor
|
|
26
|
-
final class AppSession: ObservableObject {
|
|
27
|
-
let client: PylonClient
|
|
28
|
-
@Published private(set) var engine: SyncEngine?
|
|
29
|
-
@Published var myProfileId: String?
|
|
30
|
-
|
|
31
|
-
init() {
|
|
32
|
-
let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
|
|
33
|
-
?? "http://localhost:4321"
|
|
34
|
-
guard let url = URL(string: baseURLString) else {
|
|
35
|
-
fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
|
|
36
|
-
}
|
|
37
|
-
self.client = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
|
|
38
|
-
self.myProfileId = UserDefaults.standard.string(forKey: "myProfileId")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
func bootIfNeeded() async {
|
|
42
|
-
guard engine == nil else { return }
|
|
43
|
-
let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
|
|
44
|
-
?? "http://localhost:4321"
|
|
45
|
-
guard let url = URL(string: baseURLString) else { return }
|
|
46
|
-
let config = SyncEngineConfig(baseURL: url, appName: "__APP_NAME_SNAKE__")
|
|
47
|
-
let engine = await SyncEngine(config: config, client: client)
|
|
48
|
-
await engine.start()
|
|
49
|
-
self.engine = engine
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
func setMyProfileId(_ id: String?) {
|
|
53
|
-
myProfileId = id
|
|
54
|
-
if let id { UserDefaults.standard.set(id, forKey: "myProfileId") }
|
|
55
|
-
else { UserDefaults.standard.removeObject(forKey: "myProfileId") }
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
# Materialize the Xcode project once with:
|
|
4
4
|
# brew install xcodegen
|
|
5
5
|
# xcodegen generate
|
|
6
|
-
# Then open __APP_NAME_PASCAL__.xcodeproj.
|
|
6
|
+
# Then open __APP_NAME_PASCAL__.xcodeproj. Run xcodegen again after you add
|
|
7
|
+
# or remove Swift files.
|
|
8
|
+
#
|
|
9
|
+
# The app talks to the Pylon backend in apps/api. Start it with `pylon dev`
|
|
10
|
+
# there. The simulator reaches http://localhost:4321; on a device, set
|
|
11
|
+
# PYLON_BASE_URL in the scheme's environment to your Mac's LAN address or
|
|
12
|
+
# your deployed API.
|
|
7
13
|
|
|
8
14
|
name: __APP_NAME_PASCAL__
|
|
9
15
|
options:
|
|
10
16
|
bundleIdPrefix: com.example
|
|
11
17
|
deploymentTarget:
|
|
12
|
-
iOS: "
|
|
18
|
+
iOS: "17.0"
|
|
13
19
|
|
|
14
20
|
packages:
|
|
15
21
|
pylon:
|
|
16
22
|
url: https://github.com/pylonsync/pylon.git
|
|
17
|
-
from: "
|
|
23
|
+
from: "__PYLON_VERSION__"
|
|
18
24
|
|
|
19
25
|
targets:
|
|
20
26
|
__APP_NAME_PASCAL__:
|
|
@@ -25,15 +31,13 @@ targets:
|
|
|
25
31
|
settings:
|
|
26
32
|
base:
|
|
27
33
|
GENERATE_INFOPLIST_FILE: YES
|
|
34
|
+
INFOPLIST_KEY_CFBundleDisplayName: "__APP_NAME__"
|
|
28
35
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
|
|
29
36
|
INFOPLIST_KEY_UILaunchScreen_Generation: YES
|
|
30
|
-
INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait
|
|
31
|
-
TARGETED_DEVICE_FAMILY: "1
|
|
32
|
-
SWIFT_VERSION: "5.
|
|
37
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait"
|
|
38
|
+
TARGETED_DEVICE_FAMILY: "1"
|
|
39
|
+
SWIFT_VERSION: "5.0"
|
|
33
40
|
ENABLE_PREVIEWS: YES
|
|
34
|
-
# __APP_NAME__ talks to a Pylon backend on localhost during dev.
|
|
35
|
-
# The simulator can reach localhost directly; on a physical device
|
|
36
|
-
# set PYLON_BASE_URL to your machine's LAN IP.
|
|
37
41
|
INFOPLIST_KEY_NSAppTransportSecurity: "{NSAllowsLocalNetworking = YES;}"
|
|
38
42
|
dependencies:
|
|
39
43
|
- package: pylon
|
|
@@ -10,3 +10,10 @@ PYLON_OWNER_EMAIL=you@yourshop.com
|
|
|
10
10
|
# ── Site URL (optional) ──────────────────────────────────────────────────────
|
|
11
11
|
# Used by robots.txt + sitemap.xml. Point it at your real domain in production.
|
|
12
12
|
# SITE_URL=https://yourshop.com
|
|
13
|
+
|
|
14
|
+
# ── Demo data ────────────────────────────────────────────────────────────────
|
|
15
|
+
# `pylon dev` fills an empty booking book with fictional appointments. Seeding
|
|
16
|
+
# runs only under `pylon dev` (it checks PYLON_DEV_MODE and
|
|
17
|
+
# PYLON_DEV_WATCH_DIR), so `pylon start`, Docker, and Pylon Cloud deploys never
|
|
18
|
+
# seed. Set 0 to start empty in development. Never set 1 on a deploy.
|
|
19
|
+
# PYLON_DEMO_DATA=0
|
|
@@ -45,6 +45,23 @@ Set `PYLON_OWNER_EMAIL` in `.env` (see `.env.example`) to the email you'll sign
|
|
|
45
45
|
in with, then create that account at `/login`. Only that account can see
|
|
46
46
|
bookings.
|
|
47
47
|
|
|
48
|
+
## Demo data
|
|
49
|
+
|
|
50
|
+
In `pylon dev`, an empty booking book gets fictional appointments from the past
|
|
51
|
+
week and the next ten days (`functions/seedDemo.ts`, `lib/demo.ts`). Each one
|
|
52
|
+
writes a `Booking` and its `BookedSlot`, like a real booking, so the public grid
|
|
53
|
+
shows those times as booked and the dashboard lists them. Seeding runs only
|
|
54
|
+
when `PYLON_DEMO_DATA` is on, or when it is unset and the app runs under `pylon dev`,
|
|
55
|
+
and only while the `Booking` table is empty. `pylon start`, Docker, and Pylon Cloud deploys do not seed, because only `pylon dev` sets `PYLON_DEV_WATCH_DIR`. Set `PYLON_DEMO_DATA=0` to start empty in
|
|
56
|
+
development. Delete both files once you take real bookings.
|
|
57
|
+
|
|
58
|
+
## Map or hours board
|
|
59
|
+
|
|
60
|
+
Set `location.mapEmbedUrl` in `lib/site.config.ts` to a Google Maps embed URL to
|
|
61
|
+
show a map beside the address. Without one, the site shows the weekly hours
|
|
62
|
+
(from `booking.hours`, the same hours the booking grid uses) and a directions
|
|
63
|
+
link. The dashboard reminds you while the URL is empty.
|
|
64
|
+
|
|
48
65
|
## Rebrand + reconfigure it
|
|
49
66
|
|
|
50
67
|
Brand, colors, services, weekly hours, slot length, lead time, reviews,
|
|
@@ -52,6 +69,10 @@ location, and FAQ content live in **`lib/site.config.ts`**. Editing or
|
|
|
52
69
|
generating that file updates both the site and booking engine. Services and
|
|
53
70
|
hours stay in config rather than a separate database.
|
|
54
71
|
|
|
72
|
+
The tab icon is `app/icon.svg` (with `public/favicon.ico` for browsers that
|
|
73
|
+
request `/favicon.ico`). Both carry this brand's letter and colors; replace
|
|
74
|
+
them when you rename the brand.
|
|
75
|
+
|
|
55
76
|
## Layout
|
|
56
77
|
|
|
57
78
|
```
|
|
@@ -59,12 +80,15 @@ app.ts data model (Booking, BookedSlot, User) + policies
|
|
|
59
80
|
lib/site.config.ts ALL copy + brand + services + hours (edit this)
|
|
60
81
|
lib/slots.ts pure slot math, shared by picker + server re-check
|
|
61
82
|
lib/booking.ts shared booking-row types
|
|
83
|
+
lib/hours.ts weekly hours rows + directions link
|
|
84
|
+
lib/demo.ts demo bookings + the dev-only gate
|
|
62
85
|
lib/owner.ts owner-email gate (PYLON_OWNER_EMAIL)
|
|
63
86
|
functions/createBooking.ts public mutation: re-check + book (race-safe)
|
|
64
87
|
functions/bookingsForOwner.ts owner-only query: bookings + customer PII
|
|
65
88
|
functions/{confirm,cancel}Booking.ts owner-only mutations
|
|
66
|
-
|
|
67
|
-
app/
|
|
89
|
+
functions/seedDemo.ts dev-only demo bookings (see Demo data)
|
|
90
|
+
app/(marketing)/page.tsx the landing page (server-rendered)
|
|
91
|
+
app/(marketing)/booking-widget.tsx client island: live slot picker + booking form
|
|
68
92
|
app/login/page.tsx owner sign-in
|
|
69
93
|
app/dashboard/ owner dashboard (auth-gated, live)
|
|
70
94
|
app/globals.css Tailwind entrypoint (compiled by Pylon)
|