@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.
- 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 +31 -89
- 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/src/vue.config.js +0 -0
- 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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
export function setupFontAwesome(app) {
|
|
8
|
+
app.component('FontAwesomeIcon', FontAwesomeIcon)
|
|
9
|
+
}
|
package/src/plugins/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import './bootstrap-vue'
|
|
2
|
-
import './font-awesome'
|
|
3
|
-
import './
|
|
4
|
-
|
|
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
|
+
}
|
package/src/pwa/index.js
ADDED
|
@@ -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
|
+
]
|