@bettertogether/community-engine-vue 0.1.6 → 0.2.2

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.
Files changed (229) hide show
  1. package/README.md +140 -1
  2. package/dist/assets/BBadge.vue_vue_type_script_setup_true_lang-IIZ8QpjG-Z9WDKHqT.js +1 -0
  3. package/dist/assets/BCardText.vue_vue_type_script_setup_true_lang-Be6CD36N-B5JCTdmm.js +3 -0
  4. package/dist/assets/BFormSelect.vue_vue_type_script_setup_true_lang-BigptVap-B_HbOOZR.js +1 -0
  5. package/dist/assets/BRow.vue_vue_type_script_setup_true_lang-69TY75-8-DJdEdyx7.js +1 -0
  6. package/dist/assets/Communities-Cx4tT-bx.js +1 -0
  7. package/dist/assets/Communities-n33ssuUH.css +1 -0
  8. package/dist/assets/CommunityConversation-bBkYBs2k.css +1 -0
  9. package/dist/assets/CommunityConversation-jHAnv_Ps.js +1 -0
  10. package/dist/assets/CommunityConversations-rEDGS7To.js +1 -0
  11. package/dist/assets/CommunityEvent-CUdT0aT4.js +1 -0
  12. package/dist/assets/CommunityEvents-rsOgcxQr.js +1 -0
  13. package/dist/assets/CommunityHome-ChuTE2Nz.js +1 -0
  14. package/dist/assets/CommunityJoaTu-CpLIY_83.js +1 -0
  15. package/dist/assets/CommunityMembers-C3UtzQGp.css +1 -0
  16. package/dist/assets/CommunityMembers-DKf74ltl.js +1 -0
  17. package/dist/assets/CommunityPage-C5x23iQl.css +1 -0
  18. package/dist/assets/CommunityPage-CRYg9-rW.js +1 -0
  19. package/dist/assets/CommunityPages-IsLTNFC3.js +1 -0
  20. package/dist/assets/CommunityPost-BOnqqxVs.js +1 -0
  21. package/dist/assets/CommunityPost-BRYtkDSY.css +1 -0
  22. package/dist/assets/CommunityPosts-DY1olmcU.js +1 -0
  23. package/dist/assets/Error404-D10VQARe.js +1 -0
  24. package/dist/assets/EventCard-vfutXTdg.js +1 -0
  25. package/dist/assets/EventList-ChtehYcJ.js +1 -0
  26. package/dist/assets/ExtensionSlot-DJKbrq4c.js +1 -0
  27. package/dist/assets/PostList-BuHrBBqX.css +1 -0
  28. package/dist/assets/PostList-DYFgxlE8.js +1 -0
  29. package/dist/assets/SyncBadge-B1JBsdUk.js +1 -0
  30. package/dist/assets/SyncBadge-FNO-QLuu.css +1 -0
  31. package/dist/assets/UserPasswordNew-D_Djldm9.css +1 -0
  32. package/dist/assets/UserPasswordNew-al9bNBTZ.js +1 -0
  33. package/dist/assets/UserPasswordReset-42zs98RW.js +1 -0
  34. package/dist/assets/UserPasswordReset-D_6OQDZY.css +1 -0
  35. package/dist/assets/UserResendConfirmation-CGavYB81.js +1 -0
  36. package/dist/assets/UserResendConfirmation-DNTHcaar.css +1 -0
  37. package/dist/assets/UserSignIn-BIRb0HkV.js +1 -0
  38. package/dist/assets/UserSignIn-C-Pol8OD.css +1 -0
  39. package/dist/assets/UserSignUp-ChkKQAd2.css +1 -0
  40. package/dist/assets/UserSignUp-Df6o3vlO.js +1 -0
  41. package/dist/assets/better-together-logo-61cxo5d5.png +0 -0
  42. package/dist/assets/index-BFt-JKVh.css +5 -0
  43. package/dist/assets/index-COo3Jb7v.js +1088 -0
  44. package/dist/assets/nodefs-Bfyh92qg.js +1 -0
  45. package/dist/assets/opfs-ahp-BLzlXf6u.js +3 -0
  46. package/dist/assets/pglite-BdRI_ZYT.wasm +0 -0
  47. package/dist/assets/pglite-COscPi1Y.data +0 -0
  48. package/dist/assets/usePages-DDjDQRCy.js +1 -0
  49. package/dist/assets/usePosts-Bf2Ccwr4.js +1 -0
  50. package/{public → dist}/index.html +9 -19
  51. package/package.json +57 -45
  52. package/src/BtApp.vue +34 -43
  53. package/src/components/BtBrandingLogo.vue +10 -18
  54. package/src/components/BtHeader.vue +34 -84
  55. package/src/components/BtMainContent.vue +12 -43
  56. package/src/components/BtNavBar.vue +25 -38
  57. package/src/components/BtNavItem.vue +25 -58
  58. package/src/components/BtNavUser.vue +65 -86
  59. package/src/components/BtProfileForm.vue +48 -39
  60. package/src/components/BtUserNewPasswordForm.vue +52 -74
  61. package/src/components/BtUserResendConfirmationForm.vue +45 -83
  62. package/src/components/BtUserResetPasswordForm.vue +45 -77
  63. package/src/components/BtUserSignInForm.vue +59 -75
  64. package/src/components/BtUserSignUpForm.vue +90 -103
  65. package/src/components/CommunityForm.vue +47 -39
  66. package/src/components/community/CommunityCard.vue +113 -0
  67. package/src/components/community/CommunityHeader.vue +91 -0
  68. package/src/components/community/CommunityList.vue +59 -0
  69. package/src/components/community/MemberRoleRow.vue +107 -0
  70. package/src/components/conversation/ConversationCard.vue +49 -0
  71. package/src/components/conversation/ConversationDetail.vue +53 -0
  72. package/src/components/conversation/ConversationList.vue +51 -0
  73. package/src/components/conversation/MessageForm.vue +45 -0
  74. package/src/components/conversation/MessageItem.vue +43 -0
  75. package/src/components/conversation/MessageList.vue +39 -0
  76. package/src/components/event/EventCard.vue +82 -0
  77. package/src/components/event/EventForm.vue +99 -0
  78. package/src/components/event/EventList.vue +47 -0
  79. package/src/components/invitation/InvitationCard.vue +56 -0
  80. package/src/components/invitation/InvitationForm.vue +70 -0
  81. package/src/components/invitation/InvitationList.vue +51 -0
  82. package/src/components/joatu/AgreementCard.vue +57 -0
  83. package/src/components/joatu/AgreementList.vue +51 -0
  84. package/src/components/joatu/OfferCard.vue +65 -0
  85. package/src/components/joatu/OfferForm.vue +82 -0
  86. package/src/components/joatu/OfferList.vue +51 -0
  87. package/src/components/joatu/RequestCard.vue +65 -0
  88. package/src/components/joatu/RequestForm.vue +82 -0
  89. package/src/components/joatu/RequestList.vue +51 -0
  90. package/src/components/page/PageCard.vue +55 -0
  91. package/src/components/page/PageDetail.vue +35 -0
  92. package/src/components/page/PageList.vue +51 -0
  93. package/src/components/person/MemberList.vue +61 -0
  94. package/src/components/person/PersonAvatar.vue +54 -0
  95. package/src/components/person/PersonCard.vue +47 -0
  96. package/src/components/post/PostCard.vue +105 -0
  97. package/src/components/post/PostDetail.vue +98 -0
  98. package/src/components/post/PostForm.vue +84 -0
  99. package/src/components/post/PostList.vue +53 -0
  100. package/src/components/role/BlockButton.vue +44 -0
  101. package/src/components/role/RoleBadge.vue +19 -0
  102. package/src/components/role/RoleGate.vue +62 -0
  103. package/src/components/role/RoleSelector.vue +29 -0
  104. package/src/components/shared/ExtensionSlot.vue +27 -0
  105. package/src/components/sync/OfflineBanner.vue +49 -0
  106. package/src/components/sync/SyncBadge.vue +108 -0
  107. package/src/components/sync/SyncStatusBar.vue +121 -0
  108. package/src/composables/useCommunities.js +19 -0
  109. package/src/composables/useConversations.js +5 -0
  110. package/src/composables/useEvents.js +28 -0
  111. package/src/composables/useInvitations.js +10 -0
  112. package/src/composables/useJoaTuAgreements.js +11 -0
  113. package/src/composables/useJoaTuOffers.js +10 -0
  114. package/src/composables/useJoaTuRequests.js +10 -0
  115. package/src/composables/useMembers.js +30 -0
  116. package/src/composables/useMessages.js +5 -0
  117. package/src/composables/useNotifications.js +5 -0
  118. package/src/composables/usePages.js +6 -0
  119. package/src/composables/usePersonBlocks.js +65 -0
  120. package/src/composables/usePosts.js +27 -0
  121. package/src/composables/useResource.js +137 -0
  122. package/src/composables/useRoles.js +94 -0
  123. package/src/composables/useSyncStatus.js +22 -0
  124. package/src/composables/useToaster.js +20 -0
  125. package/src/context.js +18 -0
  126. package/src/db/client.js +343 -0
  127. package/src/db/migrations/001_initial.sql +131 -0
  128. package/src/db/migrations/003_conversations_invitations_pages_joatu.sql +76 -0
  129. package/src/db/sync.js +276 -0
  130. package/src/endpoints/BtApiAuth.js +1 -1
  131. package/src/endpoints/BtApiV1.js +1 -1
  132. package/src/extension.js +45 -0
  133. package/src/i18n/index.js +103 -0
  134. package/src/i18n/locales/en.json +275 -0
  135. package/src/i18n/locales/es.json +275 -0
  136. package/src/i18n/locales/fr.json +223 -0
  137. package/src/i18n/locales/uk.json +275 -0
  138. package/src/index.js +168 -22
  139. package/src/layouts/CommunityLayout.vue +89 -0
  140. package/src/main.js +16 -12
  141. package/src/mixins/error-handling.js +6 -15
  142. package/src/mixins/toaster.js +15 -10
  143. package/src/pages/Communities.vue +59 -0
  144. package/src/pages/Error404.vue +10 -14
  145. package/src/pages/Home.vue +11 -18
  146. package/src/pages/Me.vue +39 -59
  147. package/src/pages/UserPasswordNew.vue +12 -68
  148. package/src/pages/UserPasswordReset.vue +15 -64
  149. package/src/pages/UserResendConfirmation.vue +39 -113
  150. package/src/pages/UserSignIn.vue +18 -67
  151. package/src/pages/UserSignUp.vue +15 -64
  152. package/src/pages/community/CommunityConversation.vue +31 -0
  153. package/src/pages/community/CommunityConversations.vue +18 -0
  154. package/src/pages/community/CommunityEvent.vue +39 -0
  155. package/src/pages/community/CommunityEvents.vue +58 -0
  156. package/src/pages/community/CommunityHome.vue +49 -0
  157. package/src/pages/community/CommunityJoaTu.vue +115 -0
  158. package/src/pages/community/CommunityMembers.vue +23 -0
  159. package/src/pages/community/CommunityPage.vue +31 -0
  160. package/src/pages/community/CommunityPages.vue +25 -0
  161. package/src/pages/community/CommunityPost.vue +28 -0
  162. package/src/pages/community/CommunityPosts.vue +58 -0
  163. package/src/pages/community/CommunitySettingsPage.vue +117 -0
  164. package/src/pages/community/RoleManagerPage.vue +93 -0
  165. package/src/plugins/bootstrap-vue.js +5 -5
  166. package/src/plugins/font-awesome.js +3 -2
  167. package/src/plugins/index.js +9 -4
  168. package/src/plugins/progress.js +16 -0
  169. package/src/pwa/index.js +156 -0
  170. package/src/pwa/sw-helpers.js +130 -0
  171. package/src/router/communityRoutes.js +78 -0
  172. package/src/router/index.js +30 -144
  173. package/src/slot-registry.js +15 -0
  174. package/src/stores/auth.js +134 -0
  175. package/src/stores/communities.js +59 -0
  176. package/src/stores/index.js +5 -0
  177. package/src/stores/menus.js +14 -0
  178. package/src/stores/people.js +48 -0
  179. package/src/stores/sync.js +93 -0
  180. package/src/stylesheets/sync-indicators.scss +34 -0
  181. package/.env.sample +0 -1
  182. package/.eslintrc.js +0 -51
  183. package/.gitlab-ci.yml +0 -14
  184. package/.travis/.rbenv-gemsets +0 -1
  185. package/.travis/.ruby-version +0 -1
  186. package/.travis.yml +0 -31
  187. package/babel.config.js +0 -5
  188. package/cypress.json +0 -3
  189. package/deploy/build.sh +0 -8
  190. package/eslint.config.js +0 -16
  191. package/postcss.config.js +0 -5
  192. package/src/eslint.config.js +0 -16
  193. package/src/forms/BtProfileFormSchema.js +0 -19
  194. package/src/forms/BtUserConfirmationFormSchema.js +0 -20
  195. package/src/forms/BtUserNewPasswordFormSchema.js +0 -29
  196. package/src/forms/BtUserResetPasswordFormSchema.js +0 -20
  197. package/src/forms/BtUserSignInFormSchema.js +0 -29
  198. package/src/forms/BtUserSignUpFormSchema.js +0 -63
  199. package/src/forms/CommunityFormSchema.js +0 -19
  200. package/src/plugins/vue-form-generator.js +0 -4
  201. package/src/plugins/vue-loading.js +0 -10
  202. package/src/registerServiceWorker.js +0 -32
  203. package/src/store/index.js +0 -32
  204. package/src/store/modules/authentication.js +0 -170
  205. package/src/store/modules/communities.js +0 -98
  206. package/src/store/modules/community-engine.js +0 -14
  207. package/src/store/modules/menus.js +0 -52
  208. package/src/store/modules/people.js +0 -88
  209. package/tests/e2e/.eslintrc.js +0 -12
  210. package/tests/e2e/plugins/index.js +0 -26
  211. package/tests/e2e/specs/home.js +0 -8
  212. package/tests/e2e/support/commands.js +0 -25
  213. package/tests/e2e/support/index.js +0 -20
  214. package/tests/unit/.eslintrc.js +0 -5
  215. package/tests/unit/example.spec.js +0 -13
  216. package/vue.config.js +0 -11
  217. package/webpack.config.js +0 -28
  218. /package/{public → dist}/_redirects +0 -0
  219. /package/{public → dist}/favicon.ico +0 -0
  220. /package/{public → dist}/img/favicon.ico +0 -0
  221. /package/{public → dist}/img/icons/android-chrome-192x192.png +0 -0
  222. /package/{public → dist}/img/icons/android-chrome-384x384.png +0 -0
  223. /package/{public → dist}/img/icons/apple-touch-icon.png +0 -0
  224. /package/{public → dist}/img/icons/favicon-16x16.png +0 -0
  225. /package/{public → dist}/img/icons/favicon-32x32.png +0 -0
  226. /package/{public → dist}/img/icons/favicon.ico +0 -0
  227. /package/{public → dist}/img/icons/mstile-150x150.png +0 -0
  228. /package/{public → dist}/img/icons/safari-pinned-tab.svg +0 -0
  229. /package/{public → dist}/robots.txt +0 -0
package/src/db/sync.js ADDED
@@ -0,0 +1,276 @@
1
+ // Electric SQL shape sync — connects PGlite to a CE Rails Electric sidecar.
2
+ // Graceful no-op when VITE_ELECTRIC_URL is not configured.
3
+ //
4
+ // Electric HTTP API: GET /v1/shape?table=<table>&offset=<offset>
5
+ // Responses are SSE streams: each line is a JSON object { headers, value, offset }
6
+ // Special message: { headers: { control: 'up-to-date' } } means initial sync complete
7
+
8
+ const ELECTRIC_URL = import.meta.env?.VITE_ELECTRIC_URL
9
+
10
+ // Tables to sync — ordered by dependency (communities before memberships, etc.)
11
+ const SYNC_TABLES = [
12
+ 'communities',
13
+ 'people',
14
+ 'posts',
15
+ 'events',
16
+ 'conversations',
17
+ 'messages',
18
+ 'notifications',
19
+ 'navigation_areas',
20
+ 'navigation_items',
21
+ 'invitations',
22
+ 'pages',
23
+ ]
24
+
25
+ const OFFSET_KEY = 'cev_sync_offsets'
26
+ const RETRY_DELAY_MS = 30000
27
+
28
+ // Active AbortControllers keyed by table name
29
+ const _controllers = {}
30
+
31
+ function loadOffsets() {
32
+ try {
33
+ return JSON.parse(localStorage.getItem(OFFSET_KEY) || '{}')
34
+ } catch {
35
+ return {}
36
+ }
37
+ }
38
+
39
+ function saveOffsets(offsets) {
40
+ try {
41
+ localStorage.setItem(OFFSET_KEY, JSON.stringify(offsets))
42
+ } catch { /* storage full / private mode */ }
43
+ }
44
+
45
+ /**
46
+ * Build a parameterised upsert or delete SQL for a shape message row.
47
+ * Returns null for control messages.
48
+ */
49
+ function buildStatement(table, headers, value) {
50
+ const op = headers?.operation
51
+
52
+ if (op === 'insert' || op === 'update') {
53
+ const cols = Object.keys(value).filter(k => !['_sync_status', '_server_at'].includes(k))
54
+ // Preserve local-first: skip overwrite when row is locally modified
55
+ const colList = cols.join(', ')
56
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
57
+ const updates = cols.map((c, i) => `${c} = $${i + 1}`).join(', ')
58
+ const vals = cols.map(c => value[c])
59
+
60
+ const sql = `
61
+ INSERT INTO ${table} (${colList}, _sync_status, _server_at)
62
+ VALUES (${placeholders}, 'synced', strftime('%Y-%m-%dT%H:%M:%fZ','now'))
63
+ ON CONFLICT (id) DO UPDATE SET
64
+ ${updates},
65
+ _sync_status = CASE WHEN ${table}._sync_status = 'local' THEN 'local' ELSE 'synced' END,
66
+ _server_at = CASE WHEN ${table}._sync_status = 'local' THEN ${table}._server_at ELSE strftime('%Y-%m-%dT%H:%M:%fZ','now') END
67
+ `
68
+ return { sql, params: vals }
69
+ }
70
+
71
+ if (op === 'delete') {
72
+ return {
73
+ sql: `DELETE FROM ${table} WHERE id = $1 AND _sync_status != 'local'`,
74
+ params: [value.id],
75
+ }
76
+ }
77
+
78
+ return null
79
+ }
80
+
81
+ /**
82
+ * Apply a batch of shape messages to PGlite.
83
+ */
84
+ export async function applyShapeMessages(db, table, messages) {
85
+ for (const msg of messages) {
86
+ const { headers, value } = msg
87
+ if (!value || !headers?.operation) continue
88
+ const stmt = buildStatement(table, headers, value)
89
+ if (!stmt) continue
90
+ try {
91
+ await db.query(stmt.sql, stmt.params)
92
+ } catch (err) {
93
+ // eslint-disable-next-line no-console
94
+ console.warn(`[cev:sync] Failed to apply ${headers.operation} to ${table}:`, err)
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Start a shape subscription for a single table.
101
+ * Streams SSE data from GET /v1/shape, parses line-by-line JSON.
102
+ * Returns a cleanup function.
103
+ */
104
+ function subscribeToShape(db, table, initialOffset, onUpToDate) {
105
+ const controller = new AbortController()
106
+ _controllers[table] = controller
107
+
108
+ async function stream(offset) {
109
+ const url = `${ELECTRIC_URL}/v1/shape?table=${table}&offset=${offset}`
110
+ const response = await fetch(url, { signal: controller.signal })
111
+
112
+ if (!response.ok) {
113
+ throw new Error(`HTTP ${response.status} for shape ${table}`)
114
+ }
115
+
116
+ const reader = response.body.getReader()
117
+ const decoder = new TextDecoder()
118
+ let buffer = ''
119
+ const offsets = loadOffsets()
120
+ let currentOffset = offset
121
+ const batch = []
122
+
123
+ // eslint-disable-next-line no-constant-condition
124
+ while (true) {
125
+ const { done, value } = await reader.read()
126
+ if (done) break
127
+
128
+ buffer += decoder.decode(value, { stream: true })
129
+ const lines = buffer.split('\n')
130
+ buffer = lines.pop() // keep incomplete last line
131
+
132
+ for (const line of lines) {
133
+ const trimmed = line.trim()
134
+ if (!trimmed) continue
135
+ let msg
136
+ try {
137
+ msg = JSON.parse(trimmed)
138
+ } catch {
139
+ continue
140
+ }
141
+
142
+ if (msg.offset != null) {
143
+ currentOffset = msg.offset
144
+ }
145
+
146
+ if (msg.headers?.control === 'up-to-date') {
147
+ // Flush any accumulated batch before signalling up-to-date
148
+ if (batch.length) {
149
+ await applyShapeMessages(db, table, batch.splice(0))
150
+ }
151
+ offsets[table] = currentOffset
152
+ saveOffsets(offsets)
153
+ onUpToDate(table)
154
+ continue
155
+ }
156
+
157
+ if (msg.headers?.operation) {
158
+ batch.push(msg)
159
+ }
160
+ }
161
+
162
+ // Flush batch periodically to avoid unbounded accumulation
163
+ if (batch.length >= 50) {
164
+ await applyShapeMessages(db, table, batch.splice(0))
165
+ offsets[table] = currentOffset
166
+ saveOffsets(offsets)
167
+ }
168
+ }
169
+
170
+ // Flush remaining
171
+ if (batch.length) {
172
+ await applyShapeMessages(db, table, batch.splice(0))
173
+ }
174
+ offsets[table] = currentOffset
175
+ saveOffsets(offsets)
176
+
177
+ // Stream ended (server closed) — reconnect with latest offset
178
+ if (!controller.signal.aborted) {
179
+ await stream(currentOffset)
180
+ }
181
+ }
182
+
183
+ stream(initialOffset).catch(err => {
184
+ if (controller.signal.aborted) return
185
+ // eslint-disable-next-line no-console
186
+ console.warn(`[cev:sync] Shape stream error for ${table}:`, err)
187
+ throw err
188
+ })
189
+
190
+ return () => controller.abort()
191
+ }
192
+
193
+ let _retryTimer = null
194
+ const _cleanups = []
195
+
196
+ /**
197
+ * Start syncing all tables.
198
+ * No-op when VITE_ELECTRIC_URL is not set.
199
+ * Resumes from saved localStorage offsets.
200
+ * Retries on failure every 30s.
201
+ */
202
+ export function startSync(db) {
203
+ if (!ELECTRIC_URL) return
204
+
205
+ // Lazy import to avoid Pinia init order issues
206
+ import('../stores/sync').then(({ useSyncStore }) => {
207
+ _doStart(db, useSyncStore)
208
+ }).catch(() => {
209
+ _doStart(db, null)
210
+ })
211
+ }
212
+
213
+ function _doStart(db, useSyncStore) {
214
+ const syncStore = useSyncStore ? useSyncStore() : null
215
+ const offsets = loadOffsets()
216
+ let upToDateCount = 0
217
+
218
+ function onUpToDate(_table) {
219
+ upToDateCount++
220
+ if (upToDateCount >= SYNC_TABLES.length) {
221
+ // eslint-disable-next-line no-console
222
+ console.info('[cev:sync] Initial sync complete — all tables up to date')
223
+ }
224
+ if (syncStore) syncStore.setConnected(true)
225
+ }
226
+
227
+ function startAll() {
228
+ for (const table of SYNC_TABLES) {
229
+ const offset = offsets[table] ?? '-1'
230
+ try {
231
+ const cleanup = subscribeToShape(db, table, offset, onUpToDate)
232
+ _cleanups.push(cleanup)
233
+ } catch (err) {
234
+ // eslint-disable-next-line no-console
235
+ console.warn(`[cev:sync] Could not subscribe to ${table}:`, err)
236
+ scheduleRetry()
237
+ return
238
+ }
239
+ }
240
+ }
241
+
242
+ // Wrap the whole start in a promise to catch initial fetch errors
243
+ Promise.resolve().then(startAll).catch(err => {
244
+ // eslint-disable-next-line no-console
245
+ console.warn('[cev:sync] Startup error:', err)
246
+ if (syncStore) syncStore.setConnected(false)
247
+ scheduleRetry()
248
+ })
249
+
250
+ function scheduleRetry() {
251
+ if (_retryTimer) return
252
+ _retryTimer = setTimeout(() => {
253
+ _retryTimer = null
254
+ // eslint-disable-next-line no-console
255
+ console.info('[cev:sync] Retrying Electric connection…')
256
+ _doStart(db, useSyncStore)
257
+ }, RETRY_DELAY_MS)
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Stop all active shape subscriptions.
263
+ */
264
+ export function stopSync() {
265
+ if (_retryTimer) {
266
+ clearTimeout(_retryTimer)
267
+ _retryTimer = null
268
+ }
269
+ for (const cleanup of _cleanups.splice(0)) {
270
+ try { cleanup() } catch { /* ignore */ }
271
+ }
272
+ Object.keys(_controllers).forEach(k => {
273
+ try { _controllers[k].abort() } catch { /* ignore */ }
274
+ delete _controllers[k]
275
+ })
276
+ }
@@ -1,7 +1,7 @@
1
1
  import axios from 'axios'
2
2
 
3
3
  const BtApiAuth = axios.create({
4
- baseURL: `${process.env.VUE_APP_BETTER_TOGETHER_API_URI}/bt/api/auth`,
4
+ baseURL: `${import.meta.env.VITE_BETTER_TOGETHER_API_URI}/api/auth`,
5
5
  headers: {
6
6
  'Content-Type': 'application/vnd.api+json',
7
7
  },
@@ -1,7 +1,7 @@
1
1
  import JsonApi from 'devour-client'
2
2
 
3
3
  const BtApiV1 = new JsonApi({
4
- apiUrl: `${process.env.VUE_APP_BETTER_TOGETHER_API_URI}/bt/api/v1`,
4
+ apiUrl: `${import.meta.env.VITE_BETTER_TOGETHER_API_URI}/api/v1`,
5
5
  })
6
6
 
7
7
  // Define Model
@@ -0,0 +1,45 @@
1
+ import { getCevContext } from './context'
2
+ import { registerSlotInjection } from './slot-registry'
3
+ import { registerExtensionMigration } from './db/client'
4
+
5
+ export function defineExtension(spec) {
6
+ return {
7
+ ...spec,
8
+
9
+ _install() {
10
+ const { app, router } = getCevContext()
11
+
12
+ // Register extension DB migrations (must run before getDb() is awaited elsewhere)
13
+ if (spec.migrations?.length) {
14
+ spec.migrations.forEach(({ version, sql }) => registerExtensionMigration(version, sql))
15
+ }
16
+
17
+ // Register global components
18
+ if (spec.components) {
19
+ Object.entries(spec.components).forEach(([name, component]) => {
20
+ app.component(name, component)
21
+ })
22
+ }
23
+
24
+ // Add platform-level routes
25
+ if (spec.routes?.length) {
26
+ spec.routes.forEach((route) => router.addRoute(route))
27
+ }
28
+
29
+ // Add community child routes (as siblings under CommunityHome)
30
+ if (spec.communityRoutes?.length) {
31
+ spec.communityRoutes.forEach((route) => router.addRoute('CommunityHome', route))
32
+ }
33
+
34
+ // Register slot injections
35
+ if (spec.slotInjections?.length) {
36
+ spec.slotInjections.forEach((injection) => registerSlotInjection(injection))
37
+ }
38
+
39
+ // Run custom setup hook
40
+ if (spec.setup) {
41
+ spec.setup(getCevContext())
42
+ }
43
+ },
44
+ }
45
+ }
@@ -0,0 +1,103 @@
1
+ import { createI18n } from 'vue-i18n'
2
+ import en from './locales/en.json'
3
+ import fr from './locales/fr.json'
4
+ import es from './locales/es.json'
5
+ import uk from './locales/uk.json'
6
+
7
+ // Extension messages registered by companion packages before install()
8
+ const _extensionMessages = []
9
+
10
+ /**
11
+ * Register additional locale messages from a companion package.
12
+ * Must be called before CommunityEngineVue is installed.
13
+ *
14
+ * @param {Object} messages - Locale messages object, e.g. { en: { commerce: {...} }, fr: {...} }
15
+ */
16
+ export function registerExtensionMessages(messages) {
17
+ _extensionMessages.push(messages)
18
+ }
19
+
20
+ /**
21
+ * Build the merged messages object for a given set of option messages.
22
+ * Merges CEV base (bt.*), extension messages, and host-app overrides.
23
+ */
24
+ function buildMessages(optionMessages = {}) {
25
+ const merged = {
26
+ en: { bt: { ...en.bt } },
27
+ fr: { bt: { ...fr.bt } },
28
+ es: { bt: { ...es.bt } },
29
+ uk: { bt: { ...uk.bt } },
30
+ }
31
+
32
+ // Merge companion package extension messages
33
+ for (const ext of _extensionMessages) {
34
+ for (const [locale, msgs] of Object.entries(ext)) {
35
+ if (!merged[locale]) merged[locale] = {}
36
+ Object.assign(merged[locale], msgs)
37
+ }
38
+ }
39
+
40
+ // Merge host-app-provided message overrides (can override bt.* or add new locales)
41
+ for (const [locale, msgs] of Object.entries(optionMessages)) {
42
+ if (!merged[locale]) merged[locale] = {}
43
+ // Deep merge bt.* — allows partial overrides of individual bt keys
44
+ if (msgs.bt) {
45
+ merged[locale].bt = { ...(merged[locale].bt ?? {}), ...msgs.bt }
46
+ }
47
+ // Merge everything else at top level
48
+ // eslint-disable-next-line no-unused-vars
49
+ const { bt: _, ...rest } = msgs
50
+ Object.assign(merged[locale], rest)
51
+ }
52
+
53
+ return merged
54
+ }
55
+
56
+ /**
57
+ * Create a standalone vue-i18n instance with CEV messages.
58
+ * Used when the host app does not have its own vue-i18n instance.
59
+ */
60
+ export function createCevI18n(optionMessages = {}, locale = 'en') {
61
+ return createI18n({
62
+ legacy: false, // Composition API mode
63
+ locale,
64
+ fallbackLocale: 'en',
65
+ globalInjection: true,
66
+ messages: buildMessages(optionMessages),
67
+ // Silence missing key warnings in production — keys fall back to en
68
+ missingWarn: import.meta.env.DEV,
69
+ fallbackWarn: import.meta.env.DEV,
70
+ })
71
+ }
72
+
73
+ /**
74
+ * Install i18n into the Vue app.
75
+ *
76
+ * Strategy:
77
+ * - If the host app already installed vue-i18n (detectable via $i18n on globalProperties),
78
+ * merge CEV messages into the existing instance instead of installing a new one.
79
+ * - If no i18n instance exists, install a fresh one with CEV messages.
80
+ *
81
+ * @param {import('vue').App} app
82
+ * @param {Object} options - Plugin options (may include options.messages for locale overrides)
83
+ */
84
+ export function installI18n(app, options = {}) {
85
+ const existing = app.config.globalProperties.$i18n
86
+
87
+ if (existing) {
88
+ // Merge CEV bt.* into the host app's existing i18n instance
89
+ const merged = buildMessages(options.messages ?? {})
90
+ for (const [locale, msgs] of Object.entries(merged)) {
91
+ existing.global.mergeLocaleMessage(locale, msgs)
92
+ }
93
+ if (options.locale) {
94
+ existing.global.locale.value = options.locale
95
+ }
96
+ } else {
97
+ const i18n = createCevI18n(options.messages ?? {}, options.locale ?? 'en')
98
+ app.use(i18n)
99
+ if (options.locale) {
100
+ i18n.global.locale.value = options.locale
101
+ }
102
+ }
103
+ }