@bettertogether/community-engine-vue 0.1.7 → 0.2.3

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 +105 -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,121 @@
1
+ <template>
2
+ <Transition name="sync-bar-fade">
3
+ <div
4
+ v-if="showBar"
5
+ class="sync-status-bar"
6
+ :class="barClass"
7
+ role="status"
8
+ :aria-live="syncStore.online ? 'polite' : 'assertive'"
9
+ >
10
+ <span class="sync-status-bar__icon">
11
+ <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
12
+ <span v-if="!syncStore.online">⚡</span>
13
+ <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
14
+ <span v-else-if="syncStore.syncing" class="sync-spin-icon">↻</span>
15
+ <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
16
+ <span v-else>↻</span>
17
+ </span>
18
+ <span class="sync-status-bar__text">{{ statusText }}</span>
19
+ <span
20
+ v-if="syncStore.online && syncStore.electricConnected"
21
+ class="sync-status-bar__electric"
22
+ title="Electric sync connected"
23
+ >
24
+ <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
25
+ <span class="sync-electric-dot" aria-hidden="true">●</span>
26
+ </span>
27
+ </div>
28
+ </Transition>
29
+ </template>
30
+
31
+ <script setup>
32
+ import { computed } from 'vue'
33
+ import { useI18n } from 'vue-i18n'
34
+ import { useSyncStore } from '../../stores/sync'
35
+
36
+ const { t } = useI18n()
37
+ const syncStore = useSyncStore()
38
+
39
+ const showBar = computed(
40
+ () => !syncStore.online || syncStore.syncing || syncStore.pendingCount > 0,
41
+ )
42
+
43
+ const barClass = computed(() => ({
44
+ 'sync-status-bar--offline': !syncStore.online,
45
+ 'sync-status-bar--syncing': syncStore.online && (syncStore.syncing || syncStore.pendingCount > 0),
46
+ }))
47
+
48
+ const statusText = computed(() => {
49
+ if (!syncStore.online) {
50
+ const n = syncStore.pendingCount
51
+ return n > 0
52
+ ? `${t('bt.sync.status_bar_offline')} — ${t('bt.sync.pending_count', n)}`
53
+ : t('bt.sync.status_bar_offline')
54
+ }
55
+ if (syncStore.syncing || syncStore.pendingCount > 0) {
56
+ return t('bt.sync.pending_count', syncStore.pendingCount)
57
+ }
58
+ return ''
59
+ })
60
+ </script>
61
+
62
+ <style scoped lang="scss">
63
+ @import '../../stylesheets/sync-indicators.scss';
64
+
65
+ .sync-status-bar {
66
+ position: sticky;
67
+ top: 0;
68
+ left: 0;
69
+ right: 0;
70
+ z-index: 2000;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ gap: 6px;
75
+ padding: 4px 12px;
76
+ font-size: 0.82rem;
77
+ font-weight: 500;
78
+ transition: background-color 0.3s;
79
+
80
+ &--offline {
81
+ background-color: $sync-local;
82
+ color: #1a1a1a;
83
+ height: $sync-bar-height-offline;
84
+ }
85
+
86
+ &--syncing {
87
+ background-color: $sync-syncing;
88
+ color: white;
89
+ height: $sync-bar-height-offline;
90
+ }
91
+
92
+ &__icon {
93
+ font-size: 1rem;
94
+ line-height: 1;
95
+ }
96
+ }
97
+
98
+ .sync-spin-icon {
99
+ display: inline-block;
100
+ animation: sync-spin 1s linear infinite;
101
+ }
102
+
103
+ .sync-status-bar__electric {
104
+ margin-left: 6px;
105
+ font-size: 0.65rem;
106
+ color: #4caf50;
107
+ line-height: 1;
108
+ }
109
+
110
+ .sync-bar-fade-enter-active,
111
+ .sync-bar-fade-leave-active {
112
+ transition: opacity 0.3s, max-height 0.3s;
113
+ max-height: 40px;
114
+ }
115
+
116
+ .sync-bar-fade-enter-from,
117
+ .sync-bar-fade-leave-to {
118
+ opacity: 0;
119
+ max-height: 0;
120
+ }
121
+ </style>
@@ -0,0 +1,19 @@
1
+ import { useResource } from './useResource'
2
+ import { getDb } from '../db/client'
3
+
4
+ export function useCommunities() {
5
+ const resource = useResource('communities')
6
+
7
+ async function findBySlug(slug) {
8
+ const db = await getDb()
9
+ const { rows } = await db.query('SELECT * FROM communities WHERE slug = $1 LIMIT 1', [slug])
10
+ resource.current.value = rows[0] ?? null
11
+ return resource.current.value
12
+ }
13
+
14
+ async function listPublic() {
15
+ return resource.list({ privacy: 'public' })
16
+ }
17
+
18
+ return { ...resource, findBySlug, listPublic }
19
+ }
@@ -0,0 +1,5 @@
1
+ import { useResource } from './useResource'
2
+
3
+ export function useConversations(communityId = null) {
4
+ return useResource('conversations', communityId ? { community_id: communityId } : {})
5
+ }
@@ -0,0 +1,28 @@
1
+ import { useResource } from './useResource'
2
+ import { getDb } from '../db/client'
3
+
4
+ export function useEvents(communityId = null) {
5
+ const resource = useResource('events', communityId ? { community_id: communityId } : {})
6
+
7
+ async function listUpcoming(_extraFilters = {}) {
8
+ const db = await getDb()
9
+ const now = new Date().toISOString()
10
+ if (communityId) {
11
+ const { rows } = await db.query(
12
+ 'SELECT * FROM events WHERE starts_at >= $1 AND community_id = $2 ORDER BY starts_at ASC',
13
+ [now, communityId],
14
+ )
15
+ resource.items.value = rows
16
+ return rows
17
+ } else {
18
+ const { rows } = await db.query(
19
+ 'SELECT * FROM events WHERE starts_at >= $1 ORDER BY starts_at ASC',
20
+ [now],
21
+ )
22
+ resource.items.value = rows
23
+ return rows
24
+ }
25
+ }
26
+
27
+ return { ...resource, listUpcoming }
28
+ }
@@ -0,0 +1,10 @@
1
+ import { computed } from 'vue'
2
+ import { useResource } from './useResource'
3
+
4
+ export function useInvitations(communityId = null) {
5
+ const filters = communityId ? { community_id: communityId } : {}
6
+ const resource = useResource('invitations', filters)
7
+ const pending = computed(() => resource.items.value.filter((i) => i.status === 'pending'))
8
+ const accepted = computed(() => resource.items.value.filter((i) => i.status === 'accepted'))
9
+ return { ...resource, pending, accepted }
10
+ }
@@ -0,0 +1,11 @@
1
+ import { computed } from 'vue'
2
+ import { useResource } from './useResource'
3
+
4
+ export function useJoaTuAgreements(communityId = null) {
5
+ const filters = communityId ? { community_id: communityId } : {}
6
+ const resource = useResource('joa_tu_agreements', filters)
7
+ const pending = computed(() => resource.items.value.filter((a) => a.status === 'pending'))
8
+ const active = computed(() => resource.items.value.filter((a) => a.status === 'active'))
9
+ const completed = computed(() => resource.items.value.filter((a) => a.status === 'completed'))
10
+ return { ...resource, pending, active, completed }
11
+ }
@@ -0,0 +1,10 @@
1
+ import { computed } from 'vue'
2
+ import { useResource } from './useResource'
3
+
4
+ export function useJoaTuOffers(communityId = null) {
5
+ const filters = communityId ? { community_id: communityId } : {}
6
+ const resource = useResource('joa_tu_offers', filters)
7
+ const open = computed(() => resource.items.value.filter((o) => o.status === 'open'))
8
+ const fulfilled = computed(() => resource.items.value.filter((o) => o.status === 'fulfilled'))
9
+ return { ...resource, open, fulfilled }
10
+ }
@@ -0,0 +1,10 @@
1
+ import { computed } from 'vue'
2
+ import { useResource } from './useResource'
3
+
4
+ export function useJoaTuRequests(communityId = null) {
5
+ const filters = communityId ? { community_id: communityId } : {}
6
+ const resource = useResource('joa_tu_requests', filters)
7
+ const open = computed(() => resource.items.value.filter((r) => r.status === 'open'))
8
+ const fulfilled = computed(() => resource.items.value.filter((r) => r.status === 'fulfilled'))
9
+ return { ...resource, open, fulfilled }
10
+ }
@@ -0,0 +1,30 @@
1
+ import { useResource } from './useResource'
2
+ import { getDb } from '../db/client'
3
+
4
+ export function useMembers(communityId = null) {
5
+ const resource = useResource(
6
+ 'person_community_memberships',
7
+ communityId ? { community_id: communityId } : {},
8
+ )
9
+
10
+ async function listActive() {
11
+ const db = await getDb()
12
+ if (communityId) {
13
+ const { rows } = await db.query(
14
+ 'SELECT * FROM person_community_memberships WHERE left_at IS NULL AND community_id = $1 ORDER BY joined_at DESC',
15
+ [communityId],
16
+ )
17
+ resource.items.value = rows
18
+ return rows
19
+ } else {
20
+ const { rows } = await db.query(
21
+ 'SELECT * FROM person_community_memberships WHERE left_at IS NULL ORDER BY joined_at DESC',
22
+ [],
23
+ )
24
+ resource.items.value = rows
25
+ return rows
26
+ }
27
+ }
28
+
29
+ return { ...resource, listActive }
30
+ }
@@ -0,0 +1,5 @@
1
+ import { useResource } from './useResource'
2
+
3
+ export function useMessages(conversationId = null) {
4
+ return useResource('messages', conversationId ? { conversation_id: conversationId } : {})
5
+ }
@@ -0,0 +1,5 @@
1
+ import { useResource } from './useResource'
2
+
3
+ export function useNotifications(personId = null) {
4
+ return useResource('notifications', personId ? { person_id: personId } : {})
5
+ }
@@ -0,0 +1,6 @@
1
+ import { useResource } from './useResource'
2
+
3
+ export function usePages(communityId = null) {
4
+ const filters = communityId ? { community_id: communityId } : {}
5
+ return useResource('pages', filters)
6
+ }
@@ -0,0 +1,65 @@
1
+ import { useResource } from './useResource'
2
+ import { useAuthStore } from '../stores/auth'
3
+
4
+ /**
5
+ * usePersonBlocks — manage block relationships.
6
+ *
7
+ * A block is one-directional: blocker_id → blocked_id.
8
+ * Blocked people are hidden from lists and cannot interact.
9
+ */
10
+ export function usePersonBlocks() {
11
+ const auth = useAuthStore()
12
+ const blocksResource = useResource('person_blocks')
13
+
14
+ const { items: blocks, loading, create, destroy, list } = blocksResource
15
+
16
+ async function blockPerson(blockedId, reason = null) {
17
+ const { nanoid } = await import('nanoid')
18
+ return create({
19
+ id: nanoid(),
20
+ blocker_id: auth.personId,
21
+ blocked_id: blockedId,
22
+ reason,
23
+ })
24
+ }
25
+
26
+ async function unblockPerson(blockedId) {
27
+ const record = blocks.value.find(
28
+ b => b.blocker_id === auth.personId && b.blocked_id === blockedId
29
+ )
30
+ if (record) return destroy(record.id)
31
+ }
32
+
33
+ function isBlocked(personId) {
34
+ return blocks.value.some(
35
+ b => b.blocker_id === auth.personId && b.blocked_id === personId
36
+ )
37
+ }
38
+
39
+ function isBlockedBy(personId) {
40
+ return blocks.value.some(
41
+ b => b.blocker_id === personId && b.blocked_id === auth.personId
42
+ )
43
+ }
44
+
45
+ // Filter a list of people/items, removing any that are blocked
46
+ function filterBlocked(items, idKey = 'id') {
47
+ const blockedIds = new Set(
48
+ blocks.value
49
+ .filter(b => b.blocker_id === auth.personId)
50
+ .map(b => b.blocked_id)
51
+ )
52
+ return items.filter(item => !blockedIds.has(item[idKey]))
53
+ }
54
+
55
+ return {
56
+ blocks,
57
+ loading,
58
+ loadBlocks: list,
59
+ blockPerson,
60
+ unblockPerson,
61
+ isBlocked,
62
+ isBlockedBy,
63
+ filterBlocked,
64
+ }
65
+ }
@@ -0,0 +1,27 @@
1
+ import { useResource } from './useResource'
2
+ import { getDb } from '../db/client'
3
+
4
+ export function usePosts(communityId = null) {
5
+ const resource = useResource('posts', communityId ? { community_id: communityId } : {})
6
+
7
+ async function listPublished(_extraFilters = {}) {
8
+ const db = await getDb()
9
+ if (communityId) {
10
+ const { rows } = await db.query(
11
+ 'SELECT * FROM posts WHERE published_at IS NOT NULL AND community_id = $1 ORDER BY published_at DESC',
12
+ [communityId],
13
+ )
14
+ resource.items.value = rows
15
+ return rows
16
+ } else {
17
+ const { rows } = await db.query(
18
+ 'SELECT * FROM posts WHERE published_at IS NOT NULL ORDER BY published_at DESC',
19
+ [],
20
+ )
21
+ resource.items.value = rows
22
+ return rows
23
+ }
24
+ }
25
+
26
+ return { ...resource, listPublished }
27
+ }
@@ -0,0 +1,137 @@
1
+ import { ref, computed } from 'vue'
2
+ import { getDb } from '../db/client'
3
+ import { useSyncStore } from '../stores/sync'
4
+
5
+ function buildWhereClause(filters) {
6
+ const conditions = Object.entries(filters)
7
+ .filter(([, v]) => v != null)
8
+ .map(([k]) => `${k} = ?`)
9
+ return conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
10
+ }
11
+
12
+ function buildWhereValues(filters) {
13
+ return Object.entries(filters)
14
+ .filter(([, v]) => v != null)
15
+ .map(([, v]) => v)
16
+ }
17
+
18
+ function buildInsertSql(table, row) {
19
+ const cols = Object.keys(row)
20
+ const placeholders = cols.map(() => '?').join(', ')
21
+ return `INSERT INTO ${table} (${cols.join(', ')}) VALUES (${placeholders})`
22
+ }
23
+
24
+ function buildUpdateSql(table, attrs) {
25
+ const sets = Object.keys(attrs).map((k) => `${k} = ?`).join(', ')
26
+ return `UPDATE ${table} SET ${sets} WHERE id = ?`
27
+ }
28
+
29
+ export function useResource(table, defaultFilters = {}) {
30
+ const items = ref([])
31
+ const current = ref(null)
32
+ const loading = ref(false)
33
+ const error = ref(null)
34
+
35
+ const pendingCount = computed(() => items.value.filter((i) => i._sync_status === 'local').length)
36
+
37
+ async function list(extraFilters = {}) {
38
+ loading.value = true
39
+ error.value = null
40
+ try {
41
+ const db = await getDb()
42
+ const filters = { ...defaultFilters, ...extraFilters }
43
+ const where = buildWhereClause(filters)
44
+ const vals = buildWhereValues(filters)
45
+ const result = await db.query(
46
+ `SELECT * FROM ${table} ${where} ORDER BY _local_updated DESC`,
47
+ vals,
48
+ )
49
+ items.value = result.rows
50
+ const sync = useSyncStore()
51
+ sync.setPendingCount(result.rows.filter((r) => r._sync_status === 'local').length)
52
+ return result.rows
53
+ } catch (e) {
54
+ error.value = e
55
+ throw e
56
+ } finally {
57
+ loading.value = false
58
+ }
59
+ }
60
+
61
+ async function get(id) {
62
+ loading.value = true
63
+ error.value = null
64
+ try {
65
+ const db = await getDb()
66
+ const result = await db.query(`SELECT * FROM ${table} WHERE id = $1`, [id])
67
+ current.value = result.rows[0] ?? null
68
+ return current.value
69
+ } catch (e) {
70
+ error.value = e
71
+ throw e
72
+ } finally {
73
+ loading.value = false
74
+ }
75
+ }
76
+
77
+ async function create(attrs) {
78
+ const db = await getDb()
79
+ const id = attrs.id || crypto.randomUUID()
80
+ const now = new Date().toISOString()
81
+ const row = {
82
+ id,
83
+ ...attrs,
84
+ _sync_status: 'local',
85
+ _local_updated: now,
86
+ }
87
+ await db.query(buildInsertSql(table, row), Object.values(row))
88
+ items.value.unshift(row)
89
+ return row
90
+ }
91
+
92
+ async function update(id, attrs) {
93
+ const db = await getDb()
94
+ const now = new Date().toISOString()
95
+ const updates = { ...attrs, _sync_status: 'local', _local_updated: now }
96
+ await db.query(buildUpdateSql(table, updates), [...Object.values(updates), id])
97
+ const idx = items.value.findIndex((i) => i.id === id)
98
+ if (idx >= 0) items.value[idx] = { ...items.value[idx], ...updates }
99
+ if (current.value?.id === id) current.value = { ...current.value, ...updates }
100
+ return items.value[idx] ?? current.value
101
+ }
102
+
103
+ async function destroy(id) {
104
+ const db = await getDb()
105
+ await db.query(`DELETE FROM ${table} WHERE id = $1`, [id])
106
+ items.value = items.value.filter((i) => i.id !== id)
107
+ if (current.value?.id === id) current.value = null
108
+ }
109
+
110
+ async function markSynced(id, serverAt = null) {
111
+ const db = await getDb()
112
+ const now = serverAt || new Date().toISOString()
113
+ await db.query(
114
+ `UPDATE ${table} SET _sync_status = 'synced', _server_at = ? WHERE id = ?`,
115
+ [now, id],
116
+ )
117
+ const idx = items.value.findIndex((i) => i.id === id)
118
+ if (idx >= 0) items.value[idx] = { ...items.value[idx], _sync_status: 'synced', _server_at: now }
119
+ if (current.value?.id === id) {
120
+ current.value = { ...current.value, _sync_status: 'synced', _server_at: now }
121
+ }
122
+ }
123
+
124
+ return {
125
+ items,
126
+ current,
127
+ loading,
128
+ error,
129
+ pendingCount,
130
+ list,
131
+ get,
132
+ create,
133
+ update,
134
+ destroy,
135
+ markSynced,
136
+ }
137
+ }
@@ -0,0 +1,94 @@
1
+ import { computed } from 'vue'
2
+ import { useResource } from './useResource'
3
+ import { useAuthStore } from '../stores/auth'
4
+
5
+ /**
6
+ * useRoles — manages roles and person_roles assignments.
7
+ *
8
+ * Roles are scoped to a resource (community, etc.) or global (resource_type = null).
9
+ * The current person's effective roles are derived from their person_roles records.
10
+ */
11
+ export function useRoles(resourceType = null, resourceId = null) {
12
+ const auth = useAuthStore()
13
+
14
+ const rolesResource = useResource('roles')
15
+ const personRolesResource = useResource('person_roles')
16
+
17
+ // All roles for this resource scope
18
+ const scopedRoles = computed(() =>
19
+ rolesResource.items.value.filter(r =>
20
+ r.resource_type === resourceType && r.resource_id === resourceId
21
+ )
22
+ )
23
+
24
+ // Current person's role assignments in this scope
25
+ const myAssignments = computed(() =>
26
+ personRolesResource.items.value.filter(pr =>
27
+ pr.person_id === auth.personId &&
28
+ pr.resource_type === resourceType &&
29
+ pr.resource_id === resourceId
30
+ )
31
+ )
32
+
33
+ // Slugs the current person holds in this scope
34
+ const mySlugs = computed(() => {
35
+ const assignedRoleIds = new Set(myAssignments.value.map(pr => pr.role_id))
36
+ return rolesResource.items.value
37
+ .filter(r => assignedRoleIds.has(r.id))
38
+ .map(r => r.slug)
39
+ })
40
+
41
+ /**
42
+ * Check if the current person has a given role slug in this scope.
43
+ * Also returns true for 'admin' if the person has any admin role anywhere.
44
+ */
45
+ function hasRole(slug) {
46
+ return mySlugs.value.includes(slug)
47
+ }
48
+
49
+ /**
50
+ * Check if person has any of the given role slugs.
51
+ */
52
+ function hasAnyRole(...slugs) {
53
+ return slugs.some(s => hasRole(s))
54
+ }
55
+
56
+ async function loadRoles() {
57
+ await rolesResource.list()
58
+ await personRolesResource.list()
59
+ }
60
+
61
+ async function grantRole(personId, roleId) {
62
+ const { nanoid } = await import('nanoid')
63
+ return personRolesResource.create({
64
+ id: nanoid(),
65
+ person_id: personId,
66
+ role_id: roleId,
67
+ resource_type: resourceType,
68
+ resource_id: resourceId,
69
+ granted_by_id: auth.personId,
70
+ })
71
+ }
72
+
73
+ async function revokeRole(personRoleId) {
74
+ return personRolesResource.destroy(personRoleId)
75
+ }
76
+
77
+ return {
78
+ // Data
79
+ roles: rolesResource.items,
80
+ personRoles: personRolesResource.items,
81
+ scopedRoles,
82
+ myAssignments,
83
+ mySlugs,
84
+ // Checks
85
+ hasRole,
86
+ hasAnyRole,
87
+ // Actions
88
+ loadRoles,
89
+ grantRole,
90
+ revokeRole,
91
+ // Loading state
92
+ loading: rolesResource.loading,
93
+ }
94
+ }
@@ -0,0 +1,22 @@
1
+ import { computed } from 'vue'
2
+
3
+ export function useSyncStatus(item) {
4
+ const syncStatus = computed(() => item?.value?._sync_status ?? item?._sync_status ?? 'synced')
5
+
6
+ const isLocal = computed(() => syncStatus.value === 'local')
7
+ const isSyncing = computed(() => syncStatus.value === 'syncing')
8
+ const isSynced = computed(() => syncStatus.value === 'synced')
9
+ const isConflict = computed(() => syncStatus.value === 'conflict')
10
+
11
+ const label = computed(() => {
12
+ switch (syncStatus.value) {
13
+ case 'local': return 'Saved locally — will sync when online'
14
+ case 'syncing': return 'Syncing…'
15
+ case 'synced': return 'Synced'
16
+ case 'conflict': return 'Conflict — newer version on server'
17
+ default: return ''
18
+ }
19
+ })
20
+
21
+ return { syncStatus, isLocal, isSyncing, isSynced, isConflict, label }
22
+ }
@@ -0,0 +1,20 @@
1
+ import { useToastController } from 'bootstrap-vue-next'
2
+
3
+ export function useToaster() {
4
+ const { show } = useToastController()
5
+
6
+ function toast(msg, type = null, opts = {}) {
7
+ show({
8
+ props: {
9
+ body: msg,
10
+ variant: type,
11
+ solid: true,
12
+ value: opts.autoHideDelay || 2000,
13
+ title: opts.title,
14
+ pos: 'top-end',
15
+ },
16
+ })
17
+ }
18
+
19
+ return { toast }
20
+ }
package/src/context.js ADDED
@@ -0,0 +1,18 @@
1
+ let _context = null
2
+
3
+ export function setCevContext(ctx) {
4
+ _context = ctx
5
+ }
6
+
7
+ export function getCevContext() {
8
+ if (!_context) {
9
+ throw new Error(
10
+ '[@bettertogether/community-engine-vue] Plugin not installed. ' +
11
+ 'Call app.use(CommunityEngineVue) before accessing the plugin context.'
12
+ )
13
+ }
14
+ return {
15
+ ..._context,
16
+ get router() { return _context.app?.config?.globalProperties?.$router },
17
+ }
18
+ }