@bettertogether/community-engine-vue 0.1.7 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +140 -1
  2. package/dist/assets/BBadge.vue_vue_type_script_setup_true_lang-IIZ8QpjG-Z9WDKHqT.js +1 -0
  3. package/dist/assets/BCardText.vue_vue_type_script_setup_true_lang-Be6CD36N-B5JCTdmm.js +3 -0
  4. package/dist/assets/BFormSelect.vue_vue_type_script_setup_true_lang-BigptVap-B_HbOOZR.js +1 -0
  5. package/dist/assets/BRow.vue_vue_type_script_setup_true_lang-69TY75-8-DJdEdyx7.js +1 -0
  6. package/dist/assets/Communities-Cx4tT-bx.js +1 -0
  7. package/dist/assets/Communities-n33ssuUH.css +1 -0
  8. package/dist/assets/CommunityConversation-bBkYBs2k.css +1 -0
  9. package/dist/assets/CommunityConversation-jHAnv_Ps.js +1 -0
  10. package/dist/assets/CommunityConversations-rEDGS7To.js +1 -0
  11. package/dist/assets/CommunityEvent-CUdT0aT4.js +1 -0
  12. package/dist/assets/CommunityEvents-rsOgcxQr.js +1 -0
  13. package/dist/assets/CommunityHome-ChuTE2Nz.js +1 -0
  14. package/dist/assets/CommunityJoaTu-CpLIY_83.js +1 -0
  15. package/dist/assets/CommunityMembers-C3UtzQGp.css +1 -0
  16. package/dist/assets/CommunityMembers-DKf74ltl.js +1 -0
  17. package/dist/assets/CommunityPage-C5x23iQl.css +1 -0
  18. package/dist/assets/CommunityPage-CRYg9-rW.js +1 -0
  19. package/dist/assets/CommunityPages-IsLTNFC3.js +1 -0
  20. package/dist/assets/CommunityPost-BOnqqxVs.js +1 -0
  21. package/dist/assets/CommunityPost-BRYtkDSY.css +1 -0
  22. package/dist/assets/CommunityPosts-DY1olmcU.js +1 -0
  23. package/dist/assets/Error404-D10VQARe.js +1 -0
  24. package/dist/assets/EventCard-vfutXTdg.js +1 -0
  25. package/dist/assets/EventList-ChtehYcJ.js +1 -0
  26. package/dist/assets/ExtensionSlot-DJKbrq4c.js +1 -0
  27. package/dist/assets/PostList-BuHrBBqX.css +1 -0
  28. package/dist/assets/PostList-DYFgxlE8.js +1 -0
  29. package/dist/assets/SyncBadge-B1JBsdUk.js +1 -0
  30. package/dist/assets/SyncBadge-FNO-QLuu.css +1 -0
  31. package/dist/assets/UserPasswordNew-D_Djldm9.css +1 -0
  32. package/dist/assets/UserPasswordNew-al9bNBTZ.js +1 -0
  33. package/dist/assets/UserPasswordReset-42zs98RW.js +1 -0
  34. package/dist/assets/UserPasswordReset-D_6OQDZY.css +1 -0
  35. package/dist/assets/UserResendConfirmation-CGavYB81.js +1 -0
  36. package/dist/assets/UserResendConfirmation-DNTHcaar.css +1 -0
  37. package/dist/assets/UserSignIn-BIRb0HkV.js +1 -0
  38. package/dist/assets/UserSignIn-C-Pol8OD.css +1 -0
  39. package/dist/assets/UserSignUp-ChkKQAd2.css +1 -0
  40. package/dist/assets/UserSignUp-Df6o3vlO.js +1 -0
  41. package/dist/assets/better-together-logo-61cxo5d5.png +0 -0
  42. package/dist/assets/index-BFt-JKVh.css +5 -0
  43. package/dist/assets/index-COo3Jb7v.js +1088 -0
  44. package/dist/assets/nodefs-Bfyh92qg.js +1 -0
  45. package/dist/assets/opfs-ahp-BLzlXf6u.js +3 -0
  46. package/dist/assets/pglite-BdRI_ZYT.wasm +0 -0
  47. package/dist/assets/pglite-COscPi1Y.data +0 -0
  48. package/dist/assets/usePages-DDjDQRCy.js +1 -0
  49. package/dist/assets/usePosts-Bf2Ccwr4.js +1 -0
  50. package/{public → dist}/index.html +9 -19
  51. package/package.json +57 -45
  52. package/src/BtApp.vue +34 -43
  53. package/src/components/BtBrandingLogo.vue +10 -18
  54. package/src/components/BtHeader.vue +31 -89
  55. package/src/components/BtMainContent.vue +12 -43
  56. package/src/components/BtNavBar.vue +25 -38
  57. package/src/components/BtNavItem.vue +25 -58
  58. package/src/components/BtNavUser.vue +65 -86
  59. package/src/components/BtProfileForm.vue +48 -39
  60. package/src/components/BtUserNewPasswordForm.vue +52 -74
  61. package/src/components/BtUserResendConfirmationForm.vue +45 -83
  62. package/src/components/BtUserResetPasswordForm.vue +45 -77
  63. package/src/components/BtUserSignInForm.vue +59 -75
  64. package/src/components/BtUserSignUpForm.vue +90 -103
  65. package/src/components/CommunityForm.vue +47 -39
  66. package/src/components/community/CommunityCard.vue +113 -0
  67. package/src/components/community/CommunityHeader.vue +91 -0
  68. package/src/components/community/CommunityList.vue +59 -0
  69. package/src/components/community/MemberRoleRow.vue +107 -0
  70. package/src/components/conversation/ConversationCard.vue +49 -0
  71. package/src/components/conversation/ConversationDetail.vue +53 -0
  72. package/src/components/conversation/ConversationList.vue +51 -0
  73. package/src/components/conversation/MessageForm.vue +45 -0
  74. package/src/components/conversation/MessageItem.vue +43 -0
  75. package/src/components/conversation/MessageList.vue +39 -0
  76. package/src/components/event/EventCard.vue +82 -0
  77. package/src/components/event/EventForm.vue +99 -0
  78. package/src/components/event/EventList.vue +47 -0
  79. package/src/components/invitation/InvitationCard.vue +56 -0
  80. package/src/components/invitation/InvitationForm.vue +70 -0
  81. package/src/components/invitation/InvitationList.vue +51 -0
  82. package/src/components/joatu/AgreementCard.vue +57 -0
  83. package/src/components/joatu/AgreementList.vue +51 -0
  84. package/src/components/joatu/OfferCard.vue +65 -0
  85. package/src/components/joatu/OfferForm.vue +82 -0
  86. package/src/components/joatu/OfferList.vue +51 -0
  87. package/src/components/joatu/RequestCard.vue +65 -0
  88. package/src/components/joatu/RequestForm.vue +82 -0
  89. package/src/components/joatu/RequestList.vue +51 -0
  90. package/src/components/page/PageCard.vue +55 -0
  91. package/src/components/page/PageDetail.vue +35 -0
  92. package/src/components/page/PageList.vue +51 -0
  93. package/src/components/person/MemberList.vue +61 -0
  94. package/src/components/person/PersonAvatar.vue +54 -0
  95. package/src/components/person/PersonCard.vue +47 -0
  96. package/src/components/post/PostCard.vue +105 -0
  97. package/src/components/post/PostDetail.vue +98 -0
  98. package/src/components/post/PostForm.vue +84 -0
  99. package/src/components/post/PostList.vue +53 -0
  100. package/src/components/role/BlockButton.vue +44 -0
  101. package/src/components/role/RoleBadge.vue +19 -0
  102. package/src/components/role/RoleGate.vue +62 -0
  103. package/src/components/role/RoleSelector.vue +29 -0
  104. package/src/components/shared/ExtensionSlot.vue +27 -0
  105. package/src/components/sync/OfflineBanner.vue +49 -0
  106. package/src/components/sync/SyncBadge.vue +108 -0
  107. package/src/components/sync/SyncStatusBar.vue +121 -0
  108. package/src/composables/useCommunities.js +19 -0
  109. package/src/composables/useConversations.js +5 -0
  110. package/src/composables/useEvents.js +28 -0
  111. package/src/composables/useInvitations.js +10 -0
  112. package/src/composables/useJoaTuAgreements.js +11 -0
  113. package/src/composables/useJoaTuOffers.js +10 -0
  114. package/src/composables/useJoaTuRequests.js +10 -0
  115. package/src/composables/useMembers.js +30 -0
  116. package/src/composables/useMessages.js +5 -0
  117. package/src/composables/useNotifications.js +5 -0
  118. package/src/composables/usePages.js +6 -0
  119. package/src/composables/usePersonBlocks.js +65 -0
  120. package/src/composables/usePosts.js +27 -0
  121. package/src/composables/useResource.js +137 -0
  122. package/src/composables/useRoles.js +94 -0
  123. package/src/composables/useSyncStatus.js +22 -0
  124. package/src/composables/useToaster.js +20 -0
  125. package/src/context.js +18 -0
  126. package/src/db/client.js +343 -0
  127. package/src/db/migrations/001_initial.sql +131 -0
  128. package/src/db/migrations/003_conversations_invitations_pages_joatu.sql +76 -0
  129. package/src/db/sync.js +276 -0
  130. package/src/endpoints/BtApiAuth.js +1 -1
  131. package/src/endpoints/BtApiV1.js +1 -1
  132. package/src/extension.js +45 -0
  133. package/src/i18n/index.js +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/src/vue.config.js +0 -0
  210. package/tests/e2e/.eslintrc.js +0 -12
  211. package/tests/e2e/plugins/index.js +0 -26
  212. package/tests/e2e/specs/home.js +0 -8
  213. package/tests/e2e/support/commands.js +0 -25
  214. package/tests/e2e/support/index.js +0 -20
  215. package/tests/unit/.eslintrc.js +0 -5
  216. package/tests/unit/example.spec.js +0 -13
  217. package/vue.config.js +0 -11
  218. package/webpack.config.js +0 -28
  219. /package/{public → dist}/_redirects +0 -0
  220. /package/{public → dist}/favicon.ico +0 -0
  221. /package/{public → dist}/img/favicon.ico +0 -0
  222. /package/{public → dist}/img/icons/android-chrome-192x192.png +0 -0
  223. /package/{public → dist}/img/icons/android-chrome-384x384.png +0 -0
  224. /package/{public → dist}/img/icons/apple-touch-icon.png +0 -0
  225. /package/{public → dist}/img/icons/favicon-16x16.png +0 -0
  226. /package/{public → dist}/img/icons/favicon-32x32.png +0 -0
  227. /package/{public → dist}/img/icons/favicon.ico +0 -0
  228. /package/{public → dist}/img/icons/mstile-150x150.png +0 -0
  229. /package/{public → dist}/img/icons/safari-pinned-tab.svg +0 -0
  230. /package/{public → dist}/robots.txt +0 -0
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <div class="bt-message-item d-flex gap-2 mb-2">
3
+ <div class="bt-message-item__avatar">
4
+ <div class="bt-message-item__avatar-circle">
5
+ {{ initials }}
6
+ </div>
7
+ </div>
8
+ <div class="bt-message-item__body flex-grow-1">
9
+ <div class="d-flex align-items-center gap-2 mb-1">
10
+ <span class="bt-message-item__author fw-semibold">{{ message.author_name || t('bt.messages.author_unknown') }}</span>
11
+ <small class="text-muted">{{ formattedTime }}</small>
12
+ <SyncBadge :item="message" />
13
+ </div>
14
+ <div class="bt-message-item__content">
15
+ {{ message.content }}
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </template>
20
+ <script setup>
21
+ import { computed } from 'vue'
22
+ import { useI18n } from 'vue-i18n'
23
+ import SyncBadge from '../sync/SyncBadge.vue'
24
+ const { t } = useI18n()
25
+ const props = defineProps({ message: { type: Object, required: true } })
26
+ const initials = computed(() => {
27
+ const name = props.message.author_name || '?'
28
+ return name.split(' ').map((n) => n[0]).slice(0, 2).join('').toUpperCase()
29
+ })
30
+ const formattedTime = computed(() => {
31
+ if (!props.message._local_updated) return ''
32
+ return new Date(props.message._local_updated).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
33
+ })
34
+ </script>
35
+ <style scoped lang="scss">
36
+ .bt-message-item__avatar-circle {
37
+ width: 32px; height: 32px; border-radius: 50%;
38
+ background: #6c757d; color: #fff;
39
+ display: flex; align-items: center; justify-content: center;
40
+ font-size: 0.75rem; font-weight: 600;
41
+ }
42
+ .bt-message-item__content { font-size: 0.9rem; }
43
+ </style>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div class="bt-message-list">
3
+ <div
4
+ v-if="loading"
5
+ class="text-center p-3 text-muted"
6
+ >
7
+ <slot name="loading">
8
+ {{ t('bt.messages.loading') }}
9
+ </slot>
10
+ </div>
11
+ <div
12
+ v-else-if="!messages.length"
13
+ class="text-center p-3 text-muted"
14
+ >
15
+ <slot name="empty">
16
+ {{ t('bt.messages.list_empty') }}
17
+ </slot>
18
+ </div>
19
+ <template v-else>
20
+ <MessageItem
21
+ v-for="message in messages"
22
+ :key="message.id"
23
+ :message="message"
24
+ />
25
+ </template>
26
+ </div>
27
+ </template>
28
+ <script setup>
29
+ import { useI18n } from 'vue-i18n'
30
+ import MessageItem from './MessageItem.vue'
31
+ const { t } = useI18n()
32
+ defineProps({
33
+ messages: { type: Array, default: () => [] },
34
+ loading: { type: Boolean, default: false },
35
+ })
36
+ </script>
37
+ <style scoped lang="scss">
38
+ .bt-message-list { max-height: 400px; overflow-y: auto; padding: 1rem; border: 1px solid #dee2e6; border-radius: 0.5rem; }
39
+ </style>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <BCard class="bt-event-card">
3
+ <BCardBody>
4
+ <slot name="title">
5
+ <div class="d-flex align-items-start justify-content-between gap-2 mb-1">
6
+ <h5 class="bt-event-card__name mb-0">
7
+ {{ event.name }}
8
+ </h5>
9
+ <slot name="sync-badge">
10
+ <SyncBadge :item="event" />
11
+ </slot>
12
+ </div>
13
+ </slot>
14
+
15
+ <slot name="meta">
16
+ <div class="bt-event-card__meta mb-2">
17
+ <small class="text-muted">
18
+ <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
19
+ <span v-if="event.starts_at">📅 {{ formattedDate }}</span>
20
+ </small>
21
+ <BBadge
22
+ :variant="privacyVariant"
23
+ class="ms-2"
24
+ >
25
+ {{ event.privacy }}
26
+ </BBadge>
27
+ </div>
28
+ </slot>
29
+
30
+ <slot name="body">
31
+ <BCardText class="bt-event-card__description">
32
+ {{ truncatedDescription }}
33
+ </BCardText>
34
+ </slot>
35
+ </BCardBody>
36
+
37
+ <template #footer>
38
+ <slot name="footer">
39
+ <div class="d-flex align-items-center gap-2">
40
+ <BButton
41
+ variant="outline-primary"
42
+ size="sm"
43
+ @click="$emit('view', event)"
44
+ >
45
+ {{ t('bt.events.view') }}
46
+ </BButton>
47
+ <ExtensionSlot
48
+ slot="footer"
49
+ target="EventCard"
50
+ :context="{ item: event }"
51
+ />
52
+ </div>
53
+ </slot>
54
+ </template>
55
+ </BCard>
56
+ </template>
57
+
58
+ <script setup>
59
+ import { computed } from 'vue'
60
+ import { useI18n } from 'vue-i18n'
61
+ import { BCard, BCardBody, BCardText, BBadge, BButton } from 'bootstrap-vue-next'
62
+ import SyncBadge from '../sync/SyncBadge.vue'
63
+ import ExtensionSlot from '../shared/ExtensionSlot.vue'
64
+
65
+ const { t } = useI18n()
66
+
67
+ const props = defineProps({ event: { type: Object, required: true } })
68
+ defineEmits(['view'])
69
+
70
+ const formattedDate = computed(() => {
71
+ if (!props.event.starts_at) return ''
72
+ const start = new Date(props.event.starts_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
73
+ if (!props.event.ends_at) return start
74
+ const end = new Date(props.event.ends_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
75
+ return `${start} – ${end}`
76
+ })
77
+ const privacyVariant = computed(() => ({ public: 'success', private: 'danger', protected: 'warning' }[props.event.privacy] ?? 'secondary'))
78
+ const truncatedDescription = computed(() => {
79
+ const d = props.event.description || ''
80
+ return d.length > 150 ? d.slice(0, 150) + '…' : d
81
+ })
82
+ </script>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <BForm @submit.prevent="handleSubmit">
3
+ <BFormGroup
4
+ :label="t('bt.events.name_label')"
5
+ label-for="event-name"
6
+ >
7
+ <BFormInput
8
+ id="event-name"
9
+ v-model="form.name"
10
+ required
11
+ :placeholder="t('bt.events.name_label')"
12
+ />
13
+ </BFormGroup>
14
+ <BFormGroup
15
+ :label="t('bt.events.description_label')"
16
+ label-for="event-description"
17
+ >
18
+ <BFormTextarea
19
+ id="event-description"
20
+ v-model="form.description"
21
+ rows="3"
22
+ :placeholder="t('bt.events.description_label')"
23
+ />
24
+ </BFormGroup>
25
+ <BRow>
26
+ <BCol md="6">
27
+ <BFormGroup
28
+ :label="t('bt.events.starts_at_label')"
29
+ label-for="event-starts"
30
+ >
31
+ <BFormInput
32
+ id="event-starts"
33
+ v-model="form.starts_at"
34
+ type="datetime-local"
35
+ />
36
+ </BFormGroup>
37
+ </BCol>
38
+ <BCol md="6">
39
+ <BFormGroup
40
+ :label="t('bt.events.ends_at_label')"
41
+ label-for="event-ends"
42
+ >
43
+ <BFormInput
44
+ id="event-ends"
45
+ v-model="form.ends_at"
46
+ type="datetime-local"
47
+ />
48
+ </BFormGroup>
49
+ </BCol>
50
+ </BRow>
51
+ <BFormGroup
52
+ :label="t('bt.posts.privacy_label')"
53
+ label-for="event-privacy"
54
+ >
55
+ <BFormSelect
56
+ id="event-privacy"
57
+ v-model="form.privacy"
58
+ :options="privacyOptions"
59
+ />
60
+ </BFormGroup>
61
+ <BButton
62
+ type="submit"
63
+ variant="primary"
64
+ >
65
+ {{ t('bt.events.save') }}
66
+ </BButton>
67
+ </BForm>
68
+ </template>
69
+
70
+ <script setup>
71
+ import { reactive } from 'vue'
72
+ import { useI18n } from 'vue-i18n'
73
+ import { BForm, BFormGroup, BFormInput, BFormTextarea, BFormSelect, BButton, BRow, BCol } from 'bootstrap-vue-next'
74
+
75
+ const { t } = useI18n()
76
+
77
+ const props = defineProps({
78
+ model: { type: Object, default: () => ({}) },
79
+ communityId: { type: String, default: null },
80
+ })
81
+ const emit = defineEmits(['submit'])
82
+
83
+ const form = reactive({
84
+ name: props.model.name || '',
85
+ description: props.model.description || '',
86
+ starts_at: props.model.starts_at || '',
87
+ ends_at: props.model.ends_at || '',
88
+ privacy: props.model.privacy || 'public',
89
+ community_id: props.communityId || props.model.community_id || null,
90
+ })
91
+
92
+ const privacyOptions = [
93
+ { value: 'public', text: 'Public' },
94
+ { value: 'protected', text: 'Community members only' },
95
+ { value: 'private', text: 'Private' },
96
+ ]
97
+
98
+ function handleSubmit() { emit('submit', { ...form }) }
99
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div class="bt-event-list">
3
+ <slot name="header" />
4
+ <div v-if="loading">
5
+ <slot name="loading">
6
+ <BSpinner :label="t('bt.events.loading')" />
7
+ </slot>
8
+ </div>
9
+ <div v-else-if="!events.length">
10
+ <slot name="empty">
11
+ <p class="text-muted">
12
+ {{ t('bt.events.list_empty') }}
13
+ </p>
14
+ </slot>
15
+ </div>
16
+ <div v-else>
17
+ <div
18
+ v-for="(event, index) in events"
19
+ :key="event.id"
20
+ class="mb-3"
21
+ >
22
+ <slot
23
+ name="item"
24
+ :item="event"
25
+ :index="index"
26
+ >
27
+ <EventCard
28
+ :event="event"
29
+ @view="$emit('view', event)"
30
+ />
31
+ </slot>
32
+ </div>
33
+ </div>
34
+ <slot name="footer" />
35
+ </div>
36
+ </template>
37
+
38
+ <script setup>
39
+ import { useI18n } from 'vue-i18n'
40
+ import { BSpinner } from 'bootstrap-vue-next'
41
+ import EventCard from './EventCard.vue'
42
+
43
+ const { t } = useI18n()
44
+
45
+ defineProps({ events: { type: Array, default: () => [] }, loading: { type: Boolean, default: false } })
46
+ defineEmits(['view'])
47
+ </script>
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <BCard class="bt-invitation-card">
3
+ <BCardBody>
4
+ <slot name="title">
5
+ <div class="d-flex align-items-start justify-content-between gap-2 mb-1">
6
+ <div>
7
+ <h6 class="mb-0">
8
+ {{ invitation.name || invitation.email }}
9
+ </h6>
10
+ <small class="text-muted">{{ invitation.email }}</small>
11
+ </div>
12
+ <slot name="sync-badge">
13
+ <SyncBadge :item="invitation" />
14
+ </slot>
15
+ </div>
16
+ </slot>
17
+ <slot name="meta">
18
+ <div class="d-flex align-items-center gap-2 mt-2">
19
+ <BBadge :variant="statusVariant">
20
+ {{ invitation.status }}
21
+ </BBadge>
22
+ <small
23
+ v-if="invitation.expires_at"
24
+ class="text-muted"
25
+ >
26
+ {{ t('bt.invitations.expires', { date: formattedExpiry }) }}
27
+ </small>
28
+ </div>
29
+ </slot>
30
+ <slot name="body" />
31
+ </BCardBody>
32
+ <template #footer>
33
+ <slot name="footer">
34
+ <ExtensionSlot
35
+ slot="footer"
36
+ target="InvitationCard"
37
+ :context="{ item: invitation }"
38
+ />
39
+ </slot>
40
+ </template>
41
+ </BCard>
42
+ </template>
43
+ <script setup>
44
+ import { computed } from 'vue'
45
+ import { useI18n } from 'vue-i18n'
46
+ import { BCard, BCardBody, BBadge } from 'bootstrap-vue-next'
47
+ import SyncBadge from '../sync/SyncBadge.vue'
48
+ import ExtensionSlot from '../shared/ExtensionSlot.vue'
49
+ const { t } = useI18n()
50
+ const props = defineProps({ invitation: { type: Object, required: true } })
51
+ const statusVariant = computed(() => ({ pending: 'warning', accepted: 'success', expired: 'secondary' }[props.invitation.status] ?? 'secondary'))
52
+ const formattedExpiry = computed(() => {
53
+ if (!props.invitation.expires_at) return ''
54
+ return new Date(props.invitation.expires_at).toLocaleDateString()
55
+ })
56
+ </script>
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <BForm
3
+ class="bt-invitation-form"
4
+ @submit.prevent="handleSubmit"
5
+ >
6
+ <BFormGroup
7
+ :label="t('bt.invitations.email_label')"
8
+ label-for="invitation-email"
9
+ >
10
+ <BFormInput
11
+ id="invitation-email"
12
+ v-model="form.email"
13
+ type="email"
14
+ required
15
+ :placeholder="t('bt.invitations.email_placeholder')"
16
+ />
17
+ </BFormGroup>
18
+ <BFormGroup
19
+ :label="t('bt.invitations.name_label')"
20
+ label-for="invitation-name"
21
+ >
22
+ <BFormInput
23
+ id="invitation-name"
24
+ v-model="form.name"
25
+ :placeholder="t('bt.invitations.name_placeholder')"
26
+ />
27
+ </BFormGroup>
28
+ <BFormGroup
29
+ :label="t('bt.invitations.message_label')"
30
+ label-for="invitation-message"
31
+ >
32
+ <BFormTextarea
33
+ id="invitation-message"
34
+ v-model="form.message"
35
+ rows="3"
36
+ :placeholder="t('bt.invitations.message_placeholder')"
37
+ />
38
+ </BFormGroup>
39
+ <div class="d-flex justify-content-end gap-2 mt-3">
40
+ <BButton
41
+ type="button"
42
+ variant="outline-secondary"
43
+ @click="$emit('cancel')"
44
+ >
45
+ {{ t('bt.actions.cancel') }}
46
+ </BButton>
47
+ <BButton
48
+ type="submit"
49
+ variant="primary"
50
+ >
51
+ {{ t('bt.invitations.send') }}
52
+ </BButton>
53
+ </div>
54
+ </BForm>
55
+ </template>
56
+ <script setup>
57
+ import { reactive } from 'vue'
58
+ import { useI18n } from 'vue-i18n'
59
+ import { BForm, BFormGroup, BFormInput, BFormTextarea, BButton } from 'bootstrap-vue-next'
60
+ const { t } = useI18n()
61
+ const emit = defineEmits(['submit', 'cancel'])
62
+ const form = reactive({ email: '', name: '', message: '' })
63
+ function handleSubmit() {
64
+ if (!form.email) return
65
+ emit('submit', { ...form })
66
+ form.email = ''
67
+ form.name = ''
68
+ form.message = ''
69
+ }
70
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div class="bt-invitation-list">
3
+ <slot name="header" />
4
+ <div
5
+ v-if="loading"
6
+ class="bt-invitation-list__loading"
7
+ >
8
+ <slot name="loading">
9
+ <div class="text-center p-4 text-muted">
10
+ {{ t('bt.invitations.loading') }}
11
+ </div>
12
+ </slot>
13
+ </div>
14
+ <div
15
+ v-else-if="!invitations.length"
16
+ class="bt-invitation-list__empty"
17
+ >
18
+ <slot name="empty">
19
+ <div class="text-center p-4 text-muted">
20
+ {{ t('bt.invitations.list_empty') }}
21
+ </div>
22
+ </slot>
23
+ </div>
24
+ <template v-else>
25
+ <slot
26
+ v-for="(invitation, index) in invitations"
27
+ :key="invitation.id"
28
+ name="item"
29
+ :item="invitation"
30
+ :index="index"
31
+ >
32
+ <InvitationCard
33
+ :invitation="invitation"
34
+ class="mb-3"
35
+ @view="$emit('view', invitation)"
36
+ />
37
+ </slot>
38
+ </template>
39
+ <slot name="footer" />
40
+ </div>
41
+ </template>
42
+ <script setup>
43
+ import { useI18n } from 'vue-i18n'
44
+ import InvitationCard from './InvitationCard.vue'
45
+ const { t } = useI18n()
46
+ defineProps({
47
+ invitations: { type: Array, default: () => [] },
48
+ loading: { type: Boolean, default: false },
49
+ })
50
+ defineEmits(['view'])
51
+ </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <BCard class="bt-agreement-card">
3
+ <BCardBody>
4
+ <slot name="title">
5
+ <div class="d-flex align-items-start justify-content-between gap-2 mb-1">
6
+ <h5 class="bt-agreement-card__title mb-0">
7
+ {{ t('bt.joatu.agreements.credits_display', agreement.time_credits) }}
8
+ </h5>
9
+ <slot name="sync-badge">
10
+ <SyncBadge :item="agreement" />
11
+ </slot>
12
+ </div>
13
+ </slot>
14
+ <slot name="meta">
15
+ <div class="d-flex align-items-center gap-2 mb-2">
16
+ <BBadge :variant="statusVariant">
17
+ {{ agreement.status }}
18
+ </BBadge>
19
+ <small
20
+ v-if="agreement.agreed_at"
21
+ class="text-muted"
22
+ >
23
+ {{ t('bt.joatu.agreements.agreed_on', { date: formattedAgreedAt }) }}
24
+ </small>
25
+ </div>
26
+ </slot>
27
+ <slot name="body">
28
+ <BCardText
29
+ v-if="agreement.notes"
30
+ class="text-muted"
31
+ >
32
+ {{ agreement.notes }}
33
+ </BCardText>
34
+ </slot>
35
+ </BCardBody>
36
+ <template #footer>
37
+ <slot name="footer">
38
+ <ExtensionSlot
39
+ slot="footer"
40
+ target="AgreementCard"
41
+ :context="{ item: agreement }"
42
+ />
43
+ </slot>
44
+ </template>
45
+ </BCard>
46
+ </template>
47
+ <script setup>
48
+ import { computed } from 'vue'
49
+ import { useI18n } from 'vue-i18n'
50
+ import { BCard, BCardBody, BCardText, BBadge } from 'bootstrap-vue-next'
51
+ import SyncBadge from '../sync/SyncBadge.vue'
52
+ import ExtensionSlot from '../shared/ExtensionSlot.vue'
53
+ const { t } = useI18n()
54
+ const props = defineProps({ agreement: { type: Object, required: true } })
55
+ const statusVariant = computed(() => ({ pending: 'warning', active: 'info', completed: 'success', disputed: 'danger' }[props.agreement.status] ?? 'secondary'))
56
+ const formattedAgreedAt = computed(() => { if (!props.agreement.agreed_at) return ''; return new Date(props.agreement.agreed_at).toLocaleDateString() })
57
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div class="bt-agreement-list">
3
+ <slot name="header" />
4
+ <div
5
+ v-if="loading"
6
+ class="bt-agreement-list__loading"
7
+ >
8
+ <slot name="loading">
9
+ <div class="text-center p-4 text-muted">
10
+ {{ t('bt.joatu.agreements.loading') }}
11
+ </div>
12
+ </slot>
13
+ </div>
14
+ <div
15
+ v-else-if="!agreements.length"
16
+ class="bt-agreement-list__empty"
17
+ >
18
+ <slot name="empty">
19
+ <div class="text-center p-4 text-muted">
20
+ {{ t('bt.joatu.agreements.list_empty') }}
21
+ </div>
22
+ </slot>
23
+ </div>
24
+ <template v-else>
25
+ <slot
26
+ v-for="(agreement, index) in agreements"
27
+ :key="agreement.id"
28
+ name="item"
29
+ :item="agreement"
30
+ :index="index"
31
+ >
32
+ <AgreementCard
33
+ :agreement="agreement"
34
+ class="mb-3"
35
+ @view="$emit('view', agreement)"
36
+ />
37
+ </slot>
38
+ </template>
39
+ <slot name="footer" />
40
+ </div>
41
+ </template>
42
+ <script setup>
43
+ import { useI18n } from 'vue-i18n'
44
+ import AgreementCard from './AgreementCard.vue'
45
+ const { t } = useI18n()
46
+ defineProps({
47
+ agreements: { type: Array, default: () => [] },
48
+ loading: { type: Boolean, default: false },
49
+ })
50
+ defineEmits(['view'])
51
+ </script>
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <BCard class="bt-offer-card">
3
+ <BCardBody>
4
+ <slot name="title">
5
+ <div class="d-flex align-items-start justify-content-between gap-2 mb-1">
6
+ <h5 class="bt-offer-card__title mb-0">
7
+ {{ offer.title }}
8
+ </h5>
9
+ <slot name="sync-badge">
10
+ <SyncBadge :item="offer" />
11
+ </slot>
12
+ </div>
13
+ </slot>
14
+ <slot name="meta">
15
+ <div class="d-flex align-items-center gap-2 mb-2">
16
+ <BBadge :variant="statusVariant">
17
+ {{ offer.status }}
18
+ </BBadge>
19
+ <span class="bt-offer-card__credits">
20
+ {{ t('bt.joatu.offers.credits_display', offer.time_credits) }}
21
+ </span>
22
+ <BBadge
23
+ v-if="offer.category"
24
+ variant="outline-secondary"
25
+ >
26
+ {{ offer.category }}
27
+ </BBadge>
28
+ </div>
29
+ </slot>
30
+ <slot name="body">
31
+ <BCardText>{{ truncatedDescription }}</BCardText>
32
+ </slot>
33
+ </BCardBody>
34
+ <template #footer>
35
+ <slot name="footer">
36
+ <div class="d-flex align-items-center gap-2">
37
+ <BButton
38
+ variant="outline-success"
39
+ size="sm"
40
+ @click="$emit('view', offer)"
41
+ >
42
+ {{ t('bt.joatu.offers.view') }}
43
+ </BButton>
44
+ <ExtensionSlot
45
+ slot="footer"
46
+ target="OfferCard"
47
+ :context="{ item: offer }"
48
+ />
49
+ </div>
50
+ </slot>
51
+ </template>
52
+ </BCard>
53
+ </template>
54
+ <script setup>
55
+ import { computed } from 'vue'
56
+ import { useI18n } from 'vue-i18n'
57
+ import { BCard, BCardBody, BCardText, BBadge, BButton } from 'bootstrap-vue-next'
58
+ import SyncBadge from '../sync/SyncBadge.vue'
59
+ import ExtensionSlot from '../shared/ExtensionSlot.vue'
60
+ const { t } = useI18n()
61
+ const props = defineProps({ offer: { type: Object, required: true } })
62
+ defineEmits(['view'])
63
+ const truncatedDescription = computed(() => { const d = props.offer.description || ''; return d.length > 150 ? d.slice(0, 150) + '…' : d })
64
+ const statusVariant = computed(() => ({ open: 'success', fulfilled: 'info', withdrawn: 'secondary' }[props.offer.status] ?? 'secondary'))
65
+ </script>