@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.
- 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 +105 -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,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,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,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
|
+
}
|