mvc-kit 2.12.5 → 2.13.1
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/BEST_PRACTICES.md +1390 -0
- package/agent-config/bin/postinstall.mjs +4 -3
- package/agent-config/bin/setup.mjs +5 -1
- package/agent-config/claude-code/agents/mvc-kit-architect.md +16 -8
- package/agent-config/claude-code/skills/guide/SKILL.md +29 -7
- package/agent-config/claude-code/skills/guide/patterns.md +12 -0
- package/agent-config/claude-code/skills/guide/recipes.md +510 -0
- package/agent-config/claude-code/skills/guide/testing.md +297 -0
- package/agent-config/claude-code/skills/review/SKILL.md +3 -13
- package/agent-config/claude-code/skills/review/checklist.md +30 -5
- package/agent-config/claude-code/skills/scaffold/SKILL.md +4 -13
- package/agent-config/lib/install-claude.mjs +90 -25
- package/dist/Channel.cjs +276 -300
- package/dist/Channel.cjs.map +1 -1
- package/dist/Channel.js +275 -299
- package/dist/Channel.js.map +1 -1
- package/dist/Collection.cjs +424 -504
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.js +423 -503
- package/dist/Collection.js.map +1 -1
- package/dist/Controller.cjs +70 -67
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.js +69 -66
- package/dist/Controller.js.map +1 -1
- package/dist/EventBus.cjs +77 -88
- package/dist/EventBus.cjs.map +1 -1
- package/dist/EventBus.js +76 -87
- package/dist/EventBus.js.map +1 -1
- package/dist/Feed.cjs +81 -77
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.js +80 -76
- package/dist/Feed.js.map +1 -1
- package/dist/Model.cjs +181 -207
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.js +179 -205
- package/dist/Model.js.map +1 -1
- package/dist/Pagination.cjs +75 -73
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.js +74 -72
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +255 -287
- package/dist/Pending.cjs.map +1 -1
- package/dist/Pending.js +253 -285
- package/dist/Pending.js.map +1 -1
- package/dist/PersistentCollection.cjs +242 -285
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.js +241 -284
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +166 -174
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.js +164 -172
- package/dist/Resource.js.map +1 -1
- package/dist/Selection.cjs +84 -94
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.js +83 -93
- package/dist/Selection.js.map +1 -1
- package/dist/Service.cjs +54 -55
- package/dist/Service.cjs.map +1 -1
- package/dist/Service.js +53 -54
- package/dist/Service.js.map +1 -1
- package/dist/Sorting.cjs +102 -101
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.js +102 -101
- package/dist/Sorting.js.map +1 -1
- package/dist/Trackable.cjs +112 -80
- package/dist/Trackable.cjs.map +1 -1
- package/dist/Trackable.js +111 -79
- package/dist/Trackable.js.map +1 -1
- package/dist/ViewModel.cjs +528 -576
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.js +525 -573
- package/dist/ViewModel.js.map +1 -1
- package/dist/bindPublicMethods.cjs +43 -24
- package/dist/bindPublicMethods.cjs.map +1 -1
- package/dist/bindPublicMethods.js +43 -24
- package/dist/bindPublicMethods.js.map +1 -1
- package/dist/errors.cjs +67 -68
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +68 -71
- package/dist/errors.js.map +1 -1
- package/dist/mvc-kit.cjs +44 -46
- package/dist/mvc-kit.js +5 -32
- package/dist/produceDraft.cjs +105 -95
- package/dist/produceDraft.cjs.map +1 -1
- package/dist/produceDraft.js +106 -97
- package/dist/produceDraft.js.map +1 -1
- package/dist/react/components/CardList.cjs +30 -40
- package/dist/react/components/CardList.cjs.map +1 -1
- package/dist/react/components/CardList.js +31 -41
- package/dist/react/components/CardList.js.map +1 -1
- package/dist/react/components/DataTable.cjs +146 -169
- package/dist/react/components/DataTable.cjs.map +1 -1
- package/dist/react/components/DataTable.js +147 -170
- package/dist/react/components/DataTable.js.map +1 -1
- package/dist/react/components/InfiniteScroll.cjs +51 -42
- package/dist/react/components/InfiniteScroll.cjs.map +1 -1
- package/dist/react/components/InfiniteScroll.js +52 -43
- package/dist/react/components/InfiniteScroll.js.map +1 -1
- package/dist/react/components/types.cjs +10 -6
- package/dist/react/components/types.cjs.map +1 -1
- package/dist/react/components/types.js +11 -9
- package/dist/react/components/types.js.map +1 -1
- package/dist/react/guards.cjs +10 -6
- package/dist/react/guards.cjs.map +1 -1
- package/dist/react/guards.js +11 -9
- package/dist/react/guards.js.map +1 -1
- package/dist/react/provider.cjs +23 -20
- package/dist/react/provider.cjs.map +1 -1
- package/dist/react/provider.js +23 -21
- package/dist/react/provider.js.map +1 -1
- package/dist/react/use-event-bus.cjs +24 -20
- package/dist/react/use-event-bus.cjs.map +1 -1
- package/dist/react/use-event-bus.js +24 -21
- package/dist/react/use-event-bus.js.map +1 -1
- package/dist/react/use-instance.cjs +43 -36
- package/dist/react/use-instance.cjs.map +1 -1
- package/dist/react/use-instance.js +43 -36
- package/dist/react/use-instance.js.map +1 -1
- package/dist/react/use-local.cjs +48 -64
- package/dist/react/use-local.cjs.map +1 -1
- package/dist/react/use-local.js +47 -63
- package/dist/react/use-local.js.map +1 -1
- package/dist/react/use-model.cjs +84 -98
- package/dist/react/use-model.cjs.map +1 -1
- package/dist/react/use-model.js +84 -100
- package/dist/react/use-model.js.map +1 -1
- package/dist/react/use-singleton.cjs +19 -23
- package/dist/react/use-singleton.cjs.map +1 -1
- package/dist/react/use-singleton.js +16 -20
- package/dist/react/use-singleton.js.map +1 -1
- package/dist/react/use-subscribe-only.cjs +28 -22
- package/dist/react/use-subscribe-only.cjs.map +1 -1
- package/dist/react/use-subscribe-only.js +28 -22
- package/dist/react/use-subscribe-only.js.map +1 -1
- package/dist/react/use-teardown.cjs +20 -19
- package/dist/react/use-teardown.cjs.map +1 -1
- package/dist/react/use-teardown.js +20 -19
- package/dist/react/use-teardown.js.map +1 -1
- package/dist/react-native/NativeCollection.cjs +98 -78
- package/dist/react-native/NativeCollection.cjs.map +1 -1
- package/dist/react-native/NativeCollection.js +97 -77
- package/dist/react-native/NativeCollection.js.map +1 -1
- package/dist/react-native.cjs +2 -4
- package/dist/react-native.js +1 -4
- package/dist/react.cjs +24 -26
- package/dist/react.js +1 -17
- package/dist/singleton.cjs +28 -22
- package/dist/singleton.cjs.map +1 -1
- package/dist/singleton.js +29 -26
- package/dist/singleton.js.map +1 -1
- package/dist/walkPrototypeChain.cjs +20 -12
- package/dist/walkPrototypeChain.cjs.map +1 -1
- package/dist/walkPrototypeChain.js +21 -13
- package/dist/walkPrototypeChain.js.map +1 -1
- package/dist/web/IndexedDBCollection.cjs +53 -36
- package/dist/web/IndexedDBCollection.cjs.map +1 -1
- package/dist/web/IndexedDBCollection.js +52 -35
- package/dist/web/IndexedDBCollection.js.map +1 -1
- package/dist/web/WebStorageCollection.cjs +82 -84
- package/dist/web/WebStorageCollection.cjs.map +1 -1
- package/dist/web/WebStorageCollection.js +81 -83
- package/dist/web/WebStorageCollection.js.map +1 -1
- package/dist/web/idb.cjs +107 -99
- package/dist/web/idb.cjs.map +1 -1
- package/dist/web/idb.js +108 -105
- package/dist/web/idb.js.map +1 -1
- package/dist/web.cjs +4 -6
- package/dist/web.js +1 -5
- package/dist/wrapAsyncMethods.cjs +141 -168
- package/dist/wrapAsyncMethods.cjs.map +1 -1
- package/dist/wrapAsyncMethods.js +141 -168
- package/dist/wrapAsyncMethods.js.map +1 -1
- package/examples/primitive/channel.ts +109 -0
- package/examples/primitive/collection.ts +118 -0
- package/examples/primitive/controller.ts +118 -0
- package/examples/primitive/counter.ts +108 -0
- package/examples/primitive/env.d.ts +1 -0
- package/examples/primitive/eventbus.ts +77 -0
- package/examples/primitive/feed.ts +162 -0
- package/examples/primitive/model.ts +82 -0
- package/examples/primitive/pagination.ts +91 -0
- package/examples/primitive/pending.ts +189 -0
- package/examples/primitive/persistent-collection.ts +116 -0
- package/examples/primitive/resource.ts +114 -0
- package/examples/primitive/selection.ts +96 -0
- package/examples/primitive/sorting.ts +112 -0
- package/examples/primitive/timer.ts +58 -0
- package/examples/primitive/trackable.ts +225 -0
- package/examples/primitive/tsconfig.json +20 -0
- package/examples/primitive/viewmodel-service.ts +161 -0
- package/examples/react/AuthExample/index.html +12 -0
- package/examples/react/AuthExample/src/App.tsx +29 -0
- package/examples/react/AuthExample/src/components/AdminPage.tsx +51 -0
- package/examples/react/AuthExample/src/components/AppHeader.tsx +32 -0
- package/examples/react/AuthExample/src/components/AuthGuard.tsx +50 -0
- package/examples/react/AuthExample/src/components/AuthScreen.tsx +181 -0
- package/examples/react/AuthExample/src/components/DashboardPage.tsx +41 -0
- package/examples/react/AuthExample/src/components/ProfilePage.tsx +44 -0
- package/examples/react/AuthExample/src/components/Toast.tsx +41 -0
- package/examples/react/AuthExample/src/env.d.ts +10 -0
- package/examples/react/AuthExample/src/events/AppEventBus.ts +7 -0
- package/examples/react/AuthExample/src/main.tsx +10 -0
- package/examples/react/AuthExample/src/mock/api.ts +78 -0
- package/examples/react/AuthExample/src/models/LoginFormModel.ts +19 -0
- package/examples/react/AuthExample/src/models/RegisterFormModel.ts +25 -0
- package/examples/react/AuthExample/src/services/AuthService.ts +21 -0
- package/examples/react/AuthExample/src/styles.css +445 -0
- package/examples/react/AuthExample/src/types/auth.ts +12 -0
- package/examples/react/AuthExample/src/viewmodels/AuthViewModel.ts +111 -0
- package/examples/react/AuthExample/tsconfig.json +22 -0
- package/examples/react/AuthExample/vite.config.ts +18 -0
- package/examples/react/ComplexApp/index.html +12 -0
- package/examples/react/ComplexApp/src/App.tsx +17 -0
- package/examples/react/ComplexApp/src/channels/ActivityChannel.ts +24 -0
- package/examples/react/ComplexApp/src/channels/DashboardChannel.ts +26 -0
- package/examples/react/ComplexApp/src/channels/ErrorsChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/LatencyChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/OrdersChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/RevenueChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/TrafficChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/UsersMetricChannel.ts +5 -0
- package/examples/react/ComplexApp/src/collections/DashboardCollection.ts +6 -0
- package/examples/react/ComplexApp/src/collections/ErrorsCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/LatencyCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/OrdersCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/RevenueCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/TrafficCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/UsersMetricCollection.ts +3 -0
- package/examples/react/ComplexApp/src/components/activity/ActivityFeed.tsx +31 -0
- package/examples/react/ComplexApp/src/components/activity/ActivityItemRow.tsx +35 -0
- package/examples/react/ComplexApp/src/components/dashboard/DashboardCard.tsx +37 -0
- package/examples/react/ComplexApp/src/components/dashboard/DashboardPage.tsx +34 -0
- package/examples/react/ComplexApp/src/components/layout/Navbar.tsx +32 -0
- package/examples/react/ComplexApp/src/components/layout/SocialFeedPanel.tsx +57 -0
- package/examples/react/ComplexApp/src/components/shared/Spinner.tsx +3 -0
- package/examples/react/ComplexApp/src/components/shared/StatusIndicator.tsx +13 -0
- package/examples/react/ComplexApp/src/components/shared/Toast.tsx +40 -0
- package/examples/react/ComplexApp/src/env.d.ts +10 -0
- package/examples/react/ComplexApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/ComplexApp/src/main.tsx +10 -0
- package/examples/react/ComplexApp/src/mock-remote/MockWebSocket.ts +38 -0
- package/examples/react/ComplexApp/src/mock-remote/activity-api.ts +48 -0
- package/examples/react/ComplexApp/src/mock-remote/dashboard-generators.ts +45 -0
- package/examples/react/ComplexApp/src/mock-remote/delay.ts +18 -0
- package/examples/react/ComplexApp/src/mock-remote/social-api.ts +55 -0
- package/examples/react/ComplexApp/src/resources/ActivityResource.ts +12 -0
- package/examples/react/ComplexApp/src/resources/SocialFeedResource.ts +17 -0
- package/examples/react/ComplexApp/src/styles.css +463 -0
- package/examples/react/ComplexApp/src/types/activity.ts +8 -0
- package/examples/react/ComplexApp/src/types/dashboard.ts +5 -0
- package/examples/react/ComplexApp/src/types/social.ts +8 -0
- package/examples/react/ComplexApp/src/types/users.ts +6 -0
- package/examples/react/ComplexApp/src/viewmodels/ActivityFeedViewModel.ts +68 -0
- package/examples/react/ComplexApp/src/viewmodels/AppStateViewModel.ts +26 -0
- package/examples/react/ComplexApp/src/viewmodels/DashboardCardViewModel.ts +69 -0
- package/examples/react/ComplexApp/src/viewmodels/ErrorsCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/LatencyCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/OrdersCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/RevenueCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/SocialFeedViewModel.ts +39 -0
- package/examples/react/ComplexApp/src/viewmodels/TrafficCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/UsersMetricCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/tsconfig.json +22 -0
- package/examples/react/ComplexApp/vite.config.ts +18 -0
- package/examples/react/FullApp/index.html +12 -0
- package/examples/react/FullApp/src/App.tsx +28 -0
- package/examples/react/FullApp/src/collections/ConversationsCollection.ts +4 -0
- package/examples/react/FullApp/src/collections/LocationsCollection.ts +4 -0
- package/examples/react/FullApp/src/components/auth/LoginPage.tsx +80 -0
- package/examples/react/FullApp/src/components/dashboard/DashboardPage.tsx +29 -0
- package/examples/react/FullApp/src/components/dashboard/RecentActivityCard.tsx +35 -0
- package/examples/react/FullApp/src/components/dashboard/StatsCard.tsx +19 -0
- package/examples/react/FullApp/src/components/layout/AppShell.tsx +31 -0
- package/examples/react/FullApp/src/components/layout/Header.tsx +25 -0
- package/examples/react/FullApp/src/components/layout/Sidebar.tsx +29 -0
- package/examples/react/FullApp/src/components/locations/LocationFilters.tsx +60 -0
- package/examples/react/FullApp/src/components/locations/LocationForm.tsx +112 -0
- package/examples/react/FullApp/src/components/locations/LocationProfilePage.tsx +81 -0
- package/examples/react/FullApp/src/components/locations/LocationsPage.tsx +127 -0
- package/examples/react/FullApp/src/components/messaging/ConversationList.tsx +59 -0
- package/examples/react/FullApp/src/components/messaging/MessageBubble.tsx +22 -0
- package/examples/react/FullApp/src/components/messaging/MessageThread.tsx +100 -0
- package/examples/react/FullApp/src/components/messaging/MessagingPage.tsx +52 -0
- package/examples/react/FullApp/src/components/shared/ErrorBanner.tsx +3 -0
- package/examples/react/FullApp/src/components/shared/Spinner.tsx +7 -0
- package/examples/react/FullApp/src/components/shared/Toast.tsx +41 -0
- package/examples/react/FullApp/src/components/users/UserFilters.tsx +59 -0
- package/examples/react/FullApp/src/components/users/UsersPage.tsx +80 -0
- package/examples/react/FullApp/src/components/users/UsersTable.tsx +52 -0
- package/examples/react/FullApp/src/env.d.ts +10 -0
- package/examples/react/FullApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/FullApp/src/main.tsx +10 -0
- package/examples/react/FullApp/src/mock/delay.ts +21 -0
- package/examples/react/FullApp/src/mock/locations.ts +76 -0
- package/examples/react/FullApp/src/mock/messages.ts +237 -0
- package/examples/react/FullApp/src/mock/users.ts +84 -0
- package/examples/react/FullApp/src/models/LocationFormModel.ts +31 -0
- package/examples/react/FullApp/src/models/LoginFormModel.ts +19 -0
- package/examples/react/FullApp/src/resources/UsersResource.ts +12 -0
- package/examples/react/FullApp/src/services/AuthService.ts +18 -0
- package/examples/react/FullApp/src/services/LocationService.ts +23 -0
- package/examples/react/FullApp/src/services/MessageService.ts +65 -0
- package/examples/react/FullApp/src/services/UserService.ts +23 -0
- package/examples/react/FullApp/src/styles.css +767 -0
- package/examples/react/FullApp/src/types/conversation.ts +7 -0
- package/examples/react/FullApp/src/types/location.ts +12 -0
- package/examples/react/FullApp/src/types/message.ts +7 -0
- package/examples/react/FullApp/src/types/user.ts +10 -0
- package/examples/react/FullApp/src/viewmodels/AuthViewModel.ts +51 -0
- package/examples/react/FullApp/src/viewmodels/ConversationsViewModel.ts +89 -0
- package/examples/react/FullApp/src/viewmodels/DashboardViewModel.ts +56 -0
- package/examples/react/FullApp/src/viewmodels/LocationProfileViewModel.ts +81 -0
- package/examples/react/FullApp/src/viewmodels/LocationsViewModel.ts +113 -0
- package/examples/react/FullApp/src/viewmodels/MessageThreadViewModel.ts +83 -0
- package/examples/react/FullApp/src/viewmodels/UsersViewModel.ts +88 -0
- package/examples/react/FullApp/tsconfig.json +22 -0
- package/examples/react/FullApp/vite.config.ts +18 -0
- package/examples/react/WorkerApp/index.html +12 -0
- package/examples/react/WorkerApp/src/App.tsx +24 -0
- package/examples/react/WorkerApp/src/channels/MessagingChannel.ts +46 -0
- package/examples/react/WorkerApp/src/channels/WorkerStatusChannel.ts +35 -0
- package/examples/react/WorkerApp/src/components/auth/LoginPage.tsx +60 -0
- package/examples/react/WorkerApp/src/components/layout/AppShell.tsx +31 -0
- package/examples/react/WorkerApp/src/components/layout/Header.tsx +23 -0
- package/examples/react/WorkerApp/src/components/layout/Sidebar.tsx +28 -0
- package/examples/react/WorkerApp/src/components/messaging/ComposeBar.tsx +33 -0
- package/examples/react/WorkerApp/src/components/messaging/ConversationList.tsx +59 -0
- package/examples/react/WorkerApp/src/components/messaging/MessageBubble.tsx +45 -0
- package/examples/react/WorkerApp/src/components/messaging/MessageThread.tsx +93 -0
- package/examples/react/WorkerApp/src/components/messaging/MessagingPage.tsx +53 -0
- package/examples/react/WorkerApp/src/components/shared/ErrorBanner.tsx +3 -0
- package/examples/react/WorkerApp/src/components/shared/PendingBanner.tsx +37 -0
- package/examples/react/WorkerApp/src/components/shared/Spinner.tsx +7 -0
- package/examples/react/WorkerApp/src/components/shared/Toast.tsx +41 -0
- package/examples/react/WorkerApp/src/components/shift/ShiftPage.tsx +98 -0
- package/examples/react/WorkerApp/src/components/shift/ShiftTimer.tsx +24 -0
- package/examples/react/WorkerApp/src/components/shift/SiteSelector.tsx +27 -0
- package/examples/react/WorkerApp/src/components/sites/SiteFilters.tsx +61 -0
- package/examples/react/WorkerApp/src/components/sites/SitesPage.tsx +102 -0
- package/examples/react/WorkerApp/src/env.d.ts +10 -0
- package/examples/react/WorkerApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/WorkerApp/src/main.tsx +10 -0
- package/examples/react/WorkerApp/src/mock/MockWebSocket.ts +38 -0
- package/examples/react/WorkerApp/src/mock/delay.ts +31 -0
- package/examples/react/WorkerApp/src/mock/messages.ts +120 -0
- package/examples/react/WorkerApp/src/mock/shifts.ts +57 -0
- package/examples/react/WorkerApp/src/mock/sites.ts +14 -0
- package/examples/react/WorkerApp/src/mock/workers.ts +12 -0
- package/examples/react/WorkerApp/src/models/ComposeMessageModel.ts +17 -0
- package/examples/react/WorkerApp/src/resources/ConversationsResource.ts +10 -0
- package/examples/react/WorkerApp/src/resources/MessagesResource.ts +32 -0
- package/examples/react/WorkerApp/src/resources/ShiftResource.ts +73 -0
- package/examples/react/WorkerApp/src/resources/SitesResource.ts +11 -0
- package/examples/react/WorkerApp/src/resources/WorkersResource.ts +11 -0
- package/examples/react/WorkerApp/src/styles.css +756 -0
- package/examples/react/WorkerApp/src/types/conversation.ts +7 -0
- package/examples/react/WorkerApp/src/types/message.ts +7 -0
- package/examples/react/WorkerApp/src/types/shift.ts +13 -0
- package/examples/react/WorkerApp/src/types/site.ts +8 -0
- package/examples/react/WorkerApp/src/types/worker.ts +8 -0
- package/examples/react/WorkerApp/src/viewmodels/AuthViewModel.ts +41 -0
- package/examples/react/WorkerApp/src/viewmodels/ConversationsViewModel.ts +83 -0
- package/examples/react/WorkerApp/src/viewmodels/MessageThreadViewModel.ts +113 -0
- package/examples/react/WorkerApp/src/viewmodels/ShiftViewModel.ts +147 -0
- package/examples/react/WorkerApp/src/viewmodels/SitesViewModel.ts +82 -0
- package/examples/react/WorkerApp/tsconfig.json +22 -0
- package/examples/react/WorkerApp/vite.config.ts +18 -0
- package/package.json +11 -9
- package/src/Pending.test.ts +1 -2
- package/src/Sorting.test.ts +1 -1
- package/src/produceDraft.test.ts +3 -3
- package/src/react/components/CardList.test.tsx +1 -1
- package/src/react/components/DataTable.test.tsx +1 -1
- package/src/react/components/InfiniteScroll.test.tsx +5 -5
- package/dist/mvc-kit.cjs.map +0 -1
- package/dist/mvc-kit.js.map +0 -1
- package/dist/react-native.cjs.map +0 -1
- package/dist/react-native.js.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/web.cjs.map +0 -1
- package/dist/web.js.map +0 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useLocal } from 'mvc-kit/react';
|
|
2
|
+
import { ActivityFeedViewModel } from '../../viewmodels/ActivityFeedViewModel';
|
|
3
|
+
import { ActivityItemRow } from './ActivityItemRow';
|
|
4
|
+
import { StatusIndicator } from '../shared/StatusIndicator';
|
|
5
|
+
import { Spinner } from '../shared/Spinner';
|
|
6
|
+
|
|
7
|
+
export function ActivityFeed() {
|
|
8
|
+
const [, vm] = useLocal(ActivityFeedViewModel);
|
|
9
|
+
const { loading, error } = vm.async.load;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="activity-feed">
|
|
13
|
+
<div className="section-header">
|
|
14
|
+
<h2>Activity Feed</h2>
|
|
15
|
+
<StatusIndicator connected={vm.isConnected} label={vm.isOnline ? 'Live' : 'Offline'} />
|
|
16
|
+
<span className="item-count">{vm.itemCount} items</span>
|
|
17
|
+
</div>
|
|
18
|
+
{loading && vm.itemCount === 0 && (
|
|
19
|
+
<div className="loading-center"><Spinner /></div>
|
|
20
|
+
)}
|
|
21
|
+
{error && (
|
|
22
|
+
<div className="error-banner">{error}</div>
|
|
23
|
+
)}
|
|
24
|
+
<div className="activity-list">
|
|
25
|
+
{vm.items.map(item => (
|
|
26
|
+
<ActivityItemRow key={item.id} item={item} />
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ActivityItem } from '../../types/activity';
|
|
2
|
+
|
|
3
|
+
interface ActivityItemRowProps {
|
|
4
|
+
item: ActivityItem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ActivityItemRow({ item }: ActivityItemRowProps) {
|
|
8
|
+
const timeAgo = getTimeAgo(item.timestamp);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="activity-row">
|
|
12
|
+
<img
|
|
13
|
+
className={`activity-avatar ${item.userOnline ? '' : 'avatar-offline'}`}
|
|
14
|
+
src={item.userAvatar}
|
|
15
|
+
alt={item.userName}
|
|
16
|
+
width={36}
|
|
17
|
+
height={36}
|
|
18
|
+
/>
|
|
19
|
+
<div className="activity-content">
|
|
20
|
+
<span className="activity-text">{item.text}</span>
|
|
21
|
+
<span className="activity-time">{timeAgo}</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getTimeAgo(timestamp: string): string {
|
|
28
|
+
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
29
|
+
if (seconds < 5) return 'just now';
|
|
30
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
31
|
+
const minutes = Math.floor(seconds / 60);
|
|
32
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
33
|
+
const hours = Math.floor(minutes / 60);
|
|
34
|
+
return `${hours}h ago`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useLocal } from 'mvc-kit/react';
|
|
2
|
+
import type { DashboardCardViewModel } from '../../viewmodels/DashboardCardViewModel';
|
|
3
|
+
import { StatusIndicator } from '../shared/StatusIndicator';
|
|
4
|
+
|
|
5
|
+
interface DashboardCardProps {
|
|
6
|
+
VMClass: new () => DashboardCardViewModel;
|
|
7
|
+
title: string;
|
|
8
|
+
icon: string;
|
|
9
|
+
unit?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DashboardCard({ VMClass, title, icon, unit }: DashboardCardProps) {
|
|
13
|
+
const [, vm] = useLocal(VMClass);
|
|
14
|
+
|
|
15
|
+
const trendArrow = vm.trend === 'up' ? '\u2191' : vm.trend === 'down' ? '\u2193' : '\u2192';
|
|
16
|
+
const trendClass = `trend-${vm.trend}`;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={`card ${vm.isOnline ? '' : 'card-disconnected'}`}>
|
|
20
|
+
<div className="card-header">
|
|
21
|
+
<span className="card-icon">{icon}</span>
|
|
22
|
+
<span className="card-title">{title}</span>
|
|
23
|
+
<StatusIndicator connected={vm.isConnected} />
|
|
24
|
+
</div>
|
|
25
|
+
<div className="card-value">
|
|
26
|
+
<span className="card-number">
|
|
27
|
+
{vm.latestValue.toLocaleString()}
|
|
28
|
+
</span>
|
|
29
|
+
{unit && <span className="card-unit">{unit}</span>}
|
|
30
|
+
<span className={`card-trend ${trendClass}`}>{trendArrow}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div className="card-footer">
|
|
33
|
+
{vm.dataPointCount} data points
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DashboardCard } from './DashboardCard';
|
|
2
|
+
import type { DashboardCardViewModel } from '../../viewmodels/DashboardCardViewModel';
|
|
3
|
+
import { OrdersCardViewModel } from '../../viewmodels/OrdersCardViewModel';
|
|
4
|
+
import { RevenueCardViewModel } from '../../viewmodels/RevenueCardViewModel';
|
|
5
|
+
import { UsersMetricCardViewModel } from '../../viewmodels/UsersMetricCardViewModel';
|
|
6
|
+
import { ErrorsCardViewModel } from '../../viewmodels/ErrorsCardViewModel';
|
|
7
|
+
import { LatencyCardViewModel } from '../../viewmodels/LatencyCardViewModel';
|
|
8
|
+
import { TrafficCardViewModel } from '../../viewmodels/TrafficCardViewModel';
|
|
9
|
+
|
|
10
|
+
interface CardConfig {
|
|
11
|
+
VMClass: new () => DashboardCardViewModel;
|
|
12
|
+
title: string;
|
|
13
|
+
icon: string;
|
|
14
|
+
unit?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cards: CardConfig[] = [
|
|
18
|
+
{ VMClass: OrdersCardViewModel, title: 'Orders', icon: '\uD83D\uDCE6', unit: '/hr' },
|
|
19
|
+
{ VMClass: RevenueCardViewModel, title: 'Revenue', icon: '\uD83D\uDCB0', unit: '$' },
|
|
20
|
+
{ VMClass: UsersMetricCardViewModel, title: 'Active Users', icon: '\uD83D\uDC65' },
|
|
21
|
+
{ VMClass: ErrorsCardViewModel, title: 'Errors', icon: '\u26A0\uFE0F', unit: '/min' },
|
|
22
|
+
{ VMClass: LatencyCardViewModel, title: 'Latency', icon: '\u23F1\uFE0F', unit: 'ms' },
|
|
23
|
+
{ VMClass: TrafficCardViewModel, title: 'Traffic', icon: '\uD83C\uDF10', unit: 'req/s' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function DashboardPage() {
|
|
27
|
+
return (
|
|
28
|
+
<div className="dashboard-grid">
|
|
29
|
+
{cards.map(({ VMClass, title, icon, unit }) => (
|
|
30
|
+
<DashboardCard key={title} VMClass={VMClass} title={title} icon={icon} unit={unit} />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useSingleton } from 'mvc-kit/react';
|
|
2
|
+
import { AppStateViewModel } from '../../viewmodels/AppStateViewModel';
|
|
3
|
+
import { StatusIndicator } from '../shared/StatusIndicator';
|
|
4
|
+
import { SocialFeedPanel } from './SocialFeedPanel';
|
|
5
|
+
|
|
6
|
+
export function Navbar() {
|
|
7
|
+
const [state, vm] = useSingleton(AppStateViewModel);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<nav className="navbar">
|
|
12
|
+
<span className="navbar-brand">ComplexApp</span>
|
|
13
|
+
<div className="navbar-actions">
|
|
14
|
+
<StatusIndicator connected={state.online} label={state.online ? 'Online' : 'Offline'} />
|
|
15
|
+
<button
|
|
16
|
+
className={`btn ${state.online ? 'btn-danger' : 'btn-success'}`}
|
|
17
|
+
onClick={() => vm.toggleOnline()}
|
|
18
|
+
>
|
|
19
|
+
{state.online ? 'Go Offline' : 'Go Online'}
|
|
20
|
+
</button>
|
|
21
|
+
<button
|
|
22
|
+
className={`btn btn-secondary ${state.socialPanelOpen ? 'btn-active' : ''}`}
|
|
23
|
+
onClick={() => vm.toggleSocialPanel()}
|
|
24
|
+
>
|
|
25
|
+
Social Feed
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</nav>
|
|
29
|
+
{state.socialPanelOpen && <SocialFeedPanel />}
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useLocal } from 'mvc-kit/react';
|
|
2
|
+
import { InfiniteScroll, CardList } from 'mvc-kit/react';
|
|
3
|
+
import { SocialFeedViewModel } from '../../viewmodels/SocialFeedViewModel';
|
|
4
|
+
import { Spinner } from '../shared/Spinner';
|
|
5
|
+
import type { SocialPost } from '../../types/social';
|
|
6
|
+
|
|
7
|
+
function PostCard({ post }: { post: SocialPost }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="social-post">
|
|
10
|
+
<div className="social-post-header">
|
|
11
|
+
<img
|
|
12
|
+
className="social-avatar"
|
|
13
|
+
src={post.avatarUrl}
|
|
14
|
+
alt={post.author}
|
|
15
|
+
width={32}
|
|
16
|
+
height={32}
|
|
17
|
+
/>
|
|
18
|
+
<span className="social-author">{post.author}</span>
|
|
19
|
+
</div>
|
|
20
|
+
<p className="social-content">{post.content}</p>
|
|
21
|
+
<div className="social-footer">
|
|
22
|
+
<span className="social-likes">{'\u2764\uFE0F'} {post.likes}</span>
|
|
23
|
+
<span className="social-time">
|
|
24
|
+
{new Date(post.timestamp).toLocaleTimeString()}
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SocialFeedPanel() {
|
|
32
|
+
const [state, vm] = useLocal(SocialFeedViewModel, { page: 0 });
|
|
33
|
+
const { loading } = vm.async.loadMore;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="social-feed-panel">
|
|
37
|
+
<h3 className="social-feed-title">Social Feed</h3>
|
|
38
|
+
<InfiniteScroll
|
|
39
|
+
hasMore={vm.hasMore}
|
|
40
|
+
loading={loading}
|
|
41
|
+
onLoadMore={() => vm.loadMore()}
|
|
42
|
+
renderLoading={() => (
|
|
43
|
+
<div className="loading-center"><Spinner /></div>
|
|
44
|
+
)}
|
|
45
|
+
renderEnd={() =>
|
|
46
|
+
vm.postCount > 0 ? <p className="end-of-feed">No more posts</p> : null
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
<CardList
|
|
50
|
+
items={vm.posts}
|
|
51
|
+
keyOf={(post: SocialPost) => post.id}
|
|
52
|
+
renderItem={(post: SocialPost) => <PostCard post={post} />}
|
|
53
|
+
/>
|
|
54
|
+
</InfiniteScroll>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface StatusIndicatorProps {
|
|
2
|
+
connected: boolean;
|
|
3
|
+
label?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function StatusIndicator({ connected, label }: StatusIndicatorProps) {
|
|
7
|
+
return (
|
|
8
|
+
<span className="status-indicator">
|
|
9
|
+
<span className={`status-dot ${connected ? 'status-dot-online' : 'status-dot-offline'}`} />
|
|
10
|
+
{label && <span className="status-label">{label}</span>}
|
|
11
|
+
</span>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useSingleton, useEvent } from 'mvc-kit/react';
|
|
3
|
+
import { AppEventBus } from '../../events/AppEventBus';
|
|
4
|
+
|
|
5
|
+
interface ToastItem {
|
|
6
|
+
id: number;
|
|
7
|
+
message: string;
|
|
8
|
+
type: 'info' | 'success' | 'error';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let nextId = 0;
|
|
12
|
+
|
|
13
|
+
export function Toast() {
|
|
14
|
+
const bus = useSingleton(AppEventBus);
|
|
15
|
+
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
|
16
|
+
|
|
17
|
+
const addToast = useCallback((item: Omit<ToastItem, 'id'>) => {
|
|
18
|
+
const id = nextId++;
|
|
19
|
+
setToasts(prev => [...prev, { ...item, id }]);
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
setToasts(prev => prev.filter(t => t.id !== id));
|
|
22
|
+
}, 3000);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
useEvent(bus, 'toast:show', ({ message, type }) => {
|
|
26
|
+
addToast({ message, type: type ?? 'info' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (toasts.length === 0) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="toast-container">
|
|
33
|
+
{toasts.map(t => (
|
|
34
|
+
<div key={t.id} className={`toast toast-${t.type}`}>
|
|
35
|
+
{t.message}
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const __MVC_KIT_DEV__: boolean;
|
|
2
|
+
|
|
3
|
+
declare module 'react-dom/client' {
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
interface Root {
|
|
6
|
+
render(children: ReactNode): void;
|
|
7
|
+
unmount(): void;
|
|
8
|
+
}
|
|
9
|
+
export function createRoot(container: Element): Root;
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface MockWSConfig {
|
|
2
|
+
minInterval: number;
|
|
3
|
+
maxInterval: number;
|
|
4
|
+
generator: () => any;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class MockWebSocket {
|
|
8
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
9
|
+
private closed = false;
|
|
10
|
+
|
|
11
|
+
constructor(private config: MockWSConfig) {}
|
|
12
|
+
|
|
13
|
+
connect(onMessage: (data: any) => void, signal?: AbortSignal): void {
|
|
14
|
+
if (signal?.aborted) return;
|
|
15
|
+
signal?.addEventListener('abort', () => this.close(), { once: true });
|
|
16
|
+
this.scheduleNext(onMessage);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
close(): void {
|
|
20
|
+
if (this.closed) return;
|
|
21
|
+
this.closed = true;
|
|
22
|
+
if (this.timer !== null) {
|
|
23
|
+
clearTimeout(this.timer);
|
|
24
|
+
this.timer = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private scheduleNext(onMessage: (data: any) => void): void {
|
|
29
|
+
if (this.closed) return;
|
|
30
|
+
const { minInterval, maxInterval, generator } = this.config;
|
|
31
|
+
const interval = minInterval + Math.random() * (maxInterval - minInterval);
|
|
32
|
+
this.timer = setTimeout(() => {
|
|
33
|
+
if (this.closed) return;
|
|
34
|
+
onMessage(generator());
|
|
35
|
+
this.scheduleNext(onMessage);
|
|
36
|
+
}, interval);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ActivityItem } from '../types/activity';
|
|
2
|
+
import type { MockWSConfig } from './MockWebSocket';
|
|
3
|
+
import { mockFetch } from './delay';
|
|
4
|
+
|
|
5
|
+
const NAMES = [
|
|
6
|
+
'Alice Johnson', 'Bob Smith', 'Carol White', 'Dave Brown', 'Eve Davis',
|
|
7
|
+
'Frank Wilson', 'Grace Lee', 'Hank Taylor', 'Ivy Martin', 'Jack Anderson',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const ACTIONS = [
|
|
11
|
+
'created a new order', 'updated their profile', 'submitted a report',
|
|
12
|
+
'closed a ticket', 'uploaded a document', 'left a comment',
|
|
13
|
+
'approved a request', 'joined the team', 'completed a task', 'shared a file',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function randomItem<T>(arr: T[]): T {
|
|
17
|
+
return arr[Math.floor(Math.random() * arr.length)]!;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let activitySeq = 0;
|
|
21
|
+
|
|
22
|
+
function generateActivity(): ActivityItem {
|
|
23
|
+
const name = randomItem(NAMES);
|
|
24
|
+
return {
|
|
25
|
+
id: `act-${++activitySeq}`,
|
|
26
|
+
text: `${name} ${randomItem(ACTIONS)}`,
|
|
27
|
+
userName: name,
|
|
28
|
+
userAvatar: `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(name)}`,
|
|
29
|
+
userOnline: Math.random() > 0.3,
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchInitialActivity(signal?: AbortSignal): Promise<ActivityItem[]> {
|
|
35
|
+
const items: ActivityItem[] = [];
|
|
36
|
+
for (let i = 0; i < 50; i++) {
|
|
37
|
+
items.push(generateActivity());
|
|
38
|
+
}
|
|
39
|
+
return mockFetch(items, 600, signal);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getActivityWSConfig(): MockWSConfig {
|
|
43
|
+
return {
|
|
44
|
+
minInterval: 1000,
|
|
45
|
+
maxInterval: 3000,
|
|
46
|
+
generator: generateActivity,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MockWSConfig } from './MockWebSocket';
|
|
2
|
+
|
|
3
|
+
interface GeneratorConfig {
|
|
4
|
+
baseline: number;
|
|
5
|
+
variance: number;
|
|
6
|
+
min?: number;
|
|
7
|
+
minInterval?: number;
|
|
8
|
+
maxInterval?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const configs: Record<string, GeneratorConfig> = {
|
|
12
|
+
orders: { baseline: 150, variance: 10 },
|
|
13
|
+
revenue: { baseline: 52000, variance: 500 },
|
|
14
|
+
'active-users': { baseline: 1200, variance: 50 },
|
|
15
|
+
errors: { baseline: 3, variance: 2, min: 0 },
|
|
16
|
+
latency: { baseline: 145, variance: 30, min: 1 },
|
|
17
|
+
traffic: { baseline: 8500, variance: 500 },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let counters: Record<string, number> = {};
|
|
21
|
+
|
|
22
|
+
function randomWalk(serviceId: string, cfg: GeneratorConfig): number {
|
|
23
|
+
if (!(serviceId in counters)) {
|
|
24
|
+
counters[serviceId] = cfg.baseline;
|
|
25
|
+
}
|
|
26
|
+
const delta = (Math.random() - 0.5) * 2 * cfg.variance;
|
|
27
|
+
counters[serviceId] += delta;
|
|
28
|
+
if (cfg.min !== undefined && counters[serviceId] < cfg.min) {
|
|
29
|
+
counters[serviceId] = cfg.min;
|
|
30
|
+
}
|
|
31
|
+
return Math.round(counters[serviceId]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getDashboardConfig(serviceId: string): MockWSConfig {
|
|
35
|
+
const cfg = configs[serviceId] ?? { baseline: 100, variance: 10 };
|
|
36
|
+
return {
|
|
37
|
+
minInterval: cfg.minInterval ?? 400,
|
|
38
|
+
maxInterval: cfg.maxInterval ?? 1500,
|
|
39
|
+
generator: () => ({
|
|
40
|
+
id: `${serviceId}-${Date.now()}`,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
count: randomWalk(serviceId, cfg),
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
if (signal?.aborted) {
|
|
4
|
+
reject(signal.reason);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const timer = setTimeout(resolve, ms);
|
|
8
|
+
signal?.addEventListener('abort', () => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
reject(signal.reason);
|
|
11
|
+
}, { once: true });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function mockFetch<T>(data: T, ms = 300, signal?: AbortSignal): Promise<T> {
|
|
16
|
+
await delay(ms, signal);
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { SocialPost } from '../types/social';
|
|
2
|
+
import { mockFetch } from './delay';
|
|
3
|
+
|
|
4
|
+
const AUTHORS = [
|
|
5
|
+
'Sarah Chen', 'Mike Rodriguez', 'Emily Park', 'Chris Taylor', 'Jordan Lee',
|
|
6
|
+
'Sam Williams', 'Alex Murphy', 'Kim Nguyen', 'Pat O\'Brien', 'Robin Cruz',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const CONTENTS = [
|
|
10
|
+
'Just shipped a new feature! The team crushed it this sprint.',
|
|
11
|
+
'Anyone else excited about the new TypeScript release?',
|
|
12
|
+
'Great architecture review today. Clean patterns make all the difference.',
|
|
13
|
+
'Coffee and code — name a better duo.',
|
|
14
|
+
'TIL: Object.freeze is shallow. Always has been.',
|
|
15
|
+
'Pair programming session was incredibly productive today.',
|
|
16
|
+
'Hot take: tests are documentation.',
|
|
17
|
+
'Finally figured out that race condition. AbortController saves the day!',
|
|
18
|
+
'Reading through the codebase and loving the consistency.',
|
|
19
|
+
'Deployed to production with zero downtime. Feels good.',
|
|
20
|
+
'State management doesn\'t have to be complicated.',
|
|
21
|
+
'Code review feedback is a gift, not a critique.',
|
|
22
|
+
'The best code is the code you don\'t have to write.',
|
|
23
|
+
'Refactoring legacy code is oddly satisfying.',
|
|
24
|
+
'Remember: premature optimization is the root of all evil.',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
let postSeq = 0;
|
|
28
|
+
const TOTAL_POSTS = 120;
|
|
29
|
+
|
|
30
|
+
export async function fetchSocialFeed(
|
|
31
|
+
page: number,
|
|
32
|
+
pageSize: number,
|
|
33
|
+
signal?: AbortSignal,
|
|
34
|
+
): Promise<{ items: SocialPost[]; hasMore: boolean }> {
|
|
35
|
+
const start = page * pageSize;
|
|
36
|
+
if (start >= TOTAL_POSTS) {
|
|
37
|
+
return mockFetch({ items: [], hasMore: false }, 200, signal);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const count = Math.min(pageSize, TOTAL_POSTS - start);
|
|
41
|
+
const items: SocialPost[] = [];
|
|
42
|
+
for (let i = 0; i < count; i++) {
|
|
43
|
+
const author = AUTHORS[Math.floor(Math.random() * AUTHORS.length)]!;
|
|
44
|
+
items.push({
|
|
45
|
+
id: `post-${++postSeq}`,
|
|
46
|
+
content: CONTENTS[Math.floor(Math.random() * CONTENTS.length)]!,
|
|
47
|
+
author,
|
|
48
|
+
avatarUrl: `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(author)}`,
|
|
49
|
+
timestamp: new Date(Date.now() - Math.random() * 86400000).toISOString(),
|
|
50
|
+
likes: Math.floor(Math.random() * 50),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return mockFetch({ items, hasMore: start + count < TOTAL_POSTS }, 500, signal);
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Resource } from 'mvc-kit';
|
|
2
|
+
import type { ActivityItem } from '../types/activity';
|
|
3
|
+
import { fetchInitialActivity } from '../mock-remote/activity-api';
|
|
4
|
+
|
|
5
|
+
export class ActivityResource extends Resource<ActivityItem> {
|
|
6
|
+
static override MAX_SIZE = 100;
|
|
7
|
+
|
|
8
|
+
async loadInitial(): Promise<void> {
|
|
9
|
+
const items = await fetchInitialActivity(this.disposeSignal);
|
|
10
|
+
this.reset(items);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Resource } from 'mvc-kit';
|
|
2
|
+
import type { SocialPost } from '../types/social';
|
|
3
|
+
import { fetchSocialFeed } from '../mock-remote/social-api';
|
|
4
|
+
|
|
5
|
+
export class SocialFeedResource extends Resource<SocialPost> {
|
|
6
|
+
private _hasMore = true;
|
|
7
|
+
|
|
8
|
+
get hasMore(): boolean {
|
|
9
|
+
return this._hasMore;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async loadPage(page: number): Promise<void> {
|
|
13
|
+
const { items, hasMore } = await fetchSocialFeed(page, 20, this.disposeSignal);
|
|
14
|
+
this._hasMore = hasMore;
|
|
15
|
+
this.upsert(...items);
|
|
16
|
+
}
|
|
17
|
+
}
|