@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.
- package/README.md +140 -1
- package/dist/assets/BBadge.vue_vue_type_script_setup_true_lang-IIZ8QpjG-Z9WDKHqT.js +1 -0
- package/dist/assets/BCardText.vue_vue_type_script_setup_true_lang-Be6CD36N-B5JCTdmm.js +3 -0
- package/dist/assets/BFormSelect.vue_vue_type_script_setup_true_lang-BigptVap-B_HbOOZR.js +1 -0
- package/dist/assets/BRow.vue_vue_type_script_setup_true_lang-69TY75-8-DJdEdyx7.js +1 -0
- package/dist/assets/Communities-Cx4tT-bx.js +1 -0
- package/dist/assets/Communities-n33ssuUH.css +1 -0
- package/dist/assets/CommunityConversation-bBkYBs2k.css +1 -0
- package/dist/assets/CommunityConversation-jHAnv_Ps.js +1 -0
- package/dist/assets/CommunityConversations-rEDGS7To.js +1 -0
- package/dist/assets/CommunityEvent-CUdT0aT4.js +1 -0
- package/dist/assets/CommunityEvents-rsOgcxQr.js +1 -0
- package/dist/assets/CommunityHome-ChuTE2Nz.js +1 -0
- package/dist/assets/CommunityJoaTu-CpLIY_83.js +1 -0
- package/dist/assets/CommunityMembers-C3UtzQGp.css +1 -0
- package/dist/assets/CommunityMembers-DKf74ltl.js +1 -0
- package/dist/assets/CommunityPage-C5x23iQl.css +1 -0
- package/dist/assets/CommunityPage-CRYg9-rW.js +1 -0
- package/dist/assets/CommunityPages-IsLTNFC3.js +1 -0
- package/dist/assets/CommunityPost-BOnqqxVs.js +1 -0
- package/dist/assets/CommunityPost-BRYtkDSY.css +1 -0
- package/dist/assets/CommunityPosts-DY1olmcU.js +1 -0
- package/dist/assets/Error404-D10VQARe.js +1 -0
- package/dist/assets/EventCard-vfutXTdg.js +1 -0
- package/dist/assets/EventList-ChtehYcJ.js +1 -0
- package/dist/assets/ExtensionSlot-DJKbrq4c.js +1 -0
- package/dist/assets/PostList-BuHrBBqX.css +1 -0
- package/dist/assets/PostList-DYFgxlE8.js +1 -0
- package/dist/assets/SyncBadge-B1JBsdUk.js +1 -0
- package/dist/assets/SyncBadge-FNO-QLuu.css +1 -0
- package/dist/assets/UserPasswordNew-D_Djldm9.css +1 -0
- package/dist/assets/UserPasswordNew-al9bNBTZ.js +1 -0
- package/dist/assets/UserPasswordReset-42zs98RW.js +1 -0
- package/dist/assets/UserPasswordReset-D_6OQDZY.css +1 -0
- package/dist/assets/UserResendConfirmation-CGavYB81.js +1 -0
- package/dist/assets/UserResendConfirmation-DNTHcaar.css +1 -0
- package/dist/assets/UserSignIn-BIRb0HkV.js +1 -0
- package/dist/assets/UserSignIn-C-Pol8OD.css +1 -0
- package/dist/assets/UserSignUp-ChkKQAd2.css +1 -0
- package/dist/assets/UserSignUp-Df6o3vlO.js +1 -0
- package/dist/assets/better-together-logo-61cxo5d5.png +0 -0
- package/dist/assets/index-BFt-JKVh.css +5 -0
- package/dist/assets/index-COo3Jb7v.js +1088 -0
- package/dist/assets/nodefs-Bfyh92qg.js +1 -0
- package/dist/assets/opfs-ahp-BLzlXf6u.js +3 -0
- package/dist/assets/pglite-BdRI_ZYT.wasm +0 -0
- package/dist/assets/pglite-COscPi1Y.data +0 -0
- package/dist/assets/usePages-DDjDQRCy.js +1 -0
- package/dist/assets/usePosts-Bf2Ccwr4.js +1 -0
- package/{public → dist}/index.html +9 -19
- package/package.json +57 -45
- package/src/BtApp.vue +34 -43
- package/src/components/BtBrandingLogo.vue +10 -18
- package/src/components/BtHeader.vue +34 -84
- package/src/components/BtMainContent.vue +12 -43
- package/src/components/BtNavBar.vue +25 -38
- package/src/components/BtNavItem.vue +25 -58
- package/src/components/BtNavUser.vue +65 -86
- package/src/components/BtProfileForm.vue +48 -39
- package/src/components/BtUserNewPasswordForm.vue +52 -74
- package/src/components/BtUserResendConfirmationForm.vue +45 -83
- package/src/components/BtUserResetPasswordForm.vue +45 -77
- package/src/components/BtUserSignInForm.vue +59 -75
- package/src/components/BtUserSignUpForm.vue +90 -103
- package/src/components/CommunityForm.vue +47 -39
- package/src/components/community/CommunityCard.vue +113 -0
- package/src/components/community/CommunityHeader.vue +91 -0
- package/src/components/community/CommunityList.vue +59 -0
- package/src/components/community/MemberRoleRow.vue +107 -0
- package/src/components/conversation/ConversationCard.vue +49 -0
- package/src/components/conversation/ConversationDetail.vue +53 -0
- package/src/components/conversation/ConversationList.vue +51 -0
- package/src/components/conversation/MessageForm.vue +45 -0
- package/src/components/conversation/MessageItem.vue +43 -0
- package/src/components/conversation/MessageList.vue +39 -0
- package/src/components/event/EventCard.vue +82 -0
- package/src/components/event/EventForm.vue +99 -0
- package/src/components/event/EventList.vue +47 -0
- package/src/components/invitation/InvitationCard.vue +56 -0
- package/src/components/invitation/InvitationForm.vue +70 -0
- package/src/components/invitation/InvitationList.vue +51 -0
- package/src/components/joatu/AgreementCard.vue +57 -0
- package/src/components/joatu/AgreementList.vue +51 -0
- package/src/components/joatu/OfferCard.vue +65 -0
- package/src/components/joatu/OfferForm.vue +82 -0
- package/src/components/joatu/OfferList.vue +51 -0
- package/src/components/joatu/RequestCard.vue +65 -0
- package/src/components/joatu/RequestForm.vue +82 -0
- package/src/components/joatu/RequestList.vue +51 -0
- package/src/components/page/PageCard.vue +55 -0
- package/src/components/page/PageDetail.vue +35 -0
- package/src/components/page/PageList.vue +51 -0
- package/src/components/person/MemberList.vue +61 -0
- package/src/components/person/PersonAvatar.vue +54 -0
- package/src/components/person/PersonCard.vue +47 -0
- package/src/components/post/PostCard.vue +105 -0
- package/src/components/post/PostDetail.vue +98 -0
- package/src/components/post/PostForm.vue +84 -0
- package/src/components/post/PostList.vue +53 -0
- package/src/components/role/BlockButton.vue +44 -0
- package/src/components/role/RoleBadge.vue +19 -0
- package/src/components/role/RoleGate.vue +62 -0
- package/src/components/role/RoleSelector.vue +29 -0
- package/src/components/shared/ExtensionSlot.vue +27 -0
- package/src/components/sync/OfflineBanner.vue +49 -0
- package/src/components/sync/SyncBadge.vue +108 -0
- package/src/components/sync/SyncStatusBar.vue +121 -0
- package/src/composables/useCommunities.js +19 -0
- package/src/composables/useConversations.js +5 -0
- package/src/composables/useEvents.js +28 -0
- package/src/composables/useInvitations.js +10 -0
- package/src/composables/useJoaTuAgreements.js +11 -0
- package/src/composables/useJoaTuOffers.js +10 -0
- package/src/composables/useJoaTuRequests.js +10 -0
- package/src/composables/useMembers.js +30 -0
- package/src/composables/useMessages.js +5 -0
- package/src/composables/useNotifications.js +5 -0
- package/src/composables/usePages.js +6 -0
- package/src/composables/usePersonBlocks.js +65 -0
- package/src/composables/usePosts.js +27 -0
- package/src/composables/useResource.js +137 -0
- package/src/composables/useRoles.js +94 -0
- package/src/composables/useSyncStatus.js +22 -0
- package/src/composables/useToaster.js +20 -0
- package/src/context.js +18 -0
- package/src/db/client.js +343 -0
- package/src/db/migrations/001_initial.sql +131 -0
- package/src/db/migrations/003_conversations_invitations_pages_joatu.sql +76 -0
- package/src/db/sync.js +276 -0
- package/src/endpoints/BtApiAuth.js +1 -1
- package/src/endpoints/BtApiV1.js +1 -1
- package/src/extension.js +45 -0
- package/src/i18n/index.js +103 -0
- package/src/i18n/locales/en.json +275 -0
- package/src/i18n/locales/es.json +275 -0
- package/src/i18n/locales/fr.json +223 -0
- package/src/i18n/locales/uk.json +275 -0
- package/src/index.js +168 -22
- package/src/layouts/CommunityLayout.vue +89 -0
- package/src/main.js +16 -12
- package/src/mixins/error-handling.js +6 -15
- package/src/mixins/toaster.js +15 -10
- package/src/pages/Communities.vue +59 -0
- package/src/pages/Error404.vue +10 -14
- package/src/pages/Home.vue +11 -18
- package/src/pages/Me.vue +39 -59
- package/src/pages/UserPasswordNew.vue +12 -68
- package/src/pages/UserPasswordReset.vue +15 -64
- package/src/pages/UserResendConfirmation.vue +39 -113
- package/src/pages/UserSignIn.vue +18 -67
- package/src/pages/UserSignUp.vue +15 -64
- package/src/pages/community/CommunityConversation.vue +31 -0
- package/src/pages/community/CommunityConversations.vue +18 -0
- package/src/pages/community/CommunityEvent.vue +39 -0
- package/src/pages/community/CommunityEvents.vue +58 -0
- package/src/pages/community/CommunityHome.vue +49 -0
- package/src/pages/community/CommunityJoaTu.vue +115 -0
- package/src/pages/community/CommunityMembers.vue +23 -0
- package/src/pages/community/CommunityPage.vue +31 -0
- package/src/pages/community/CommunityPages.vue +25 -0
- package/src/pages/community/CommunityPost.vue +28 -0
- package/src/pages/community/CommunityPosts.vue +58 -0
- package/src/pages/community/CommunitySettingsPage.vue +117 -0
- package/src/pages/community/RoleManagerPage.vue +93 -0
- package/src/plugins/bootstrap-vue.js +5 -5
- package/src/plugins/font-awesome.js +3 -2
- package/src/plugins/index.js +9 -4
- package/src/plugins/progress.js +16 -0
- package/src/pwa/index.js +156 -0
- package/src/pwa/sw-helpers.js +130 -0
- package/src/router/communityRoutes.js +78 -0
- package/src/router/index.js +30 -144
- package/src/slot-registry.js +15 -0
- package/src/stores/auth.js +134 -0
- package/src/stores/communities.js +59 -0
- package/src/stores/index.js +5 -0
- package/src/stores/menus.js +14 -0
- package/src/stores/people.js +48 -0
- package/src/stores/sync.js +93 -0
- package/src/stylesheets/sync-indicators.scss +34 -0
- package/.env.sample +0 -1
- package/.eslintrc.js +0 -51
- package/.gitlab-ci.yml +0 -14
- package/.travis/.rbenv-gemsets +0 -1
- package/.travis/.ruby-version +0 -1
- package/.travis.yml +0 -31
- package/babel.config.js +0 -5
- package/cypress.json +0 -3
- package/deploy/build.sh +0 -8
- package/eslint.config.js +0 -16
- package/postcss.config.js +0 -5
- package/src/eslint.config.js +0 -16
- package/src/forms/BtProfileFormSchema.js +0 -19
- package/src/forms/BtUserConfirmationFormSchema.js +0 -20
- package/src/forms/BtUserNewPasswordFormSchema.js +0 -29
- package/src/forms/BtUserResetPasswordFormSchema.js +0 -20
- package/src/forms/BtUserSignInFormSchema.js +0 -29
- package/src/forms/BtUserSignUpFormSchema.js +0 -63
- package/src/forms/CommunityFormSchema.js +0 -19
- package/src/plugins/vue-form-generator.js +0 -4
- package/src/plugins/vue-loading.js +0 -10
- package/src/registerServiceWorker.js +0 -32
- package/src/store/index.js +0 -32
- package/src/store/modules/authentication.js +0 -170
- package/src/store/modules/communities.js +0 -98
- package/src/store/modules/community-engine.js +0 -14
- package/src/store/modules/menus.js +0 -52
- package/src/store/modules/people.js +0 -88
- package/tests/e2e/.eslintrc.js +0 -12
- package/tests/e2e/plugins/index.js +0 -26
- package/tests/e2e/specs/home.js +0 -8
- package/tests/e2e/support/commands.js +0 -25
- package/tests/e2e/support/index.js +0 -20
- package/tests/unit/.eslintrc.js +0 -5
- package/tests/unit/example.spec.js +0 -13
- package/vue.config.js +0 -11
- package/webpack.config.js +0 -28
- /package/{public → dist}/_redirects +0 -0
- /package/{public → dist}/favicon.ico +0 -0
- /package/{public → dist}/img/favicon.ico +0 -0
- /package/{public → dist}/img/icons/android-chrome-192x192.png +0 -0
- /package/{public → dist}/img/icons/android-chrome-384x384.png +0 -0
- /package/{public → dist}/img/icons/apple-touch-icon.png +0 -0
- /package/{public → dist}/img/icons/favicon-16x16.png +0 -0
- /package/{public → dist}/img/icons/favicon-32x32.png +0 -0
- /package/{public → dist}/img/icons/favicon.ico +0 -0
- /package/{public → dist}/img/icons/mstile-150x150.png +0 -0
- /package/{public → dist}/img/icons/safari-pinned-tab.svg +0 -0
- /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: `${
|
|
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
|
},
|
package/src/endpoints/BtApiV1.js
CHANGED
package/src/extension.js
ADDED
|
@@ -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
|
+
}
|