@bettertogether/community-engine-vue 0.1.6 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -1
- package/dist/assets/BBadge.vue_vue_type_script_setup_true_lang-IIZ8QpjG-Z9WDKHqT.js +1 -0
- package/dist/assets/BCardText.vue_vue_type_script_setup_true_lang-Be6CD36N-B5JCTdmm.js +3 -0
- package/dist/assets/BFormSelect.vue_vue_type_script_setup_true_lang-BigptVap-B_HbOOZR.js +1 -0
- package/dist/assets/BRow.vue_vue_type_script_setup_true_lang-69TY75-8-DJdEdyx7.js +1 -0
- package/dist/assets/Communities-Cx4tT-bx.js +1 -0
- package/dist/assets/Communities-n33ssuUH.css +1 -0
- package/dist/assets/CommunityConversation-bBkYBs2k.css +1 -0
- package/dist/assets/CommunityConversation-jHAnv_Ps.js +1 -0
- package/dist/assets/CommunityConversations-rEDGS7To.js +1 -0
- package/dist/assets/CommunityEvent-CUdT0aT4.js +1 -0
- package/dist/assets/CommunityEvents-rsOgcxQr.js +1 -0
- package/dist/assets/CommunityHome-ChuTE2Nz.js +1 -0
- package/dist/assets/CommunityJoaTu-CpLIY_83.js +1 -0
- package/dist/assets/CommunityMembers-C3UtzQGp.css +1 -0
- package/dist/assets/CommunityMembers-DKf74ltl.js +1 -0
- package/dist/assets/CommunityPage-C5x23iQl.css +1 -0
- package/dist/assets/CommunityPage-CRYg9-rW.js +1 -0
- package/dist/assets/CommunityPages-IsLTNFC3.js +1 -0
- package/dist/assets/CommunityPost-BOnqqxVs.js +1 -0
- package/dist/assets/CommunityPost-BRYtkDSY.css +1 -0
- package/dist/assets/CommunityPosts-DY1olmcU.js +1 -0
- package/dist/assets/Error404-D10VQARe.js +1 -0
- package/dist/assets/EventCard-vfutXTdg.js +1 -0
- package/dist/assets/EventList-ChtehYcJ.js +1 -0
- package/dist/assets/ExtensionSlot-DJKbrq4c.js +1 -0
- package/dist/assets/PostList-BuHrBBqX.css +1 -0
- package/dist/assets/PostList-DYFgxlE8.js +1 -0
- package/dist/assets/SyncBadge-B1JBsdUk.js +1 -0
- package/dist/assets/SyncBadge-FNO-QLuu.css +1 -0
- package/dist/assets/UserPasswordNew-D_Djldm9.css +1 -0
- package/dist/assets/UserPasswordNew-al9bNBTZ.js +1 -0
- package/dist/assets/UserPasswordReset-42zs98RW.js +1 -0
- package/dist/assets/UserPasswordReset-D_6OQDZY.css +1 -0
- package/dist/assets/UserResendConfirmation-CGavYB81.js +1 -0
- package/dist/assets/UserResendConfirmation-DNTHcaar.css +1 -0
- package/dist/assets/UserSignIn-BIRb0HkV.js +1 -0
- package/dist/assets/UserSignIn-C-Pol8OD.css +1 -0
- package/dist/assets/UserSignUp-ChkKQAd2.css +1 -0
- package/dist/assets/UserSignUp-Df6o3vlO.js +1 -0
- package/dist/assets/better-together-logo-61cxo5d5.png +0 -0
- package/dist/assets/index-BFt-JKVh.css +5 -0
- package/dist/assets/index-COo3Jb7v.js +1088 -0
- package/dist/assets/nodefs-Bfyh92qg.js +1 -0
- package/dist/assets/opfs-ahp-BLzlXf6u.js +3 -0
- package/dist/assets/pglite-BdRI_ZYT.wasm +0 -0
- package/dist/assets/pglite-COscPi1Y.data +0 -0
- package/dist/assets/usePages-DDjDQRCy.js +1 -0
- package/dist/assets/usePosts-Bf2Ccwr4.js +1 -0
- package/{public → dist}/index.html +9 -19
- package/package.json +57 -45
- package/src/BtApp.vue +34 -43
- package/src/components/BtBrandingLogo.vue +10 -18
- package/src/components/BtHeader.vue +34 -84
- package/src/components/BtMainContent.vue +12 -43
- package/src/components/BtNavBar.vue +25 -38
- package/src/components/BtNavItem.vue +25 -58
- package/src/components/BtNavUser.vue +65 -86
- package/src/components/BtProfileForm.vue +48 -39
- package/src/components/BtUserNewPasswordForm.vue +52 -74
- package/src/components/BtUserResendConfirmationForm.vue +45 -83
- package/src/components/BtUserResetPasswordForm.vue +45 -77
- package/src/components/BtUserSignInForm.vue +59 -75
- package/src/components/BtUserSignUpForm.vue +90 -103
- package/src/components/CommunityForm.vue +47 -39
- package/src/components/community/CommunityCard.vue +113 -0
- package/src/components/community/CommunityHeader.vue +91 -0
- package/src/components/community/CommunityList.vue +59 -0
- package/src/components/community/MemberRoleRow.vue +107 -0
- package/src/components/conversation/ConversationCard.vue +49 -0
- package/src/components/conversation/ConversationDetail.vue +53 -0
- package/src/components/conversation/ConversationList.vue +51 -0
- package/src/components/conversation/MessageForm.vue +45 -0
- package/src/components/conversation/MessageItem.vue +43 -0
- package/src/components/conversation/MessageList.vue +39 -0
- package/src/components/event/EventCard.vue +82 -0
- package/src/components/event/EventForm.vue +99 -0
- package/src/components/event/EventList.vue +47 -0
- package/src/components/invitation/InvitationCard.vue +56 -0
- package/src/components/invitation/InvitationForm.vue +70 -0
- package/src/components/invitation/InvitationList.vue +51 -0
- package/src/components/joatu/AgreementCard.vue +57 -0
- package/src/components/joatu/AgreementList.vue +51 -0
- package/src/components/joatu/OfferCard.vue +65 -0
- package/src/components/joatu/OfferForm.vue +82 -0
- package/src/components/joatu/OfferList.vue +51 -0
- package/src/components/joatu/RequestCard.vue +65 -0
- package/src/components/joatu/RequestForm.vue +82 -0
- package/src/components/joatu/RequestList.vue +51 -0
- package/src/components/page/PageCard.vue +55 -0
- package/src/components/page/PageDetail.vue +35 -0
- package/src/components/page/PageList.vue +51 -0
- package/src/components/person/MemberList.vue +61 -0
- package/src/components/person/PersonAvatar.vue +54 -0
- package/src/components/person/PersonCard.vue +47 -0
- package/src/components/post/PostCard.vue +105 -0
- package/src/components/post/PostDetail.vue +98 -0
- package/src/components/post/PostForm.vue +84 -0
- package/src/components/post/PostList.vue +53 -0
- package/src/components/role/BlockButton.vue +44 -0
- package/src/components/role/RoleBadge.vue +19 -0
- package/src/components/role/RoleGate.vue +62 -0
- package/src/components/role/RoleSelector.vue +29 -0
- package/src/components/shared/ExtensionSlot.vue +27 -0
- package/src/components/sync/OfflineBanner.vue +49 -0
- package/src/components/sync/SyncBadge.vue +108 -0
- package/src/components/sync/SyncStatusBar.vue +121 -0
- package/src/composables/useCommunities.js +19 -0
- package/src/composables/useConversations.js +5 -0
- package/src/composables/useEvents.js +28 -0
- package/src/composables/useInvitations.js +10 -0
- package/src/composables/useJoaTuAgreements.js +11 -0
- package/src/composables/useJoaTuOffers.js +10 -0
- package/src/composables/useJoaTuRequests.js +10 -0
- package/src/composables/useMembers.js +30 -0
- package/src/composables/useMessages.js +5 -0
- package/src/composables/useNotifications.js +5 -0
- package/src/composables/usePages.js +6 -0
- package/src/composables/usePersonBlocks.js +65 -0
- package/src/composables/usePosts.js +27 -0
- package/src/composables/useResource.js +137 -0
- package/src/composables/useRoles.js +94 -0
- package/src/composables/useSyncStatus.js +22 -0
- package/src/composables/useToaster.js +20 -0
- package/src/context.js +18 -0
- package/src/db/client.js +343 -0
- package/src/db/migrations/001_initial.sql +131 -0
- package/src/db/migrations/003_conversations_invitations_pages_joatu.sql +76 -0
- package/src/db/sync.js +276 -0
- package/src/endpoints/BtApiAuth.js +1 -1
- package/src/endpoints/BtApiV1.js +1 -1
- package/src/extension.js +45 -0
- package/src/i18n/index.js +103 -0
- package/src/i18n/locales/en.json +275 -0
- package/src/i18n/locales/es.json +275 -0
- package/src/i18n/locales/fr.json +223 -0
- package/src/i18n/locales/uk.json +275 -0
- package/src/index.js +168 -22
- package/src/layouts/CommunityLayout.vue +89 -0
- package/src/main.js +16 -12
- package/src/mixins/error-handling.js +6 -15
- package/src/mixins/toaster.js +15 -10
- package/src/pages/Communities.vue +59 -0
- package/src/pages/Error404.vue +10 -14
- package/src/pages/Home.vue +11 -18
- package/src/pages/Me.vue +39 -59
- package/src/pages/UserPasswordNew.vue +12 -68
- package/src/pages/UserPasswordReset.vue +15 -64
- package/src/pages/UserResendConfirmation.vue +39 -113
- package/src/pages/UserSignIn.vue +18 -67
- package/src/pages/UserSignUp.vue +15 -64
- package/src/pages/community/CommunityConversation.vue +31 -0
- package/src/pages/community/CommunityConversations.vue +18 -0
- package/src/pages/community/CommunityEvent.vue +39 -0
- package/src/pages/community/CommunityEvents.vue +58 -0
- package/src/pages/community/CommunityHome.vue +49 -0
- package/src/pages/community/CommunityJoaTu.vue +115 -0
- package/src/pages/community/CommunityMembers.vue +23 -0
- package/src/pages/community/CommunityPage.vue +31 -0
- package/src/pages/community/CommunityPages.vue +25 -0
- package/src/pages/community/CommunityPost.vue +28 -0
- package/src/pages/community/CommunityPosts.vue +58 -0
- package/src/pages/community/CommunitySettingsPage.vue +117 -0
- package/src/pages/community/RoleManagerPage.vue +93 -0
- package/src/plugins/bootstrap-vue.js +5 -5
- package/src/plugins/font-awesome.js +3 -2
- package/src/plugins/index.js +9 -4
- package/src/plugins/progress.js +16 -0
- package/src/pwa/index.js +156 -0
- package/src/pwa/sw-helpers.js +130 -0
- package/src/router/communityRoutes.js +78 -0
- package/src/router/index.js +30 -144
- package/src/slot-registry.js +15 -0
- package/src/stores/auth.js +134 -0
- package/src/stores/communities.js +59 -0
- package/src/stores/index.js +5 -0
- package/src/stores/menus.js +14 -0
- package/src/stores/people.js +48 -0
- package/src/stores/sync.js +93 -0
- package/src/stylesheets/sync-indicators.scss +34 -0
- package/.env.sample +0 -1
- package/.eslintrc.js +0 -51
- package/.gitlab-ci.yml +0 -14
- package/.travis/.rbenv-gemsets +0 -1
- package/.travis/.ruby-version +0 -1
- package/.travis.yml +0 -31
- package/babel.config.js +0 -5
- package/cypress.json +0 -3
- package/deploy/build.sh +0 -8
- package/eslint.config.js +0 -16
- package/postcss.config.js +0 -5
- package/src/eslint.config.js +0 -16
- package/src/forms/BtProfileFormSchema.js +0 -19
- package/src/forms/BtUserConfirmationFormSchema.js +0 -20
- package/src/forms/BtUserNewPasswordFormSchema.js +0 -29
- package/src/forms/BtUserResetPasswordFormSchema.js +0 -20
- package/src/forms/BtUserSignInFormSchema.js +0 -29
- package/src/forms/BtUserSignUpFormSchema.js +0 -63
- package/src/forms/CommunityFormSchema.js +0 -19
- package/src/plugins/vue-form-generator.js +0 -4
- package/src/plugins/vue-loading.js +0 -10
- package/src/registerServiceWorker.js +0 -32
- package/src/store/index.js +0 -32
- package/src/store/modules/authentication.js +0 -170
- package/src/store/modules/communities.js +0 -98
- package/src/store/modules/community-engine.js +0 -14
- package/src/store/modules/menus.js +0 -52
- package/src/store/modules/people.js +0 -88
- package/tests/e2e/.eslintrc.js +0 -12
- package/tests/e2e/plugins/index.js +0 -26
- package/tests/e2e/specs/home.js +0 -8
- package/tests/e2e/support/commands.js +0 -25
- package/tests/e2e/support/index.js +0 -20
- package/tests/unit/.eslintrc.js +0 -5
- package/tests/unit/example.spec.js +0 -13
- package/vue.config.js +0 -11
- package/webpack.config.js +0 -28
- /package/{public → dist}/_redirects +0 -0
- /package/{public → dist}/favicon.ico +0 -0
- /package/{public → dist}/img/favicon.ico +0 -0
- /package/{public → dist}/img/icons/android-chrome-192x192.png +0 -0
- /package/{public → dist}/img/icons/android-chrome-384x384.png +0 -0
- /package/{public → dist}/img/icons/apple-touch-icon.png +0 -0
- /package/{public → dist}/img/icons/favicon-16x16.png +0 -0
- /package/{public → dist}/img/icons/favicon-32x32.png +0 -0
- /package/{public → dist}/img/icons/favicon.ico +0 -0
- /package/{public → dist}/img/icons/mstile-150x150.png +0 -0
- /package/{public → dist}/img/icons/safari-pinned-tab.svg +0 -0
- /package/{public → dist}/robots.txt +0 -0
package/src/router/index.js
CHANGED
|
@@ -1,185 +1,71 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
import { useAuthStore } from '../stores/auth'
|
|
3
3
|
import Home from '../pages/Home.vue'
|
|
4
4
|
import Me from '../pages/Me.vue'
|
|
5
|
+
import { communityRoutes } from './communityRoutes'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// const isAuthenticated = store.getters['CommunityEngine/Authentication/isAuthenticated']
|
|
9
|
-
// const isAdmin = store.getters['Authentication/isAdmin']
|
|
10
|
-
|
|
11
|
-
// const ensureAuthenticated = (to, from, next) => {
|
|
12
|
-
// if (isAuthenticated) {
|
|
13
|
-
// next()
|
|
14
|
-
// } else {
|
|
15
|
-
// next({ name: 'Sign In', query: { redirect_to: to.fullPath } })
|
|
16
|
-
// }
|
|
17
|
-
// }
|
|
18
|
-
|
|
19
|
-
// const ensureAdmin = (to, from, next) => {
|
|
20
|
-
// ensureAuthenticated(to, from, next)
|
|
21
|
-
|
|
22
|
-
// if (isAdmin) {
|
|
23
|
-
// next()
|
|
24
|
-
// } else {
|
|
25
|
-
// next({ name: 'Home' })
|
|
26
|
-
// }
|
|
27
|
-
// }
|
|
28
|
-
|
|
29
|
-
// const documentTitle = 'Better Together'
|
|
30
|
-
|
|
31
|
-
const setPageTitleAndMeta = (to, from, next) => {
|
|
32
|
-
// This goes through the matched routes from last to first,
|
|
33
|
-
// finding the closest route with a title.
|
|
34
|
-
// eg. if we have /some/deep/nested/route and /some, /deep, and
|
|
35
|
-
// /nested have titles, nested's will be chosen.
|
|
7
|
+
const setPageTitleAndMeta = (to) => {
|
|
36
8
|
const nearestWithTitle = to.matched.slice().reverse().find((r) => r.meta && r.meta.title)
|
|
37
|
-
|
|
38
|
-
// Find the nearest route element with meta tags.
|
|
39
9
|
const nearestWithMeta = to.matched.slice().reverse().find((r) => r.meta && r.meta.metaTags)
|
|
40
10
|
|
|
41
|
-
|
|
42
|
-
if (nearestWithTitle) document.title = `${nearestWithTitle.meta.title}`
|
|
11
|
+
if (nearestWithTitle) document.title = nearestWithTitle.meta.title
|
|
43
12
|
|
|
44
|
-
|
|
45
|
-
Array.from(document.querySelectorAll('[data-vue-router-controlled]')).map((el) => el.parentNode.removeChild(el))
|
|
13
|
+
Array.from(document.querySelectorAll('[data-vue-router-controlled]')).forEach((el) => el.parentNode.removeChild(el))
|
|
46
14
|
|
|
47
|
-
|
|
48
|
-
if (!nearestWithMeta) return next()
|
|
15
|
+
if (!nearestWithMeta) return
|
|
49
16
|
|
|
50
|
-
// Turn the meta tag definitions into actual elements in the head.
|
|
51
17
|
nearestWithMeta.meta.metaTags.map((tagDef) => {
|
|
52
18
|
const tag = document.createElement('meta')
|
|
53
|
-
|
|
54
|
-
Object.keys(tagDef).forEach((key) => {
|
|
55
|
-
tag.setAttribute(key, tagDef[key])
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// We use this to track which meta tags we create, so we don't interfere with other ones.
|
|
19
|
+
Object.keys(tagDef).forEach((key) => tag.setAttribute(key, tagDef[key]))
|
|
59
20
|
tag.setAttribute('data-vue-router-controlled', '')
|
|
60
|
-
|
|
61
21
|
return tag
|
|
62
|
-
})
|
|
63
|
-
// Add the meta tags to the document head.
|
|
64
|
-
.forEach((tag) => document.head.appendChild(tag))
|
|
65
|
-
|
|
66
|
-
return next()
|
|
22
|
+
}).forEach((tag) => document.head.appendChild(tag))
|
|
67
23
|
}
|
|
68
24
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const BtRoutes = [
|
|
25
|
+
export const BtRoutes = [
|
|
72
26
|
{
|
|
73
27
|
path: '/',
|
|
74
28
|
name: 'Home',
|
|
75
29
|
component: Home,
|
|
76
|
-
meta: {
|
|
77
|
-
title: 'Home',
|
|
78
|
-
metaTags: [
|
|
79
|
-
{
|
|
80
|
-
name: 'description',
|
|
81
|
-
content: 'The home page of our community.',
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
property: 'og:description',
|
|
85
|
-
content: 'The home page of our community.',
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
},
|
|
30
|
+
meta: { title: 'Home', metaTags: [{ name: 'description', content: 'The home page of our community.' }, { property: 'og:description', content: 'The home page of our community.' }] },
|
|
89
31
|
},
|
|
90
32
|
{
|
|
91
33
|
path: '/me',
|
|
92
34
|
name: 'Me',
|
|
93
35
|
component: Me,
|
|
94
|
-
meta: {
|
|
95
|
-
title: 'Me',
|
|
96
|
-
metaTags: [
|
|
97
|
-
{
|
|
98
|
-
name: 'description',
|
|
99
|
-
content: 'My home page in our community.',
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
property: 'og:description',
|
|
103
|
-
content: 'My home page in our community.',
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
path: '/users/sign-in',
|
|
110
|
-
name: 'Sign In',
|
|
111
|
-
// route level code-splitting
|
|
112
|
-
// this generates a separate chunk (about.[hash].js) for this route
|
|
113
|
-
// which is lazy-loaded when the route is visited.
|
|
114
|
-
component: () => import(/* webpackChunkName: "contact" */ '../pages/UserSignIn.vue'),
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
path: '/users/sign-up',
|
|
118
|
-
name: 'Sign Up',
|
|
119
|
-
// route level code-splitting
|
|
120
|
-
// this generates a separate chunk (about.[hash].js) for this route
|
|
121
|
-
// which is lazy-loaded when the route is visited.
|
|
122
|
-
component: () => import(/* webpackChunkName: "contact" */ '../pages/UserSignUp.vue'),
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
path: '/users/password/reset',
|
|
126
|
-
name: 'Reset Password',
|
|
127
|
-
// route level code-splitting
|
|
128
|
-
// this generates a separate chunk (about.[hash].js) for this route
|
|
129
|
-
// which is lazy-loaded when the route is visited.
|
|
130
|
-
component: () => import(/* webpackChunkName: "contact" */ '../pages/UserPasswordReset.vue'),
|
|
131
|
-
// beforeEnter: ifNotAuthenticated,
|
|
36
|
+
meta: { requiresAuth: true, title: 'Me', metaTags: [{ name: 'description', content: 'My home page in our community.' }] },
|
|
132
37
|
},
|
|
38
|
+
{ path: '/users/sign-in', name: 'Sign In', component: () => import('../pages/UserSignIn.vue') },
|
|
39
|
+
{ path: '/users/sign-up', name: 'Sign Up', component: () => import('../pages/UserSignUp.vue') },
|
|
40
|
+
{ path: '/users/password/reset', name: 'Reset Password', component: () => import('../pages/UserPasswordReset.vue') },
|
|
133
41
|
{
|
|
134
42
|
path: '/users/password/new',
|
|
135
43
|
name: 'New Password',
|
|
136
|
-
|
|
137
|
-
// this generates a separate chunk (about.[hash].js) for this route
|
|
138
|
-
// which is lazy-loaded when the route is visited.
|
|
139
|
-
component: () => import(/* webpackChunkName: "contact" */ '../pages/UserPasswordNew.vue'),
|
|
44
|
+
component: () => import('../pages/UserPasswordNew.vue'),
|
|
140
45
|
props: (route) => ({ resetPasswordToken: route.query.reset_password_token }),
|
|
141
|
-
// beforeEnter: ifNotAuthenticated,
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
path: '/users/confirmation/resend',
|
|
145
|
-
name: 'Resend Account Confirmation',
|
|
146
|
-
// route level code-splitting
|
|
147
|
-
// this generates a separate chunk (about.[hash].js) for this route
|
|
148
|
-
// which is lazy-loaded when the route is visited.
|
|
149
|
-
component: () => import(/* webpackChunkName: "contact" */ '../pages/UserResendConfirmation.vue'),
|
|
150
|
-
// beforeEnter: ifNotAuthenticated,
|
|
151
46
|
},
|
|
47
|
+
{ path: '/users/confirmation/resend', name: 'Resend Account Confirmation', component: () => import('../pages/UserResendConfirmation.vue') },
|
|
152
48
|
{
|
|
153
49
|
path: '/users/confirmation',
|
|
154
50
|
name: 'Account Confirmation',
|
|
155
|
-
|
|
156
|
-
// this generates a separate chunk (about.[hash].js) for this route
|
|
157
|
-
// which is lazy-loaded when the route is visited.
|
|
158
|
-
component: () => import(/* webpackChunkName: "contact" */ '../pages/UserResendConfirmation.vue'),
|
|
51
|
+
component: () => import('../pages/UserResendConfirmation.vue'),
|
|
159
52
|
props: (route) => ({ confirmationToken: route.query.confirmation_token }),
|
|
160
|
-
// beforeEnter: ifNotAuthenticated,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
path: '*',
|
|
164
|
-
name: 'Error404',
|
|
165
|
-
// route level code-splitting
|
|
166
|
-
// this generates a separate chunk (about.[hash].js) for this route
|
|
167
|
-
// which is lazy-loaded when the route is visited.
|
|
168
|
-
component: () => import(/* webpackChunkName: "contact" */ '../pages/Error404.vue'),
|
|
169
53
|
},
|
|
54
|
+
{ path: '/communities', name: 'Communities', component: () => import('../pages/Communities.vue') },
|
|
55
|
+
{ path: '/:pathMatch(.*)*', name: 'Error404', component: () => import('../pages/Error404.vue') },
|
|
170
56
|
]
|
|
171
57
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
mode: 'history',
|
|
176
|
-
base: process.env.BASE_URL,
|
|
177
|
-
routes,
|
|
58
|
+
const BtRouter = createRouter({
|
|
59
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
60
|
+
routes: [...BtRoutes, ...communityRoutes],
|
|
178
61
|
})
|
|
179
62
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
63
|
+
BtRouter.beforeEach((to) => {
|
|
64
|
+
setPageTitleAndMeta(to)
|
|
65
|
+
const auth = useAuthStore()
|
|
66
|
+
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
|
67
|
+
return { name: 'Sign In', query: { redirect_to: to.fullPath } }
|
|
68
|
+
}
|
|
69
|
+
})
|
|
184
70
|
|
|
185
71
|
export default BtRouter
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const _registry = {}
|
|
2
|
+
|
|
3
|
+
export function registerSlotInjection({ target, slot, component, props = {} }) {
|
|
4
|
+
const key = `${target}:${slot}`
|
|
5
|
+
if (!_registry[key]) _registry[key] = []
|
|
6
|
+
_registry[key].push({ component, props })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getSlotInjections(target, slot) {
|
|
10
|
+
return _registry[`${target}:${slot}`] ?? []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function clearSlotRegistry() {
|
|
14
|
+
Object.keys(_registry).forEach((k) => delete _registry[k])
|
|
15
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
import BtApiAuth from '../endpoints/BtApiAuth'
|
|
5
|
+
|
|
6
|
+
const TOKEN_TTL_MS = 24 * 60 * 60 * 1000 // 24h — JWT access token default lifetime
|
|
7
|
+
|
|
8
|
+
export const useAuthStore = defineStore('btAuth', () => {
|
|
9
|
+
const currentUser = ref({})
|
|
10
|
+
const token = ref('')
|
|
11
|
+
const status = ref('')
|
|
12
|
+
const refreshToken = ref(null)
|
|
13
|
+
const tokenIssuedAt = ref(null)
|
|
14
|
+
|
|
15
|
+
const isAuthenticated = computed(() => !!token.value)
|
|
16
|
+
const authStatus = computed(() => status.value)
|
|
17
|
+
const authToken = computed(() => token.value)
|
|
18
|
+
const tokenIsExpired = computed(() =>
|
|
19
|
+
!!(tokenIssuedAt.value &&
|
|
20
|
+
Date.now() - new Date(tokenIssuedAt.value).getTime() > TOKEN_TTL_MS)
|
|
21
|
+
)
|
|
22
|
+
const canSync = computed(() => !!token.value && !tokenIsExpired.value)
|
|
23
|
+
// Local DB is always readable regardless of auth state
|
|
24
|
+
const hasLocalAccess = computed(() => true)
|
|
25
|
+
|
|
26
|
+
function _setAxiosAuth(t) {
|
|
27
|
+
if (t) axios.defaults.headers.common.Authorization = t
|
|
28
|
+
else delete axios.defaults.headers.common.Authorization
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function signIn(params) {
|
|
32
|
+
status.value = 'loading'
|
|
33
|
+
try {
|
|
34
|
+
const { data, headers } = await BtApiAuth.post('sign-in', params)
|
|
35
|
+
token.value = headers.authorization
|
|
36
|
+
currentUser.value = data
|
|
37
|
+
status.value = 'success'
|
|
38
|
+
tokenIssuedAt.value = new Date().toISOString()
|
|
39
|
+
if (headers['x-refresh-token']) refreshToken.value = headers['x-refresh-token']
|
|
40
|
+
else if (data.refresh_token) refreshToken.value = data.refresh_token
|
|
41
|
+
_setAxiosAuth(token.value)
|
|
42
|
+
return data
|
|
43
|
+
} catch (err) {
|
|
44
|
+
status.value = 'error'
|
|
45
|
+
currentUser.value = {}
|
|
46
|
+
token.value = ''
|
|
47
|
+
_setAxiosAuth(null)
|
|
48
|
+
throw err
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function signOut() {
|
|
53
|
+
try {
|
|
54
|
+
const { data } = await BtApiAuth.delete('sign-out')
|
|
55
|
+
return data
|
|
56
|
+
} catch (err) {
|
|
57
|
+
status.value = 'error'
|
|
58
|
+
throw err
|
|
59
|
+
} finally {
|
|
60
|
+
status.value = ''
|
|
61
|
+
currentUser.value = {}
|
|
62
|
+
token.value = ''
|
|
63
|
+
refreshToken.value = null
|
|
64
|
+
tokenIssuedAt.value = null
|
|
65
|
+
_setAxiosAuth(null)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Receptacle for JWT refresh — CE Rails refresh endpoint pending (Deck #955).
|
|
70
|
+
// When implemented: POST /bt/api/auth/refresh with refreshToken,
|
|
71
|
+
// then set token + tokenIssuedAt on success, or dispatch 'auth:needs-reauth' on failure.
|
|
72
|
+
async function refreshTokenIfNeeded() {
|
|
73
|
+
if (canSync.value) return
|
|
74
|
+
if (!refreshToken.value) return
|
|
75
|
+
// eslint-disable-next-line no-console
|
|
76
|
+
console.warn('[btAuth] refreshTokenIfNeeded: refresh endpoint not yet implemented')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function signUp(params) {
|
|
80
|
+
const { data } = await BtApiAuth.post('sign-up', {
|
|
81
|
+
...params,
|
|
82
|
+
confirmation_url: `${window.location.origin}/users/confirmation`,
|
|
83
|
+
})
|
|
84
|
+
return data
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function resendConfirmation(params) {
|
|
88
|
+
const { data } = await BtApiAuth.post('confirmation', {
|
|
89
|
+
...params,
|
|
90
|
+
confirmation_url: `${window.location.origin}/users/confirmation`,
|
|
91
|
+
})
|
|
92
|
+
return data
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function sendConfirmation(params) {
|
|
96
|
+
const { data } = await BtApiAuth.get('confirmation', { params })
|
|
97
|
+
return data
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function resetPassword(params) {
|
|
101
|
+
const { data } = await BtApiAuth.post('password', {
|
|
102
|
+
...params,
|
|
103
|
+
new_password_url: `${window.location.origin}/users/password/new`,
|
|
104
|
+
})
|
|
105
|
+
return data
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function newPassword(params) {
|
|
109
|
+
const { data } = await BtApiAuth.put('password', params)
|
|
110
|
+
return data
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
currentUser,
|
|
115
|
+
token,
|
|
116
|
+
status,
|
|
117
|
+
refreshToken,
|
|
118
|
+
tokenIssuedAt,
|
|
119
|
+
isAuthenticated,
|
|
120
|
+
authStatus,
|
|
121
|
+
authToken,
|
|
122
|
+
tokenIsExpired,
|
|
123
|
+
canSync,
|
|
124
|
+
hasLocalAccess,
|
|
125
|
+
signIn,
|
|
126
|
+
signOut,
|
|
127
|
+
signUp,
|
|
128
|
+
resendConfirmation,
|
|
129
|
+
sendConfirmation,
|
|
130
|
+
resetPassword,
|
|
131
|
+
newPassword,
|
|
132
|
+
refreshTokenIfNeeded,
|
|
133
|
+
}
|
|
134
|
+
}, { persist: true })
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import BtApiV1 from '../endpoints/BtApiV1'
|
|
4
|
+
|
|
5
|
+
const PLATFORM_COMMUNITY = {
|
|
6
|
+
id: 0,
|
|
7
|
+
name: 'Better Together',
|
|
8
|
+
description: 'A community building platform',
|
|
9
|
+
customization: {
|
|
10
|
+
backgroundColor: '#343a40 !important',
|
|
11
|
+
coverImageUrl: '',
|
|
12
|
+
coverImagePositionY: 'center',
|
|
13
|
+
imageUrl: '',
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useCommunityStore = defineStore('btCommunities', () => {
|
|
18
|
+
const communities = ref([])
|
|
19
|
+
const activeCommunity = ref({ ...PLATFORM_COMMUNITY })
|
|
20
|
+
|
|
21
|
+
const customization = computed(() => activeCommunity.value.customization)
|
|
22
|
+
const coverImageUrl = computed(() => activeCommunity.value.customization.coverImageUrl)
|
|
23
|
+
const coverImagePositionY = computed(() => activeCommunity.value.customization.coverImagePositionY)
|
|
24
|
+
|
|
25
|
+
function setCoverImageUrl(url) {
|
|
26
|
+
activeCommunity.value.customization.coverImageUrl = url
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setCustomizationOptions(options) {
|
|
30
|
+
activeCommunity.value.customization = {
|
|
31
|
+
...PLATFORM_COMMUNITY.customization,
|
|
32
|
+
...options,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getCommunities(params) {
|
|
37
|
+
const { data } = await BtApiV1.findAll('communities', { params })
|
|
38
|
+
communities.value = data
|
|
39
|
+
return data
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function postCommunity(params) {
|
|
43
|
+
const { data } = await BtApiV1.create('community', params)
|
|
44
|
+
communities.value.unshift(data)
|
|
45
|
+
return data
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
communities,
|
|
50
|
+
activeCommunity,
|
|
51
|
+
customization,
|
|
52
|
+
coverImageUrl,
|
|
53
|
+
coverImagePositionY,
|
|
54
|
+
setCoverImageUrl,
|
|
55
|
+
setCustomizationOptions,
|
|
56
|
+
getCommunities,
|
|
57
|
+
postCommunity,
|
|
58
|
+
}
|
|
59
|
+
}, { persist: true })
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export const useMenuStore = defineStore('btMenus', () => {
|
|
5
|
+
const headerMenuItems = ref([
|
|
6
|
+
{ id: 0, path: '/', label: 'Home', title: 'Home', url: '#', external: false, sortOrder: 0 },
|
|
7
|
+
])
|
|
8
|
+
|
|
9
|
+
function setHeaderMenuItems(items) {
|
|
10
|
+
headerMenuItems.value = items
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return { headerMenuItems, setHeaderMenuItems }
|
|
14
|
+
}, { persist: false })
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
|
|
5
|
+
export const usePeopleStore = defineStore('btPeople', () => {
|
|
6
|
+
const currentPerson = ref({})
|
|
7
|
+
const me = ref({})
|
|
8
|
+
|
|
9
|
+
const hasCurrentPerson = computed(() => Object.keys(currentPerson.value).length > 0)
|
|
10
|
+
const hasMe = computed(() => Object.keys(me.value).length > 0)
|
|
11
|
+
const currentPersonChanged = computed(
|
|
12
|
+
() => JSON.stringify(currentPerson.value) !== JSON.stringify(me.value),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
async function getMe() {
|
|
16
|
+
const response = await axios.get(
|
|
17
|
+
`${import.meta.env.VITE_BETTER_TOGETHER_API_URI}/api/v1/people/me`,
|
|
18
|
+
)
|
|
19
|
+
const person = response.status === 200 ? response.data : {}
|
|
20
|
+
currentPerson.value = { ...person }
|
|
21
|
+
me.value = { ...person }
|
|
22
|
+
return response
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function postPerson(params) {
|
|
26
|
+
const { data } = await axios.post(
|
|
27
|
+
`${import.meta.env.VITE_BETTER_TOGETHER_API_URI}/api/v1/people/me`,
|
|
28
|
+
params,
|
|
29
|
+
)
|
|
30
|
+
currentPerson.value = data
|
|
31
|
+
return data
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clearCurrentPerson() {
|
|
35
|
+
currentPerson.value = {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
currentPerson,
|
|
40
|
+
me,
|
|
41
|
+
hasCurrentPerson,
|
|
42
|
+
hasMe,
|
|
43
|
+
currentPersonChanged,
|
|
44
|
+
getMe,
|
|
45
|
+
postPerson,
|
|
46
|
+
clearCurrentPerson,
|
|
47
|
+
}
|
|
48
|
+
}, { persist: true })
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { getDb } from '../db/client'
|
|
4
|
+
|
|
5
|
+
// All tables that carry _sync_status (excludes _schema_versions)
|
|
6
|
+
const SYNC_TABLES = [
|
|
7
|
+
'people', 'communities', 'posts', 'events',
|
|
8
|
+
'conversations', 'messages', 'notifications',
|
|
9
|
+
'navigation_areas', 'navigation_items',
|
|
10
|
+
'person_community_memberships', 'uploads',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export const useSyncStore = defineStore('btSync', () => {
|
|
14
|
+
const online = ref(navigator.onLine)
|
|
15
|
+
const pendingCount = ref(0)
|
|
16
|
+
const syncing = ref(false)
|
|
17
|
+
const electricConnected = ref(false)
|
|
18
|
+
|
|
19
|
+
const statusLabel = computed(() => {
|
|
20
|
+
if (!online.value) return 'offline'
|
|
21
|
+
if (syncing.value || pendingCount.value > 0) return 'syncing'
|
|
22
|
+
return 'synced'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function setOnline(val) {
|
|
26
|
+
online.value = val
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setPendingCount(n) {
|
|
30
|
+
pendingCount.value = n
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setSyncing(val) {
|
|
34
|
+
syncing.value = val
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setConnected(val) {
|
|
38
|
+
electricConnected.value = !!val
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function markPendingAsNeedsAuth() {
|
|
42
|
+
const db = await getDb()
|
|
43
|
+
for (const table of SYNC_TABLES) {
|
|
44
|
+
try {
|
|
45
|
+
await db.exec(
|
|
46
|
+
`UPDATE ${table} SET _sync_status = 'needs-auth' WHERE _sync_status = 'local'`
|
|
47
|
+
)
|
|
48
|
+
} catch { /* table may not exist yet */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function drainSyncQueue() {
|
|
53
|
+
// Placeholder — full sync requires ElectricSQL shapes (pending CE Rails sidecar).
|
|
54
|
+
// Counts pending items across all tables and updates pendingCount for UI feedback.
|
|
55
|
+
if (!online.value) return
|
|
56
|
+
setSyncing(true)
|
|
57
|
+
const db = await getDb()
|
|
58
|
+
let total = 0
|
|
59
|
+
for (const table of SYNC_TABLES) {
|
|
60
|
+
try {
|
|
61
|
+
const { rows } = await db.query(
|
|
62
|
+
`SELECT COUNT(*) AS n FROM ${table} WHERE _sync_status IN ('local', 'needs-auth')`
|
|
63
|
+
)
|
|
64
|
+
total += rows[0]?.n ?? 0
|
|
65
|
+
} catch { /* table may not exist yet */ }
|
|
66
|
+
}
|
|
67
|
+
setPendingCount(total)
|
|
68
|
+
setSyncing(false)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function initNetworkListeners() {
|
|
72
|
+
window.addEventListener('online', async () => {
|
|
73
|
+
online.value = true
|
|
74
|
+
await drainSyncQueue()
|
|
75
|
+
})
|
|
76
|
+
window.addEventListener('offline', () => { online.value = false })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
online,
|
|
81
|
+
pendingCount,
|
|
82
|
+
syncing,
|
|
83
|
+
electricConnected,
|
|
84
|
+
statusLabel,
|
|
85
|
+
setOnline,
|
|
86
|
+
setPendingCount,
|
|
87
|
+
setSyncing,
|
|
88
|
+
setConnected,
|
|
89
|
+
markPendingAsNeedsAuth,
|
|
90
|
+
drainSyncQueue,
|
|
91
|
+
initNetworkListeners,
|
|
92
|
+
}
|
|
93
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Sync status colour tokens
|
|
2
|
+
$sync-local: #f59e0b; // amber — local only, not yet synced
|
|
3
|
+
$sync-syncing: #3b82f6; // blue — in flight
|
|
4
|
+
$sync-synced: #10b981; // green — confirmed on server
|
|
5
|
+
$sync-conflict: #ef4444; // red — conflict detected
|
|
6
|
+
$sync-needs-auth: #f59e0b; // amber — needs sign-in to sync (same family as local)
|
|
7
|
+
|
|
8
|
+
// Sync badge sizes
|
|
9
|
+
$sync-badge-size: 8px;
|
|
10
|
+
$sync-badge-size-lg: 12px;
|
|
11
|
+
|
|
12
|
+
// App-wide sync bar
|
|
13
|
+
$sync-bar-height: 3px;
|
|
14
|
+
$sync-bar-height-offline: 28px;
|
|
15
|
+
|
|
16
|
+
// Animation
|
|
17
|
+
@keyframes sync-spin {
|
|
18
|
+
from { transform: rotate(0deg); }
|
|
19
|
+
to { transform: rotate(360deg); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@keyframes sync-pulse {
|
|
23
|
+
0%, 100% { opacity: 1; }
|
|
24
|
+
50% { opacity: 0.5; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Mixin for status dot
|
|
28
|
+
@mixin sync-dot($color) {
|
|
29
|
+
width: $sync-badge-size;
|
|
30
|
+
height: $sync-badge-size;
|
|
31
|
+
border-radius: 50%;
|
|
32
|
+
background-color: $color;
|
|
33
|
+
display: inline-block;
|
|
34
|
+
}
|
package/.env.sample
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VUE_APP_BETTER_TOGETHER_API_URI=http://localhost:3000
|
package/.eslintrc.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
env: {
|
|
3
|
-
browser: true,
|
|
4
|
-
es6: true,
|
|
5
|
-
},
|
|
6
|
-
extends: [
|
|
7
|
-
'eslint:recommended',
|
|
8
|
-
// 'plugin:vue/essential',
|
|
9
|
-
'airbnb-base',
|
|
10
|
-
'plugin:vue/recommended',
|
|
11
|
-
],
|
|
12
|
-
globals: {
|
|
13
|
-
Atomics: 'readonly',
|
|
14
|
-
SharedArrayBuffer: 'readonly',
|
|
15
|
-
},
|
|
16
|
-
parserOptions: {
|
|
17
|
-
ecmaVersion: 2018,
|
|
18
|
-
sourceType: 'module',
|
|
19
|
-
parser: 'babel-eslint',
|
|
20
|
-
},
|
|
21
|
-
plugins: [
|
|
22
|
-
'vue',
|
|
23
|
-
],
|
|
24
|
-
rules: {
|
|
25
|
-
indent: [
|
|
26
|
-
'error',
|
|
27
|
-
2,
|
|
28
|
-
],
|
|
29
|
-
'linebreak-style': [
|
|
30
|
-
'error',
|
|
31
|
-
'unix',
|
|
32
|
-
],
|
|
33
|
-
quotes: [
|
|
34
|
-
'error',
|
|
35
|
-
'single',
|
|
36
|
-
],
|
|
37
|
-
semi: [
|
|
38
|
-
'error',
|
|
39
|
-
'never',
|
|
40
|
-
],
|
|
41
|
-
'no-param-reassign': [
|
|
42
|
-
'error',
|
|
43
|
-
{
|
|
44
|
-
props: true,
|
|
45
|
-
ignorePropertyModificationsFor: [
|
|
46
|
-
'currentState',
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
}
|