@fy-/fws-vue-core 3.0.3 → 3.0.5

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 (121) hide show
  1. package/package.json +6 -8
  2. package/src/components/fws/CmsArticleBoxed.vue +247 -0
  3. package/src/components/fws/CmsArticleSingle.vue +201 -0
  4. package/src/components/fws/DataTable.vue +659 -0
  5. package/src/components/fws/FilterData.vue +423 -0
  6. package/src/components/fws/UserData.vue +220 -0
  7. package/src/components/fws/UserFlow.vue +955 -0
  8. package/src/components/fws/UserOAuth2.vue +521 -0
  9. package/src/components/fws/UserProfile.vue +615 -0
  10. package/src/components/fws/UserProfileStrict.vue +233 -0
  11. package/src/components/ssr/ClientOnly.ts +10 -0
  12. package/src/components/ui/DefaultBreadcrumb.vue +99 -0
  13. package/src/components/ui/DefaultConfirm.vue +178 -0
  14. package/src/components/ui/DefaultConfirmWithInput.vue +217 -0
  15. package/src/components/ui/DefaultDropdown.vue +104 -0
  16. package/src/components/ui/DefaultDropdownLink.vue +94 -0
  17. package/src/components/ui/DefaultGallery.vue +1056 -0
  18. package/src/components/ui/DefaultInput.vue +768 -0
  19. package/src/components/ui/DefaultLoader.vue +125 -0
  20. package/src/components/ui/DefaultModal.vue +350 -0
  21. package/src/components/ui/DefaultNotif.vue +332 -0
  22. package/src/components/ui/DefaultPaging.vue +395 -0
  23. package/src/components/ui/DefaultSidebar.vue +267 -0
  24. package/src/components/ui/DefaultTagInput.vue +415 -0
  25. package/src/components/ui/transitions/CollapseTransition.vue +19 -0
  26. package/src/components/ui/transitions/ExpandTransition.vue +19 -0
  27. package/src/components/ui/transitions/FadeTransition.vue +17 -0
  28. package/src/components/ui/transitions/ScaleTransition.vue +21 -0
  29. package/src/components/ui/transitions/SlideTransition.vue +32 -0
  30. package/src/composables/event-bus.ts +15 -0
  31. package/src/composables/rest.ts +165 -0
  32. package/src/composables/seo.ts +142 -0
  33. package/src/composables/ssr.ts +103 -0
  34. package/src/composables/templating.ts +133 -0
  35. package/src/composables/translations.ts +45 -0
  36. package/src/env.d.ts +10 -0
  37. package/{dist/src/index.d.ts → src/index.ts} +71 -45
  38. package/src/plugin.ts +42 -0
  39. package/src/safelist.html +11 -0
  40. package/src/stores/serverRouter.ts +62 -0
  41. package/src/stores/user.ts +118 -0
  42. package/src/types.ts +58 -0
  43. package/dist/index.css +0 -2
  44. package/dist/index.js +0 -5767
  45. package/dist/src/components/fws/CmsArticleBoxed.vue.d.ts +0 -32
  46. package/dist/src/components/fws/CmsArticleBoxed.vue.d.ts.map +0 -1
  47. package/dist/src/components/fws/CmsArticleSingle.vue.d.ts +0 -29
  48. package/dist/src/components/fws/CmsArticleSingle.vue.d.ts.map +0 -1
  49. package/dist/src/components/fws/DataTable.vue.d.ts +0 -52
  50. package/dist/src/components/fws/DataTable.vue.d.ts.map +0 -1
  51. package/dist/src/components/fws/FilterData.vue.d.ts +0 -15
  52. package/dist/src/components/fws/FilterData.vue.d.ts.map +0 -1
  53. package/dist/src/components/fws/UserData.vue.d.ts +0 -8
  54. package/dist/src/components/fws/UserData.vue.d.ts.map +0 -1
  55. package/dist/src/components/fws/UserFlow.vue.d.ts +0 -116
  56. package/dist/src/components/fws/UserFlow.vue.d.ts.map +0 -1
  57. package/dist/src/components/fws/UserOAuth2.vue.d.ts +0 -17
  58. package/dist/src/components/fws/UserOAuth2.vue.d.ts.map +0 -1
  59. package/dist/src/components/fws/UserProfile.vue.d.ts +0 -40
  60. package/dist/src/components/fws/UserProfile.vue.d.ts.map +0 -1
  61. package/dist/src/components/fws/UserProfileStrict.vue.d.ts +0 -12
  62. package/dist/src/components/fws/UserProfileStrict.vue.d.ts.map +0 -1
  63. package/dist/src/components/ssr/ClientOnly.d.ts +0 -4
  64. package/dist/src/components/ssr/ClientOnly.d.ts.map +0 -1
  65. package/dist/src/components/ui/DefaultBreadcrumb.vue.d.ts +0 -11
  66. package/dist/src/components/ui/DefaultBreadcrumb.vue.d.ts.map +0 -1
  67. package/dist/src/components/ui/DefaultConfirm.vue.d.ts +0 -81
  68. package/dist/src/components/ui/DefaultConfirm.vue.d.ts.map +0 -1
  69. package/dist/src/components/ui/DefaultConfirmWithInput.vue.d.ts +0 -81
  70. package/dist/src/components/ui/DefaultConfirmWithInput.vue.d.ts.map +0 -1
  71. package/dist/src/components/ui/DefaultDropdown.vue.d.ts +0 -35
  72. package/dist/src/components/ui/DefaultDropdown.vue.d.ts.map +0 -1
  73. package/dist/src/components/ui/DefaultDropdownLink.vue.d.ts +0 -23
  74. package/dist/src/components/ui/DefaultDropdownLink.vue.d.ts.map +0 -1
  75. package/dist/src/components/ui/DefaultGallery.vue.d.ts +0 -114
  76. package/dist/src/components/ui/DefaultGallery.vue.d.ts.map +0 -1
  77. package/dist/src/components/ui/DefaultInput.vue.d.ts +0 -61
  78. package/dist/src/components/ui/DefaultInput.vue.d.ts.map +0 -1
  79. package/dist/src/components/ui/DefaultLoader.vue.d.ts +0 -12
  80. package/dist/src/components/ui/DefaultLoader.vue.d.ts.map +0 -1
  81. package/dist/src/components/ui/DefaultModal.vue.d.ts +0 -36
  82. package/dist/src/components/ui/DefaultModal.vue.d.ts.map +0 -1
  83. package/dist/src/components/ui/DefaultNotif.vue.d.ts +0 -3
  84. package/dist/src/components/ui/DefaultNotif.vue.d.ts.map +0 -1
  85. package/dist/src/components/ui/DefaultPaging.vue.d.ts +0 -13
  86. package/dist/src/components/ui/DefaultPaging.vue.d.ts.map +0 -1
  87. package/dist/src/components/ui/DefaultSidebar.vue.d.ts +0 -29
  88. package/dist/src/components/ui/DefaultSidebar.vue.d.ts.map +0 -1
  89. package/dist/src/components/ui/DefaultTagInput.vue.d.ts +0 -34
  90. package/dist/src/components/ui/DefaultTagInput.vue.d.ts.map +0 -1
  91. package/dist/src/components/ui/transitions/CollapseTransition.vue.d.ts +0 -18
  92. package/dist/src/components/ui/transitions/CollapseTransition.vue.d.ts.map +0 -1
  93. package/dist/src/components/ui/transitions/ExpandTransition.vue.d.ts +0 -18
  94. package/dist/src/components/ui/transitions/ExpandTransition.vue.d.ts.map +0 -1
  95. package/dist/src/components/ui/transitions/FadeTransition.vue.d.ts +0 -18
  96. package/dist/src/components/ui/transitions/FadeTransition.vue.d.ts.map +0 -1
  97. package/dist/src/components/ui/transitions/ScaleTransition.vue.d.ts +0 -18
  98. package/dist/src/components/ui/transitions/ScaleTransition.vue.d.ts.map +0 -1
  99. package/dist/src/components/ui/transitions/SlideTransition.vue.d.ts +0 -21
  100. package/dist/src/components/ui/transitions/SlideTransition.vue.d.ts.map +0 -1
  101. package/dist/src/composables/event-bus.d.ts +0 -8
  102. package/dist/src/composables/event-bus.d.ts.map +0 -1
  103. package/dist/src/composables/rest.d.ts +0 -24
  104. package/dist/src/composables/rest.d.ts.map +0 -1
  105. package/dist/src/composables/seo.d.ts +0 -26
  106. package/dist/src/composables/seo.d.ts.map +0 -1
  107. package/dist/src/composables/ssr.d.ts +0 -24
  108. package/dist/src/composables/ssr.d.ts.map +0 -1
  109. package/dist/src/composables/templating.d.ts +0 -7
  110. package/dist/src/composables/templating.d.ts.map +0 -1
  111. package/dist/src/composables/translations.d.ts +0 -8
  112. package/dist/src/composables/translations.d.ts.map +0 -1
  113. package/dist/src/index.d.ts.map +0 -1
  114. package/dist/src/plugin.d.ts +0 -3
  115. package/dist/src/plugin.d.ts.map +0 -1
  116. package/dist/src/stores/serverRouter.d.ts +0 -34
  117. package/dist/src/stores/serverRouter.d.ts.map +0 -1
  118. package/dist/src/stores/user.d.ts +0 -139
  119. package/dist/src/stores/user.d.ts.map +0 -1
  120. package/dist/src/types.d.ts +0 -48
  121. package/dist/src/types.d.ts.map +0 -1
@@ -0,0 +1,423 @@
1
+ <script setup lang="ts">
2
+ import type { FilterDataItems } from '../../types'
3
+ import { AdjustmentsHorizontalIcon, ArrowPathIcon, FunnelIcon, XMarkIcon } from '@heroicons/vue/24/solid'
4
+ import { onMounted, onUnmounted, reactive, ref } from 'vue'
5
+ import { useEventBus } from '../../composables/event-bus'
6
+ import { useTranslation } from '../../composables/translations'
7
+ import DefaultInput from '../ui/DefaultInput.vue'
8
+
9
+ const emit = defineEmits(['update:modelValue'])
10
+ const hidden = ref<boolean>(false)
11
+ const state = reactive<any>({ formData: {} })
12
+ const types = reactive<any>({})
13
+ const translate = useTranslation()
14
+ const fDynamicOptions = ref<any[]>([])
15
+ const eventBus = useEventBus()
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ data?: Array<Array<FilterDataItems>>
20
+ css: string
21
+ modelValue?: Record<string, unknown>
22
+ }>(),
23
+ {
24
+ data: () => [],
25
+ },
26
+ )
27
+
28
+ function removeUndefinedStrings(input: any, undefinedValues: any[] = ['undefined']) {
29
+ const output: any = {}
30
+ Object.keys(input).forEach((key) => {
31
+ if (!undefinedValues.includes(input[key]) && input[key] !== undefined) {
32
+ if (!input[key].$between) {
33
+ output[key] = input[key]
34
+ }
35
+ else {
36
+ input[key].$between[0]
37
+ = input[key].$between[0] === '' || input[key].$between[0] == null
38
+ ? undefined
39
+ : input[key].$between[0]
40
+ input[key].$between[1]
41
+ = input[key].$between[1] === '' || input[key].$between[1] == null
42
+ ? undefined
43
+ : input[key].$between[1]
44
+ if (
45
+ input[key].$between[0] !== undefined
46
+ || input[key].$between[1] !== undefined
47
+ ) {
48
+ output[key] = input[key]
49
+ }
50
+ }
51
+ }
52
+ })
53
+ return output
54
+ }
55
+
56
+ function formatValues(obj: any) {
57
+ props.data.forEach((group) => {
58
+ group.forEach((f) => {
59
+ if (f.formats && f.formats[f.type]) {
60
+ obj[f.uid] = f.formats[f.type](obj[f.uid])
61
+ }
62
+ if (f.formatRestValue) {
63
+ obj[f.uid] = f.formatRestValue(obj[f.uid])
64
+ }
65
+ })
66
+ })
67
+ return removeUndefinedStrings(obj, ['undefined', ''])
68
+ }
69
+
70
+ function updateForms() {
71
+ state.formData = {}
72
+ props.data.forEach((group) => {
73
+ group.forEach((f) => {
74
+ state.formData[f.uid]
75
+ = typeof f.default == 'object' && f.default
76
+ ? JSON.parse(JSON.stringify(f.default))
77
+ : f.default
78
+ types[f.uid] = f.type
79
+ if (f.options && f.options.length) {
80
+ f.options = f.options.map((status) => {
81
+ const [statusKey, statusValue] = status
82
+ const translatedValue = translate(statusValue)
83
+ return [statusKey, translatedValue]
84
+ })
85
+ }
86
+ })
87
+ })
88
+ emit('update:modelValue', formatValues({ ...state.formData }))
89
+ }
90
+ updateForms()
91
+
92
+ function updateFormData(data: any) {
93
+ data.forEach((d: any) => {
94
+ if (d.uid) {
95
+ state.formData[d.uid] = d.value
96
+ }
97
+ })
98
+ submitForm()
99
+ }
100
+
101
+ function submitForm() {
102
+ const formData = formatValues({ ...state.formData })
103
+ emit('update:modelValue', formData)
104
+ eventBus.emit('forceUpdateFilters', true)
105
+ }
106
+
107
+ function resetForm() {
108
+ updateForms()
109
+ }
110
+
111
+ onMounted(() => {
112
+ eventBus.on('resetFilters', resetForm)
113
+ eventBus.on('updateFilters', updateFormData)
114
+ })
115
+ onUnmounted(() => {
116
+ eventBus.off('resetFilters', resetForm)
117
+ eventBus.off('updateFilters', updateFormData)
118
+ })
119
+ </script>
120
+
121
+ <template>
122
+ <div class="fws-filter">
123
+ <form v-if="!hidden" class="fws-filter__form" @submit.prevent="submitForm">
124
+ <div
125
+ class="fws-filter__grid"
126
+ :class="[
127
+ css,
128
+ { 'fws-filter__grid--3': data.length >= 3, 'fws-filter__grid--4': data.length >= 4 },
129
+ ]"
130
+ >
131
+ <div v-for="(g, i) in data" :key="`index_${i}`" class="fws-filter__group">
132
+ <template v-for="f in g" :key="f.uid">
133
+ <template v-if="!f.isHidden">
134
+ <DefaultInput
135
+ v-if="['text', 'select', 'date', 'email', 'autocomplete'].includes(f.type)"
136
+ :id="f.uid"
137
+ v-model="state.formData[f.uid]"
138
+ :type="f.type === 'autocomplete' ? 'text' : f.type"
139
+ :label="f.label"
140
+ :options="f.options ? f.options : [[]]"
141
+ class="fws-filter__input"
142
+ @focus="() => { f.focused = true; }"
143
+ @blur="() => { f.focused = false; }"
144
+ @change="(ev: any) => { if (f.onChangeValue) f.onChangeValue(state.formData, ev); }"
145
+ @update:model-value="(v: any) => {
146
+ if (f.autocomplete && v.length >= 2) {
147
+ fDynamicOptions = [];
148
+ f.autocomplete(v).then((r: any) => { fDynamicOptions = r; });
149
+ }
150
+ }"
151
+ >
152
+ <div
153
+ v-if="f.type === 'autocomplete' && f.focused && fDynamicOptions.length > 0"
154
+ class="fws-filter__autocomplete"
155
+ >
156
+ <button
157
+ v-for="o in fDynamicOptions"
158
+ :key="o[0]"
159
+ class="fws-filter__autocomplete-item"
160
+ type="button"
161
+ @click.prevent="() => { f.focused = false; state.formData[f.uid] = o[0]; }"
162
+ >
163
+ <span class="fws-filter__autocomplete-label">{{ o[1] }}</span>
164
+ <small v-if="o[0] !== ''" class="fws-filter__autocomplete-id">({{ o[0] }})</small>
165
+ </button>
166
+ </div>
167
+ </DefaultInput>
168
+
169
+ <!-- Date range: use two date inputs as fallback -->
170
+ <div v-if="f.type === 'range'" class="fws-filter__range">
171
+ <DefaultInput
172
+ :id="`${f.uid}_from`"
173
+ v-model="state.formData[f.uid].$between[0]"
174
+ type="date"
175
+ :label="`${f.label} (from)`"
176
+ class="fws-filter__input"
177
+ />
178
+ <DefaultInput
179
+ :id="`${f.uid}_to`"
180
+ v-model="state.formData[f.uid].$between[1]"
181
+ type="date"
182
+ :label="`${f.label} (to)`"
183
+ class="fws-filter__input"
184
+ />
185
+ </div>
186
+ </template>
187
+ </template>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="fws-filter__actions">
192
+ <div class="fws-filter__actions-left">
193
+ <button type="submit" class="btn primary sm">
194
+ <AdjustmentsHorizontalIcon class="fws-filter__btn-icon" aria-hidden="true" />
195
+ {{ $t("filters_search_cta") || 'Search' }}
196
+ </button>
197
+ <button type="button" class="btn neutral ol sm" @click.prevent="resetForm">
198
+ <ArrowPathIcon class="fws-filter__btn-icon" aria-hidden="true" />
199
+ {{ $t("filters_clear_cta") || 'Clear' }}
200
+ </button>
201
+ </div>
202
+ <button type="button" class="btn ghost sm" @click="hidden = true">
203
+ <XMarkIcon class="fws-filter__btn-icon" aria-hidden="true" />
204
+ {{ $t("hide_filters_cta") || 'Hide' }}
205
+ </button>
206
+ </div>
207
+ </form>
208
+
209
+ <div v-else class="fws-filter__show-wrap">
210
+ <button type="button" class="fws-filter__show-pill" @click="hidden = false">
211
+ <FunnelIcon class="fws-filter__btn-icon" aria-hidden="true" />
212
+ {{ $t("show_filters_cta") || 'Show filters' }}
213
+ </button>
214
+ </div>
215
+ </div>
216
+ </template>
217
+
218
+ <style scoped>
219
+ .fws-filter {
220
+ margin-bottom: 16px;
221
+ }
222
+
223
+ /* Form — Stripe card with subtle entrance */
224
+ .fws-filter__form {
225
+ padding: 16px;
226
+ border-radius: 10px;
227
+ background: #fff;
228
+ box-shadow:
229
+ 0 0 0 1px rgba(0, 0, 0, 0.05),
230
+ rgba(50, 50, 93, 0.06) 0 2px 6px;
231
+ animation: fws-filter-in 0.2s ease-out;
232
+ }
233
+
234
+ @keyframes fws-filter-in {
235
+ from { opacity: 0; transform: translateY(-6px); }
236
+ to { opacity: 1; transform: translateY(0); }
237
+ }
238
+
239
+ .dark .fws-filter__form {
240
+ background: var(--fv-neutral-900, #0a0a0a);
241
+ box-shadow:
242
+ 0 0 0 1px rgba(255, 255, 255, 0.06);
243
+ }
244
+
245
+ /* Grid layout */
246
+ .fws-filter__grid {
247
+ display: grid;
248
+ grid-template-columns: repeat(2, 1fr);
249
+ gap: 14px;
250
+ column-gap: 16px;
251
+ }
252
+
253
+ .fws-filter__grid--3 {
254
+ grid-template-columns: repeat(3, 1fr);
255
+ }
256
+
257
+ .fws-filter__grid--4 {
258
+ grid-template-columns: repeat(4, 1fr);
259
+ }
260
+
261
+ @media (max-width: 768px) {
262
+ .fws-filter__grid,
263
+ .fws-filter__grid--3,
264
+ .fws-filter__grid--4 {
265
+ grid-template-columns: 1fr;
266
+ }
267
+ }
268
+
269
+ @media (min-width: 769px) and (max-width: 1024px) {
270
+ .fws-filter__grid--3,
271
+ .fws-filter__grid--4 {
272
+ grid-template-columns: repeat(2, 1fr);
273
+ }
274
+ }
275
+
276
+ .fws-filter__group {
277
+ position: relative;
278
+ }
279
+
280
+ .fws-filter__input {
281
+ margin-bottom: 6px;
282
+ }
283
+
284
+ /* Date range pair */
285
+ .fws-filter__range {
286
+ display: grid;
287
+ grid-template-columns: 1fr 1fr;
288
+ gap: 8px;
289
+ }
290
+
291
+ /* Autocomplete dropdown */
292
+ .fws-filter__autocomplete {
293
+ position: absolute;
294
+ left: 0;
295
+ right: 0;
296
+ bottom: 0;
297
+ transform: translateY(100%);
298
+ display: flex;
299
+ flex-direction: column;
300
+ gap: 2px;
301
+ padding: 6px;
302
+ background: #fff;
303
+ border-radius: 8px;
304
+ z-index: 10;
305
+ max-height: 240px;
306
+ overflow-y: auto;
307
+ box-shadow:
308
+ 0 0 0 1px rgba(0, 0, 0, 0.06),
309
+ 0 8px 24px rgba(0, 0, 0, 0.1);
310
+ }
311
+
312
+ .dark .fws-filter__autocomplete {
313
+ background: var(--fv-neutral-800, #1e1e1e);
314
+ box-shadow:
315
+ 0 0 0 1px rgba(255, 255, 255, 0.06),
316
+ 0 8px 24px rgba(0, 0, 0, 0.4);
317
+ }
318
+
319
+ .fws-filter__autocomplete-item {
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: space-between;
323
+ padding: 8px 10px;
324
+ border: none;
325
+ background: none;
326
+ border-radius: 6px;
327
+ font-size: 14px;
328
+ color: var(--fv-neutral-800, #1e1e1e);
329
+ cursor: pointer;
330
+ transition: background 0.1s ease;
331
+ }
332
+
333
+ .fws-filter__autocomplete-item:hover {
334
+ background: rgba(0, 0, 0, 0.04);
335
+ }
336
+
337
+ .dark .fws-filter__autocomplete-item {
338
+ color: var(--fv-neutral-200, #e5e5e5);
339
+ }
340
+
341
+ .dark .fws-filter__autocomplete-item:hover {
342
+ background: rgba(255, 255, 255, 0.05);
343
+ }
344
+
345
+ .fws-filter__autocomplete-label {
346
+ font-weight: 500;
347
+ }
348
+
349
+ .fws-filter__autocomplete-id {
350
+ color: var(--fv-neutral-500, #737373);
351
+ margin-left: 8px;
352
+ }
353
+
354
+ /* Actions */
355
+ .fws-filter__actions {
356
+ display: flex;
357
+ flex-wrap: wrap;
358
+ align-items: center;
359
+ justify-content: space-between;
360
+ margin-top: 16px;
361
+ gap: 8px;
362
+ }
363
+
364
+ .fws-filter__actions-left {
365
+ display: flex;
366
+ flex-wrap: wrap;
367
+ gap: 8px;
368
+ }
369
+
370
+ .fws-filter__btn-icon {
371
+ width: 16px;
372
+ height: 16px;
373
+ margin-right: 4px;
374
+ }
375
+
376
+ /* Show filters — Stripe/Linear pill button */
377
+ .fws-filter__show-wrap {
378
+ text-align: center;
379
+ }
380
+
381
+ .fws-filter__show-pill {
382
+ display: inline-flex;
383
+ align-items: center;
384
+ gap: 6px;
385
+ padding: 6px 16px;
386
+ border: none;
387
+ border-radius: 9999px;
388
+ font-size: 13px;
389
+ font-weight: 500;
390
+ color: var(--fv-primary-600, #7c3aed);
391
+ background: rgba(124, 58, 237, 0.06);
392
+ cursor: pointer;
393
+ transition: background 0.15s ease;
394
+ }
395
+
396
+ .fws-filter__show-pill:hover {
397
+ background: rgba(124, 58, 237, 0.12);
398
+ }
399
+
400
+ .fws-filter__show-pill:focus-visible {
401
+ outline: 2px solid var(--fv-primary-500, #7c3aed);
402
+ outline-offset: 2px;
403
+ }
404
+
405
+ .dark .fws-filter__show-pill {
406
+ color: var(--fv-primary-400, #a78bfa);
407
+ background: rgba(167, 139, 250, 0.08);
408
+ }
409
+
410
+ .dark .fws-filter__show-pill:hover {
411
+ background: rgba(167, 139, 250, 0.15);
412
+ }
413
+
414
+ @media (max-width: 640px) {
415
+ .fws-filter__form {
416
+ padding: 12px;
417
+ }
418
+
419
+ .fws-filter__range {
420
+ grid-template-columns: 1fr;
421
+ }
422
+ }
423
+ </style>
@@ -0,0 +1,220 @@
1
+ <script setup lang="ts">
2
+ import { computed, reactive, watchEffect } from 'vue'
3
+ import { useEventBus } from '../../composables/event-bus'
4
+ import { useRest } from '../../composables/rest'
5
+ import { useUserStore } from '../../stores/user'
6
+ import DefaultInput from '../ui/DefaultInput.vue'
7
+
8
+ const rest = useRest()
9
+ const userStore = useUserStore()
10
+ const userData = computed(() => userStore.user)
11
+ const eventBus = useEventBus()
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ onCompleted?: (data: any) => void
16
+ }>(),
17
+ {
18
+ onCompleted: () => {},
19
+ },
20
+ )
21
+
22
+ const state = reactive({
23
+ Firstname: '',
24
+ Lastname: '',
25
+ Phone: '',
26
+ AcceptedTerms: false,
27
+ EnabledNotifications: false,
28
+ EnabledEmails: false,
29
+ EnabledTrainingFromMyData: false,
30
+ })
31
+
32
+ watchEffect(() => {
33
+ if (!userData.value) return
34
+ state.Firstname = userData.value.Firstname || ''
35
+ state.Lastname = userData.value.Lastname || ''
36
+ state.Phone = userData.value.Phone || ''
37
+ state.AcceptedTerms = userData.value.AcceptedTerms || false
38
+ state.EnabledNotifications = userData.value.EnabledNotifications || false
39
+ state.EnabledEmails = userData.value.EnabledEmails || false
40
+ state.EnabledTrainingFromMyData = userData.value.EnabledTrainingFromMyData || false
41
+ })
42
+
43
+ async function patchUser() {
44
+ eventBus.emit('main-loading', true)
45
+ const response = await rest('User', 'PATCH', { ...state })
46
+ if (response && response.result === 'success') {
47
+ eventBus.emit('user:refresh', true)
48
+ if (props.onCompleted) props.onCompleted(response)
49
+ }
50
+ eventBus.emit('main-loading', false)
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <form class="fws-userdata" @submit.prevent="patchUser">
56
+ <div class="fws-userdata__container">
57
+ <h3 class="fws-userdata__heading">
58
+ {{ $t('fws_personal_data_title') || $t('fws_personal_information') || 'Personal Information' }}
59
+ </h3>
60
+
61
+ <!-- Personal fields -->
62
+ <div class="fws-userdata__grid-2">
63
+ <DefaultInput
64
+ id="firstnameFWS"
65
+ v-model="state.Firstname"
66
+ type="text"
67
+ :label="$t('fws_firstname_label') || 'First name'"
68
+ :help="$t('fws_firstname_help') || ''"
69
+ />
70
+ <DefaultInput
71
+ id="lastnameFWS"
72
+ v-model="state.Lastname"
73
+ type="text"
74
+ :label="$t('fws_lastname_label') || 'Last name'"
75
+ :help="$t('fws_lastname_help') || ''"
76
+ />
77
+ <DefaultInput
78
+ id="phoneFWS"
79
+ v-model="state.Phone"
80
+ type="text"
81
+ :label="$t('fws_phone_label') || 'Phone'"
82
+ :help="$t('fws_phone_help') || ''"
83
+ />
84
+ </div>
85
+
86
+ <!-- Preferences -->
87
+ <div class="fws-userdata__prefs">
88
+ <h4 class="fws-userdata__section-label">
89
+ {{ $t('fws_preferences_and_settings') || 'Preferences & Settings' }}
90
+ </h4>
91
+
92
+ <div class="fws-userdata__toggles">
93
+ <DefaultInput
94
+ v-if="!userData?.AcceptedTerms"
95
+ id="acceptedTermsFWS"
96
+ v-model:checkbox-value="state.AcceptedTerms"
97
+ type="toggle"
98
+ :label="$t('fws_accepted_terms_label') || 'Accept terms'"
99
+ :help="$t('fws_accepted_terms_help') || ''"
100
+ />
101
+
102
+ <div class="fws-userdata__grid-2">
103
+ <DefaultInput
104
+ id="enabledNotificationsFWS"
105
+ v-model:checkbox-value="state.EnabledNotifications"
106
+ type="toggle"
107
+ :label="$t('fws_enabled_notifications_label') || 'Notifications'"
108
+ :help="$t('fws_enabled_notifications_help') || ''"
109
+ />
110
+ <DefaultInput
111
+ id="enabledEmailsFWS"
112
+ v-model:checkbox-value="state.EnabledEmails"
113
+ type="toggle"
114
+ :label="$t('fws_enabled_emails_label') || 'Email updates'"
115
+ :help="$t('fws_enabled_emails_help') || ''"
116
+ />
117
+ </div>
118
+
119
+ <DefaultInput
120
+ id="enabledTrainingFromMyDataFWS"
121
+ v-model:checkbox-value="state.EnabledTrainingFromMyData"
122
+ type="toggle"
123
+ :label="$t('fws_enabled_training_from_my_data_label') || 'Allow data training'"
124
+ :help="$t('fws_enabled_training_from_my_data_help') || ''"
125
+ />
126
+ </div>
127
+ </div>
128
+
129
+ <div class="fws-userdata__actions">
130
+ <button type="submit" class="btn primary">
131
+ {{ $t("fws_save_user_cta") || 'Save changes' }}
132
+ </button>
133
+ </div>
134
+ </div>
135
+ </form>
136
+ </template>
137
+
138
+ <style scoped>
139
+ .fws-userdata__container {
140
+ display: flex;
141
+ flex-direction: column;
142
+ gap: 18px;
143
+ padding: 20px 24px;
144
+ border-radius: 10px;
145
+ background: #fff;
146
+ box-shadow:
147
+ 0 0 0 1px rgba(0, 0, 0, 0.06),
148
+ 0 2px 4px rgba(0, 0, 0, 0.04);
149
+ }
150
+
151
+ .dark .fws-userdata__container {
152
+ background: var(--fv-neutral-900, #0a0a0a);
153
+ box-shadow:
154
+ 0 0 0 1px rgba(255, 255, 255, 0.06),
155
+ 0 2px 4px rgba(0, 0, 0, 0.3);
156
+ }
157
+
158
+ .fws-userdata__heading {
159
+ font-size: 16px;
160
+ font-weight: 600;
161
+ color: var(--fv-neutral-900, #0a0a0a);
162
+ margin: 0;
163
+ padding-bottom: 12px;
164
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
165
+ }
166
+
167
+ .dark .fws-userdata__heading {
168
+ color: var(--fv-neutral-100, #f5f5f5);
169
+ border-bottom-color: rgba(255, 255, 255, 0.06);
170
+ }
171
+
172
+ .fws-userdata__grid-2 {
173
+ display: grid;
174
+ grid-template-columns: 1fr 1fr;
175
+ gap: 16px;
176
+ }
177
+
178
+ @media (max-width: 640px) {
179
+ .fws-userdata__grid-2 {
180
+ grid-template-columns: 1fr;
181
+ }
182
+ }
183
+
184
+ .fws-userdata__prefs {
185
+ padding-top: 14px;
186
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
187
+ }
188
+
189
+ .dark .fws-userdata__prefs {
190
+ border-top-color: rgba(255, 255, 255, 0.06);
191
+ }
192
+
193
+ .fws-userdata__section-label {
194
+ font-size: 14px;
195
+ font-weight: 500;
196
+ color: var(--fv-neutral-800, #1e1e1e);
197
+ margin: 0 0 12px;
198
+ }
199
+
200
+ .dark .fws-userdata__section-label {
201
+ color: var(--fv-neutral-200, #e5e5e5);
202
+ }
203
+
204
+ .fws-userdata__toggles {
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 10px;
208
+ }
209
+
210
+ .fws-userdata__actions {
211
+ display: flex;
212
+ justify-content: flex-end;
213
+ }
214
+
215
+ @media (max-width: 640px) {
216
+ .fws-userdata__container {
217
+ padding: 16px;
218
+ }
219
+ }
220
+ </style>