@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.
Files changed (229) 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 +34 -84
  55. package/src/components/BtMainContent.vue +12 -43
  56. package/src/components/BtNavBar.vue +25 -38
  57. package/src/components/BtNavItem.vue +25 -58
  58. package/src/components/BtNavUser.vue +65 -86
  59. package/src/components/BtProfileForm.vue +48 -39
  60. package/src/components/BtUserNewPasswordForm.vue +52 -74
  61. package/src/components/BtUserResendConfirmationForm.vue +45 -83
  62. package/src/components/BtUserResetPasswordForm.vue +45 -77
  63. package/src/components/BtUserSignInForm.vue +59 -75
  64. package/src/components/BtUserSignUpForm.vue +90 -103
  65. package/src/components/CommunityForm.vue +47 -39
  66. package/src/components/community/CommunityCard.vue +113 -0
  67. package/src/components/community/CommunityHeader.vue +91 -0
  68. package/src/components/community/CommunityList.vue +59 -0
  69. package/src/components/community/MemberRoleRow.vue +107 -0
  70. package/src/components/conversation/ConversationCard.vue +49 -0
  71. package/src/components/conversation/ConversationDetail.vue +53 -0
  72. package/src/components/conversation/ConversationList.vue +51 -0
  73. package/src/components/conversation/MessageForm.vue +45 -0
  74. package/src/components/conversation/MessageItem.vue +43 -0
  75. package/src/components/conversation/MessageList.vue +39 -0
  76. package/src/components/event/EventCard.vue +82 -0
  77. package/src/components/event/EventForm.vue +99 -0
  78. package/src/components/event/EventList.vue +47 -0
  79. package/src/components/invitation/InvitationCard.vue +56 -0
  80. package/src/components/invitation/InvitationForm.vue +70 -0
  81. package/src/components/invitation/InvitationList.vue +51 -0
  82. package/src/components/joatu/AgreementCard.vue +57 -0
  83. package/src/components/joatu/AgreementList.vue +51 -0
  84. package/src/components/joatu/OfferCard.vue +65 -0
  85. package/src/components/joatu/OfferForm.vue +82 -0
  86. package/src/components/joatu/OfferList.vue +51 -0
  87. package/src/components/joatu/RequestCard.vue +65 -0
  88. package/src/components/joatu/RequestForm.vue +82 -0
  89. package/src/components/joatu/RequestList.vue +51 -0
  90. package/src/components/page/PageCard.vue +55 -0
  91. package/src/components/page/PageDetail.vue +35 -0
  92. package/src/components/page/PageList.vue +51 -0
  93. package/src/components/person/MemberList.vue +61 -0
  94. package/src/components/person/PersonAvatar.vue +54 -0
  95. package/src/components/person/PersonCard.vue +47 -0
  96. package/src/components/post/PostCard.vue +105 -0
  97. package/src/components/post/PostDetail.vue +98 -0
  98. package/src/components/post/PostForm.vue +84 -0
  99. package/src/components/post/PostList.vue +53 -0
  100. package/src/components/role/BlockButton.vue +44 -0
  101. package/src/components/role/RoleBadge.vue +19 -0
  102. package/src/components/role/RoleGate.vue +62 -0
  103. package/src/components/role/RoleSelector.vue +29 -0
  104. package/src/components/shared/ExtensionSlot.vue +27 -0
  105. package/src/components/sync/OfflineBanner.vue +49 -0
  106. package/src/components/sync/SyncBadge.vue +108 -0
  107. package/src/components/sync/SyncStatusBar.vue +121 -0
  108. package/src/composables/useCommunities.js +19 -0
  109. package/src/composables/useConversations.js +5 -0
  110. package/src/composables/useEvents.js +28 -0
  111. package/src/composables/useInvitations.js +10 -0
  112. package/src/composables/useJoaTuAgreements.js +11 -0
  113. package/src/composables/useJoaTuOffers.js +10 -0
  114. package/src/composables/useJoaTuRequests.js +10 -0
  115. package/src/composables/useMembers.js +30 -0
  116. package/src/composables/useMessages.js +5 -0
  117. package/src/composables/useNotifications.js +5 -0
  118. package/src/composables/usePages.js +6 -0
  119. package/src/composables/usePersonBlocks.js +65 -0
  120. package/src/composables/usePosts.js +27 -0
  121. package/src/composables/useResource.js +137 -0
  122. package/src/composables/useRoles.js +94 -0
  123. package/src/composables/useSyncStatus.js +22 -0
  124. package/src/composables/useToaster.js +20 -0
  125. package/src/context.js +18 -0
  126. package/src/db/client.js +343 -0
  127. package/src/db/migrations/001_initial.sql +131 -0
  128. package/src/db/migrations/003_conversations_invitations_pages_joatu.sql +76 -0
  129. package/src/db/sync.js +276 -0
  130. package/src/endpoints/BtApiAuth.js +1 -1
  131. package/src/endpoints/BtApiV1.js +1 -1
  132. package/src/extension.js +45 -0
  133. package/src/i18n/index.js +103 -0
  134. package/src/i18n/locales/en.json +275 -0
  135. package/src/i18n/locales/es.json +275 -0
  136. package/src/i18n/locales/fr.json +223 -0
  137. package/src/i18n/locales/uk.json +275 -0
  138. package/src/index.js +168 -22
  139. package/src/layouts/CommunityLayout.vue +89 -0
  140. package/src/main.js +16 -12
  141. package/src/mixins/error-handling.js +6 -15
  142. package/src/mixins/toaster.js +15 -10
  143. package/src/pages/Communities.vue +59 -0
  144. package/src/pages/Error404.vue +10 -14
  145. package/src/pages/Home.vue +11 -18
  146. package/src/pages/Me.vue +39 -59
  147. package/src/pages/UserPasswordNew.vue +12 -68
  148. package/src/pages/UserPasswordReset.vue +15 -64
  149. package/src/pages/UserResendConfirmation.vue +39 -113
  150. package/src/pages/UserSignIn.vue +18 -67
  151. package/src/pages/UserSignUp.vue +15 -64
  152. package/src/pages/community/CommunityConversation.vue +31 -0
  153. package/src/pages/community/CommunityConversations.vue +18 -0
  154. package/src/pages/community/CommunityEvent.vue +39 -0
  155. package/src/pages/community/CommunityEvents.vue +58 -0
  156. package/src/pages/community/CommunityHome.vue +49 -0
  157. package/src/pages/community/CommunityJoaTu.vue +115 -0
  158. package/src/pages/community/CommunityMembers.vue +23 -0
  159. package/src/pages/community/CommunityPage.vue +31 -0
  160. package/src/pages/community/CommunityPages.vue +25 -0
  161. package/src/pages/community/CommunityPost.vue +28 -0
  162. package/src/pages/community/CommunityPosts.vue +58 -0
  163. package/src/pages/community/CommunitySettingsPage.vue +117 -0
  164. package/src/pages/community/RoleManagerPage.vue +93 -0
  165. package/src/plugins/bootstrap-vue.js +5 -5
  166. package/src/plugins/font-awesome.js +3 -2
  167. package/src/plugins/index.js +9 -4
  168. package/src/plugins/progress.js +16 -0
  169. package/src/pwa/index.js +156 -0
  170. package/src/pwa/sw-helpers.js +130 -0
  171. package/src/router/communityRoutes.js +78 -0
  172. package/src/router/index.js +30 -144
  173. package/src/slot-registry.js +15 -0
  174. package/src/stores/auth.js +134 -0
  175. package/src/stores/communities.js +59 -0
  176. package/src/stores/index.js +5 -0
  177. package/src/stores/menus.js +14 -0
  178. package/src/stores/people.js +48 -0
  179. package/src/stores/sync.js +93 -0
  180. package/src/stylesheets/sync-indicators.scss +34 -0
  181. package/.env.sample +0 -1
  182. package/.eslintrc.js +0 -51
  183. package/.gitlab-ci.yml +0 -14
  184. package/.travis/.rbenv-gemsets +0 -1
  185. package/.travis/.ruby-version +0 -1
  186. package/.travis.yml +0 -31
  187. package/babel.config.js +0 -5
  188. package/cypress.json +0 -3
  189. package/deploy/build.sh +0 -8
  190. package/eslint.config.js +0 -16
  191. package/postcss.config.js +0 -5
  192. package/src/eslint.config.js +0 -16
  193. package/src/forms/BtProfileFormSchema.js +0 -19
  194. package/src/forms/BtUserConfirmationFormSchema.js +0 -20
  195. package/src/forms/BtUserNewPasswordFormSchema.js +0 -29
  196. package/src/forms/BtUserResetPasswordFormSchema.js +0 -20
  197. package/src/forms/BtUserSignInFormSchema.js +0 -29
  198. package/src/forms/BtUserSignUpFormSchema.js +0 -63
  199. package/src/forms/CommunityFormSchema.js +0 -19
  200. package/src/plugins/vue-form-generator.js +0 -4
  201. package/src/plugins/vue-loading.js +0 -10
  202. package/src/registerServiceWorker.js +0 -32
  203. package/src/store/index.js +0 -32
  204. package/src/store/modules/authentication.js +0 -170
  205. package/src/store/modules/communities.js +0 -98
  206. package/src/store/modules/community-engine.js +0 -14
  207. package/src/store/modules/menus.js +0 -52
  208. package/src/store/modules/people.js +0 -88
  209. package/tests/e2e/.eslintrc.js +0 -12
  210. package/tests/e2e/plugins/index.js +0 -26
  211. package/tests/e2e/specs/home.js +0 -8
  212. package/tests/e2e/support/commands.js +0 -25
  213. package/tests/e2e/support/index.js +0 -20
  214. package/tests/unit/.eslintrc.js +0 -5
  215. package/tests/unit/example.spec.js +0 -13
  216. package/vue.config.js +0 -11
  217. package/webpack.config.js +0 -28
  218. /package/{public → dist}/_redirects +0 -0
  219. /package/{public → dist}/favicon.ico +0 -0
  220. /package/{public → dist}/img/favicon.ico +0 -0
  221. /package/{public → dist}/img/icons/android-chrome-192x192.png +0 -0
  222. /package/{public → dist}/img/icons/android-chrome-384x384.png +0 -0
  223. /package/{public → dist}/img/icons/apple-touch-icon.png +0 -0
  224. /package/{public → dist}/img/icons/favicon-16x16.png +0 -0
  225. /package/{public → dist}/img/icons/favicon-32x32.png +0 -0
  226. /package/{public → dist}/img/icons/favicon.ico +0 -0
  227. /package/{public → dist}/img/icons/mstile-150x150.png +0 -0
  228. /package/{public → dist}/img/icons/safari-pinned-tab.svg +0 -0
  229. /package/{public → dist}/robots.txt +0 -0
@@ -1,185 +1,71 @@
1
- import Vue from 'vue'
2
- import VueRouter from 'vue-router'
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
- // import store from '../store'
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
- // If a route with a title was found, set the document (page) title to that value.
42
- if (nearestWithTitle) document.title = `${nearestWithTitle.meta.title}`
11
+ if (nearestWithTitle) document.title = nearestWithTitle.meta.title
43
12
 
44
- // Remove any stale meta tags from the document using the key attribute we set below.
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
- // Skip rendering meta tags if there are none.
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
- Vue.use(VueRouter)
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
- // route level code-splitting
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
- // route level code-splitting
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 routes = BtRoutes
173
-
174
- const BtRouter = new VueRouter({
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
- // This callback runs before every route change, including on page load.
181
- BtRouter.beforeEach(setPageTitleAndMeta)
182
-
183
- export { BtRoutes }
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,5 @@
1
+ export { useAuthStore } from './auth'
2
+ export { useCommunityStore } from './communities'
3
+ export { useMenuStore } from './menus'
4
+ export { usePeopleStore } from './people'
5
+ export { useSyncStore } from './sync'
@@ -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
- }