@bettertogether/community-engine-vue 0.1.7 → 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 (230) 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 +31 -89
  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/src/vue.config.js +0 -0
  210. package/tests/e2e/.eslintrc.js +0 -12
  211. package/tests/e2e/plugins/index.js +0 -26
  212. package/tests/e2e/specs/home.js +0 -8
  213. package/tests/e2e/support/commands.js +0 -25
  214. package/tests/e2e/support/index.js +0 -20
  215. package/tests/unit/.eslintrc.js +0 -5
  216. package/tests/unit/example.spec.js +0 -13
  217. package/vue.config.js +0 -11
  218. package/webpack.config.js +0 -28
  219. /package/{public → dist}/_redirects +0 -0
  220. /package/{public → dist}/favicon.ico +0 -0
  221. /package/{public → dist}/img/favicon.ico +0 -0
  222. /package/{public → dist}/img/icons/android-chrome-192x192.png +0 -0
  223. /package/{public → dist}/img/icons/android-chrome-384x384.png +0 -0
  224. /package/{public → dist}/img/icons/apple-touch-icon.png +0 -0
  225. /package/{public → dist}/img/icons/favicon-16x16.png +0 -0
  226. /package/{public → dist}/img/icons/favicon-32x32.png +0 -0
  227. /package/{public → dist}/img/icons/favicon.ico +0 -0
  228. /package/{public → dist}/img/icons/mstile-150x150.png +0 -0
  229. /package/{public → dist}/img/icons/safari-pinned-tab.svg +0 -0
  230. /package/{public → dist}/robots.txt +0 -0
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <div class="bt-role-manager-page">
3
+ <RoleGate
4
+ role="admin"
5
+ :resource-type="'community'"
6
+ :resource-id="communitySlug"
7
+ >
8
+ <BCard>
9
+ <template #header>
10
+ <h2 class="h5 mb-0">
11
+ {{ t('bt.roles.title') }}
12
+ </h2>
13
+ </template>
14
+
15
+ <div
16
+ v-if="loading"
17
+ class="text-center py-4"
18
+ >
19
+ <BSpinner :label="t('bt.person.loading')" />
20
+ </div>
21
+
22
+ <div
23
+ v-else-if="!items.length"
24
+ class="text-muted"
25
+ >
26
+ {{ t('bt.person.members_empty') }}
27
+ </div>
28
+
29
+ <BTableSimple
30
+ v-else
31
+ responsive
32
+ >
33
+ <BThead>
34
+ <BTr>
35
+ <BTh>{{ t('bt.person.name_label') }}</BTh>
36
+ <BTh>{{ t('bt.navigation.members') }}</BTh>
37
+ <BTh>{{ t('bt.roles.assign') }}</BTh>
38
+ <BTh />
39
+ </BTr>
40
+ </BThead>
41
+ <BTbody>
42
+ <MemberRoleRow
43
+ v-for="member in items"
44
+ :key="member.id"
45
+ :member="member"
46
+ resource-type="community"
47
+ :resource-id="communitySlug"
48
+ />
49
+ </BTbody>
50
+ </BTableSimple>
51
+
52
+ <template #footer>
53
+ <ExtensionSlot
54
+ target="RoleManagerPage"
55
+ :context="{ communitySlug }"
56
+ />
57
+ </template>
58
+ </BCard>
59
+
60
+ <template #fallback>
61
+ <BAlert
62
+ variant="warning"
63
+ :model-value="true"
64
+ >
65
+ {{ t('bt.errors.not_found') }}
66
+ </BAlert>
67
+ </template>
68
+ </RoleGate>
69
+ </div>
70
+ </template>
71
+
72
+ <script setup>
73
+ import { onMounted } from 'vue'
74
+ import { useI18n } from 'vue-i18n'
75
+ import { useRoute } from 'vue-router'
76
+ import { BCard, BSpinner, BAlert, BTableSimple, BThead, BTbody, BTr, BTh } from 'bootstrap-vue-next'
77
+ import { useMembers } from '../../composables/useMembers'
78
+ import { useRoles } from '../../composables/useRoles'
79
+ import RoleGate from '../../components/role/RoleGate.vue'
80
+ import MemberRoleRow from '../../components/community/MemberRoleRow.vue'
81
+ import ExtensionSlot from '../../components/shared/ExtensionSlot.vue'
82
+
83
+ const { t } = useI18n()
84
+ const route = useRoute()
85
+ const communitySlug = route.params.communitySlug
86
+
87
+ const { items, loading, listActive } = useMembers(communitySlug)
88
+ const { loadRoles } = useRoles('community', communitySlug)
89
+
90
+ onMounted(async () => {
91
+ await Promise.all([listActive(), loadRoles()])
92
+ })
93
+ </script>
@@ -1,7 +1,7 @@
1
- import Vue from 'vue'
2
-
3
- import BootstrapVue from 'bootstrap-vue'
1
+ import { createBootstrap } from 'bootstrap-vue-next'
4
2
  import 'bootstrap/dist/css/bootstrap.min.css'
5
- import 'bootstrap-vue/dist/bootstrap-vue.css'
3
+ import 'bootstrap-vue-next/dist/bootstrap-vue-next.css'
6
4
 
7
- Vue.use(BootstrapVue)
5
+ export function setupBootstrapVue(app) {
6
+ app.use(createBootstrap())
7
+ }
@@ -1,8 +1,9 @@
1
- import Vue from 'vue'
2
1
  import { library } from '@fortawesome/fontawesome-svg-core'
3
2
  import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'
4
3
  import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
5
4
 
6
5
  library.add(faExternalLinkAlt)
7
6
 
8
- Vue.component('FontAwesomeIcon', FontAwesomeIcon)
7
+ export function setupFontAwesome(app) {
8
+ app.component('FontAwesomeIcon', FontAwesomeIcon)
9
+ }
@@ -1,4 +1,9 @@
1
- import './bootstrap-vue'
2
- import './font-awesome'
3
- import './vue-form-generator'
4
- import './vue-loading'
1
+ import { setupBootstrapVue } from './bootstrap-vue'
2
+ import { setupFontAwesome } from './font-awesome'
3
+ import { setupProgress } from './progress'
4
+
5
+ export function setupPlugins(app) {
6
+ setupBootstrapVue(app)
7
+ setupFontAwesome(app)
8
+ setupProgress()
9
+ }
@@ -0,0 +1,16 @@
1
+ import NProgress from 'nprogress'
2
+ import axios from 'axios'
3
+ import 'nprogress/nprogress.css'
4
+
5
+ NProgress.configure({ showSpinner: false, color: '#42b983', height: '3px' })
6
+
7
+ export function setupProgress() {
8
+ axios.interceptors.request.use((config) => {
9
+ NProgress.start()
10
+ return config
11
+ })
12
+ axios.interceptors.response.use(
13
+ (response) => { NProgress.done(); return response },
14
+ (error) => { NProgress.done(); return Promise.reject(error) },
15
+ )
16
+ }
@@ -0,0 +1,156 @@
1
+ import { ref, onMounted, onUnmounted } from 'vue'
2
+
3
+ const DISMISS_KEY = 'cev_install_prompt_dismissed_until'
4
+ const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000 // 30 days
5
+
6
+ /**
7
+ * useInstallPrompt
8
+ *
9
+ * Wraps the `beforeinstallprompt` browser event so host apps can show their own
10
+ * install UI without duplicating event-handling logic.
11
+ *
12
+ * Returns:
13
+ * canInstall — true when a deferred prompt is available AND not recently dismissed
14
+ * install() — triggers the native install prompt; resolves with { outcome }
15
+ * dismiss() — hides the prompt for 30 days (stored in localStorage)
16
+ */
17
+ export function useInstallPrompt() {
18
+ const canInstall = ref(false)
19
+ let deferredPrompt = null
20
+
21
+ function isDismissed() {
22
+ if (typeof window === 'undefined') return false
23
+ const until = localStorage.getItem(DISMISS_KEY)
24
+ return until ? Date.now() < Number(until) : false
25
+ }
26
+
27
+ function onBeforeInstallPrompt(e) {
28
+ e.preventDefault()
29
+ deferredPrompt = e
30
+ canInstall.value = !isDismissed()
31
+ }
32
+
33
+ async function install() {
34
+ if (!deferredPrompt) return { outcome: 'dismissed' }
35
+ deferredPrompt.prompt()
36
+ const choice = await deferredPrompt.userChoice
37
+ deferredPrompt = null
38
+ canInstall.value = false
39
+ return choice
40
+ }
41
+
42
+ function dismiss() {
43
+ if (typeof window !== 'undefined') {
44
+ localStorage.setItem(DISMISS_KEY, String(Date.now() + DISMISS_DURATION_MS))
45
+ }
46
+ canInstall.value = false
47
+ }
48
+
49
+ onMounted(() => {
50
+ if (typeof window === 'undefined') return
51
+ window.addEventListener('beforeinstallprompt', onBeforeInstallPrompt)
52
+ })
53
+
54
+ onUnmounted(() => {
55
+ if (typeof window === 'undefined') return
56
+ window.removeEventListener('beforeinstallprompt', onBeforeInstallPrompt)
57
+ })
58
+
59
+ return { canInstall, install, dismiss }
60
+ }
61
+
62
+ /**
63
+ * useSwUpdate
64
+ *
65
+ * Listens for the `sw-updated` custom event dispatched by the service worker
66
+ * (via the SW message channel or vite-plugin-pwa's built-in hook) and exposes
67
+ * a reactive flag plus an `applyUpdate()` helper to skip waiting and reload.
68
+ *
69
+ * Returns:
70
+ * updateAvailable — true when a new SW version is waiting to activate
71
+ * applyUpdate() — tells the waiting SW to skip waiting, then reloads the page
72
+ */
73
+ export function useSwUpdate() {
74
+ const updateAvailable = ref(false)
75
+ let registration = null
76
+
77
+ function onSwUpdated(e) {
78
+ registration = e.detail?.registration ?? null
79
+ updateAvailable.value = true
80
+ }
81
+
82
+ function applyUpdate() {
83
+ if (!updateAvailable.value) return
84
+ const sw = registration?.waiting
85
+ if (sw) {
86
+ sw.postMessage({ type: 'SKIP_WAITING' })
87
+ }
88
+ // Reload after a short tick so the new SW has time to claim clients
89
+ window.location.reload()
90
+ }
91
+
92
+ onMounted(() => {
93
+ if (typeof window === 'undefined') return
94
+ if (!('serviceWorker' in navigator)) return
95
+ window.addEventListener('sw-updated', onSwUpdated)
96
+ })
97
+
98
+ onUnmounted(() => {
99
+ if (typeof window === 'undefined') return
100
+ window.removeEventListener('sw-updated', onSwUpdated)
101
+ })
102
+
103
+ return { updateAvailable, applyUpdate }
104
+ }
105
+
106
+ /**
107
+ * getCevWorkboxConfig
108
+ *
109
+ * Returns an array of Workbox `runtimeCaching` entries recommended for
110
+ * Community Engine host apps. Spread (or merge) this into the `workbox`
111
+ * option of vite-plugin-pwa's `VitePWA()` call in the host app's vite.config.js.
112
+ *
113
+ * Covered routes:
114
+ * - /bt/api/* (excluding /bt/api/auth/*) → NetworkFirst, 5s timeout, 24h TTL
115
+ * - Images / uploads → CacheFirst, 100 entries, 7-day TTL
116
+ * - Web fonts → StaleWhileRevalidate
117
+ *
118
+ * NOT cached (by design):
119
+ * - /bt/api/auth/* — always network-only (auth tokens must never be cached)
120
+ * - idb:// — PGlite IndexedDB (never touches the network layer)
121
+ */
122
+ export function getCevWorkboxConfig() {
123
+ return [
124
+ {
125
+ // CE Rails JSON:API — skip auth routes
126
+ urlPattern: /\/bt\/api\/(?!auth\/).*/i,
127
+ handler: 'NetworkFirst',
128
+ options: {
129
+ cacheName: 'ce-api-cache',
130
+ networkTimeoutSeconds: 5,
131
+ expiration: { maxAgeSeconds: 60 * 60 * 24 },
132
+ cacheableResponse: { statuses: [0, 200] },
133
+ },
134
+ },
135
+ {
136
+ // Images and user uploads
137
+ urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
138
+ handler: 'CacheFirst',
139
+ options: {
140
+ cacheName: 'ce-image-cache',
141
+ expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 7 },
142
+ cacheableResponse: { statuses: [0, 200] },
143
+ },
144
+ },
145
+ {
146
+ // Web fonts — serve cached immediately, refresh in background
147
+ urlPattern: /\.(?:woff|woff2|ttf|otf|eot)$/i,
148
+ handler: 'StaleWhileRevalidate',
149
+ options: {
150
+ cacheName: 'ce-font-cache',
151
+ expiration: { maxEntries: 30, maxAgeSeconds: 60 * 60 * 24 * 365 },
152
+ cacheableResponse: { statuses: [0, 200] },
153
+ },
154
+ },
155
+ ]
156
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * sw-helpers.js — Service Worker context helpers for Community Engine host apps.
3
+ *
4
+ * Import this file from your host app's custom service worker entry point
5
+ * (e.g. `src/sw.js`) alongside vite-plugin-pwa's Workbox integration.
6
+ *
7
+ * ⚠️ This file runs INSIDE the service worker — do NOT import Vue or any
8
+ * browser-DOM API here. Only Web Worker / Workbox globals are available.
9
+ *
10
+ * Usage in host app sw.js:
11
+ * import { registerBackgroundSyncRoutes } from '@bettertogether/community-engine-vue/pwa/sw-helpers'
12
+ * import { precacheAndRoute } from 'workbox-precaching'
13
+ * import { registerRoute } from 'workbox-routing'
14
+ *
15
+ * precacheAndRoute(self.__WB_MANIFEST)
16
+ * registerBackgroundSyncRoutes({ registerRoute })
17
+ */
18
+
19
+ /**
20
+ * registerBackgroundSyncRoutes({ registerRoute })
21
+ *
22
+ * Registers Workbox routes that queue failed CE API mutation requests
23
+ * (POST / PATCH / DELETE to /bt/api/*) into a BackgroundSync queue.
24
+ * When the browser reconnects — even if the tab is closed — the browser
25
+ * replays the queued requests automatically via the Background Sync API.
26
+ *
27
+ * This complements CEV's `drainSyncQueue()` (which runs while the tab is open)
28
+ * by handling the closed-tab / connectivity-lost scenario.
29
+ *
30
+ * @param {object} workboxModules
31
+ * @param {Function} workboxModules.registerRoute - workbox-routing registerRoute
32
+ * @param {Function} [workboxModules.NetworkOnly] - workbox-strategies NetworkOnly (loaded lazily if omitted)
33
+ * @param {Function} [workboxModules.BackgroundSyncPlugin] - workbox-background-sync BackgroundSyncPlugin
34
+ */
35
+ export function registerBackgroundSyncRoutes({
36
+ registerRoute,
37
+ NetworkOnly,
38
+ BackgroundSyncPlugin,
39
+ }) {
40
+ // Allow host apps to pass in pre-imported Workbox classes, or fall back to
41
+ // dynamic require() in SW context (works with Workbox's module bundling).
42
+ const NetworkOnlyStrategy = NetworkOnly ?? (() => {
43
+ // eslint-disable-next-line no-undef
44
+ const { NetworkOnly: NO } = require('workbox-strategies')
45
+ return NO
46
+ })()
47
+
48
+ const BgSyncPlugin = BackgroundSyncPlugin ?? (() => {
49
+ // eslint-disable-next-line no-undef
50
+ const { BackgroundSyncPlugin: BSP } = require('workbox-background-sync')
51
+ return BSP
52
+ })()
53
+
54
+ const bgSyncPlugin = new BgSyncPlugin('ce-api-mutation-queue', {
55
+ maxRetentionTime: 24 * 60, // Retry for up to 24 hours (in minutes)
56
+ onSync: async ({ queue }) => {
57
+ // Replay all queued requests; broadcast result to open tabs
58
+ let entry
59
+ while ((entry = await queue.shiftRequest())) {
60
+ try {
61
+ await fetch(entry.request)
62
+ broadcastSyncEvent('ce-bg-sync-replayed', { url: entry.request.url })
63
+ } catch {
64
+ await queue.unshiftRequest(entry)
65
+ broadcastSyncEvent('ce-bg-sync-failed', { url: entry.request.url })
66
+ throw new Error('Background sync replay failed — will retry')
67
+ }
68
+ }
69
+ },
70
+ })
71
+
72
+ // Queue POST / PATCH / DELETE to CE API (excluding auth routes)
73
+ registerRoute(
74
+ ({ url, request }) =>
75
+ /\/bt\/api\/(?!auth\/)/.test(url.pathname) &&
76
+ ['POST', 'PATCH', 'DELETE'].includes(request.method),
77
+ new NetworkOnlyStrategy({ plugins: [bgSyncPlugin] }),
78
+ 'POST',
79
+ )
80
+
81
+ registerRoute(
82
+ ({ url, request }) =>
83
+ /\/bt\/api\/(?!auth\/)/.test(url.pathname) &&
84
+ ['POST', 'PATCH', 'DELETE'].includes(request.method),
85
+ new NetworkOnlyStrategy({ plugins: [bgSyncPlugin] }),
86
+ 'PATCH',
87
+ )
88
+
89
+ registerRoute(
90
+ ({ url, request }) =>
91
+ /\/bt\/api\/(?!auth\/)/.test(url.pathname) &&
92
+ ['POST', 'PATCH', 'DELETE'].includes(request.method),
93
+ new NetworkOnlyStrategy({ plugins: [bgSyncPlugin] }),
94
+ 'DELETE',
95
+ )
96
+ }
97
+
98
+ /**
99
+ * broadcastSyncEvent(type, detail)
100
+ *
101
+ * Posts a message to all controlled tabs so the app can react to background
102
+ * sync replay events (e.g. re-run drainSyncQueue to refresh the pending count).
103
+ *
104
+ * @param {string} type - event type string forwarded as `data.type`
105
+ * @param {object} detail - arbitrary payload
106
+ */
107
+ export function broadcastSyncEvent(type, detail = {}) {
108
+ // self.clients is only available in SW context
109
+ if (typeof self === 'undefined' || !self.clients) return
110
+ self.clients.matchAll({ includeUncontrolled: false, type: 'window' }).then((clients) => {
111
+ clients.forEach((client) => client.postMessage({ type, ...detail }))
112
+ })
113
+ }
114
+
115
+ /**
116
+ * dispatchSwUpdatedEvent(registration)
117
+ *
118
+ * Call this from your SW update handler to emit the `sw-updated` DOM event
119
+ * that CEV's `useSwUpdate()` composable listens for.
120
+ *
121
+ * Typically wired up in vite-plugin-pwa's `onRegisteredSW` / `onNeedRefresh`
122
+ * callback in the host app rather than in the SW file itself, but provided
123
+ * here for completeness.
124
+ *
125
+ * @param {ServiceWorkerRegistration} registration
126
+ */
127
+ export function dispatchSwUpdatedEvent(registration) {
128
+ if (typeof window === 'undefined') return
129
+ window.dispatchEvent(new CustomEvent('sw-updated', { detail: { registration } }))
130
+ }
@@ -0,0 +1,78 @@
1
+ import CommunityLayout from '../layouts/CommunityLayout.vue'
2
+
3
+ export const communityRoutes = [
4
+ {
5
+ path: '/communities/:communitySlug',
6
+ component: CommunityLayout,
7
+ children: [
8
+ {
9
+ path: '',
10
+ name: 'CommunityHome',
11
+ component: () => import('../pages/community/CommunityHome.vue'),
12
+ meta: { requiresAuth: false },
13
+ },
14
+ {
15
+ path: 'posts',
16
+ name: 'CommunityPosts',
17
+ component: () => import('../pages/community/CommunityPosts.vue'),
18
+ },
19
+ {
20
+ path: 'posts/:id',
21
+ name: 'CommunityPost',
22
+ component: () => import('../pages/community/CommunityPost.vue'),
23
+ },
24
+ {
25
+ path: 'events',
26
+ name: 'CommunityEvents',
27
+ component: () => import('../pages/community/CommunityEvents.vue'),
28
+ },
29
+ {
30
+ path: 'events/:id',
31
+ name: 'CommunityEvent',
32
+ component: () => import('../pages/community/CommunityEvent.vue'),
33
+ },
34
+ {
35
+ path: 'members',
36
+ name: 'CommunityMembers',
37
+ component: () => import('../pages/community/CommunityMembers.vue'),
38
+ },
39
+ {
40
+ path: 'conversations',
41
+ name: 'CommunityConversations',
42
+ component: () => import('../pages/community/CommunityConversations.vue'),
43
+ },
44
+ {
45
+ path: 'conversations/:id',
46
+ name: 'CommunityConversation',
47
+ component: () => import('../pages/community/CommunityConversation.vue'),
48
+ },
49
+ {
50
+ path: 'pages',
51
+ name: 'CommunityPages',
52
+ component: () => import('../pages/community/CommunityPages.vue'),
53
+ },
54
+ {
55
+ path: 'pages/:id',
56
+ name: 'CommunityPage',
57
+ component: () => import('../pages/community/CommunityPage.vue'),
58
+ },
59
+ {
60
+ path: 'joatu',
61
+ name: 'CommunityJoaTu',
62
+ component: () => import('../pages/community/CommunityJoaTu.vue'),
63
+ },
64
+ {
65
+ path: 'settings',
66
+ name: 'community-settings',
67
+ component: () => import('../pages/community/CommunitySettingsPage.vue'),
68
+ meta: { requiresAuth: true },
69
+ },
70
+ {
71
+ path: 'settings/roles',
72
+ name: 'community-role-manager',
73
+ component: () => import('../pages/community/RoleManagerPage.vue'),
74
+ meta: { requiresAuth: true },
75
+ },
76
+ ],
77
+ },
78
+ ]