@fy-/fws-vue 0.3.4 → 0.3.8

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.
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ import useVuelidate from "@vuelidate/core";
3
+ import DefaultInput from "../ui/DefaultInput.vue";
4
+ import { useUserStore } from "../../stores/user";
5
+ import { useRest } from "../../composables/rest";
6
+ import { computed, reactive, watchEffect } from "vue";
7
+ import { required } from "@vuelidate/validators";
8
+ const rest = useRest();
9
+ const userStore = useUserStore();
10
+ const userData = computed(() => userStore.user);
11
+ const state = reactive({
12
+ userData: {
13
+ Firstname: userData.value?.Firstname || "",
14
+ Lastname: userData.value?.Lastname || "",
15
+ Phone: userData.value?.Phone || "",
16
+ Bio: userData.value?.Bio || "",
17
+ Username: userData.value?.Username || "",
18
+ },
19
+ });
20
+ watchEffect(() => {
21
+ state.userData = {
22
+ Firstname: userData.value?.Firstname || "",
23
+ Lastname: userData.value?.Lastname || "",
24
+ Phone: userData.value?.Phone || "",
25
+ Bio: userData.value?.Bio || "",
26
+ Username: userData.value?.Username || "",
27
+ };
28
+ });
29
+ const rules = {
30
+ userData: {
31
+ Username: {
32
+ required: required,
33
+ },
34
+ Firstname: {},
35
+ Lastname: {},
36
+ Phone: {},
37
+ Bio: {},
38
+ },
39
+ };
40
+ const v$ = useVuelidate(rules, state);
41
+
42
+ const patchUser = async () => {
43
+ if (await v$.value.userData.$validate()) {
44
+ const response = await rest("User", "PATCH", state.userData);
45
+ if (response && response.result == "success") {
46
+ window.location.reload();
47
+ }
48
+ }
49
+ };
50
+ </script>
51
+
52
+ <template>
53
+ <form @submit.prevent="patchUser">
54
+ <DefaultInput
55
+ id="usernameFWS"
56
+ v-model="state.userData.Username"
57
+ class="mb-4"
58
+ type="text"
59
+ :label="$t('fws_username_label')"
60
+ :help="$t('fws_username_help')"
61
+ :error-vuelidate="v$.userData.Username.$errors"
62
+ :req="true"
63
+ />
64
+ <DefaultInput
65
+ id="firstnameFWS"
66
+ v-model="state.userData.Firstname"
67
+ class="mb-4"
68
+ type="text"
69
+ :label="$t('fws_firstname_label')"
70
+ :help="$t('fws_firstname_help')"
71
+ :error-vuelidate="v$.userData.Firstname.$errors"
72
+ />
73
+ <DefaultInput
74
+ id="lastnameFWS"
75
+ v-model="state.userData.Lastname"
76
+ class="mb-4"
77
+ type="text"
78
+ :label="$t('fws_lastname_label')"
79
+ :help="$t('fws_lastname_help')"
80
+ :error-vuelidate="v$.userData.Lastname.$errors"
81
+ />
82
+ <DefaultInput
83
+ id="phoneFWS"
84
+ v-model="state.userData.Phone"
85
+ class="mb-4"
86
+ type="text"
87
+ :label="$t('fws_phone_label')"
88
+ :help="$t('fws_phone_help')"
89
+ :error-vuelidate="v$.userData.Phone.$errors"
90
+ />
91
+ <div class="flex">
92
+ <button type="submit" class="btn defaults primary">
93
+ {{ $t("fws_save_user_cta") }}
94
+ </button>
95
+ </div>
96
+ </form>
97
+ </template>
@@ -176,16 +176,6 @@ const userFlow = async (params: paramsType = { initial: false }) => {
176
176
  eventBus.emit("login-loading", false);
177
177
  };
178
178
 
179
- const getContrastingTextColor = (backgroundColor: string) => {
180
- const r = parseInt(backgroundColor.substring(1, 3), 16);
181
- const g = parseInt(backgroundColor.substring(3, 5), 16);
182
- const b = parseInt(backgroundColor.substring(5, 7), 16);
183
-
184
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
185
-
186
- return luminance > 0.5 ? "#000000" : "#FFFFFF";
187
- };
188
-
189
179
  onMounted(async () => {
190
180
  await userFlow({ initial: true });
191
181
  });
@@ -223,7 +213,7 @@ onMounted(async () => {
223
213
  class="flex border border-fv-neutral-300 dark:border-fv-neutral-700 shadow items-center gap-2 justify-start btn neutral defaults w-full mx-auto !font-semibold"
224
214
  :style="`background: ${
225
215
  field.button['background-color']
226
- }; color: ${getContrastingTextColor(
216
+ }; color: ${$getContrastingTextColor(
227
217
  field.button['background-color'],
228
218
  )}`"
229
219
  >
@@ -0,0 +1,174 @@
1
+ <script setup lang="ts">
2
+ import { useTranslation } from "../../composables/translations";
3
+ import { useEventBus } from "../../composables/event-bus";
4
+ import { useUserStore } from "../../stores/user";
5
+ import { useRest, APIResult } from "../../composables/rest";
6
+ import DefaultModal from "../ui/DefaultModal.vue";
7
+
8
+ import { ref, computed, onMounted } from "vue";
9
+ const rest = useRest();
10
+ const eventBus = useEventBus();
11
+ const userStore = useUserStore();
12
+ const isAuth = computed(() => userStore.isAuth);
13
+ const data = ref();
14
+ const providersData = ref();
15
+ const usedProviders = ref<Record<string, boolean>>({});
16
+ const props = defineProps({
17
+ returnTo: {
18
+ type: String,
19
+ required: false,
20
+ default: "/user/account?tab=user_settings",
21
+ },
22
+ });
23
+ const returnTo = ref(props.returnTo);
24
+ if (returnTo.value == "") {
25
+ returnTo.value = "/user/account?tab=user_settings";
26
+ }
27
+ const getOAuth2Providers = async () => {
28
+ eventBus.emit("main-loading", true);
29
+ const d = await rest("User/OAuth2/Providers", "GET");
30
+ if (d && d.result == "success") {
31
+ providersData.value = d.data;
32
+ }
33
+ eventBus.emit("main-loading", false);
34
+ };
35
+ const getOAuth2Redirect = async (providerUUID: string) => {
36
+ eventBus.emit("main-loading", true);
37
+ const d = await rest(`User/OAuth2/Provider/${providerUUID}`, "POST", {
38
+ ReturnTo: returnTo.value,
39
+ });
40
+ if (d && d.result == "success") {
41
+ window.location.href = d.data;
42
+ }
43
+ eventBus.emit("main-loading", false);
44
+ };
45
+ const translate = useTranslation();
46
+ const deleteOAuth2Connection = async (providerUUID: string) => {
47
+ eventBus.emit("showConfirm", {
48
+ title: translate("remove_provider_confirm_title"),
49
+ desc: translate("remove_provider_confirm_desc_warning"),
50
+ onConfirm: async () => {
51
+ eventBus.emit("main-loading", true);
52
+ const d = await rest(`User/OAuth2/Provider/${providerUUID}`, "DELETE");
53
+ if (d && d.result == "success") {
54
+ getOAuth2User();
55
+ }
56
+ eventBus.emit("main-loading", false);
57
+ },
58
+ });
59
+ };
60
+ const getOAuth2User = async () => {
61
+ eventBus.emit("main-loading", true);
62
+ if (!isAuth.value) {
63
+ return;
64
+ }
65
+ const d = await rest("User/OAuth2", "GET");
66
+ usedProviders.value = {};
67
+ if (d && d.result == "success") {
68
+ data.value = d.data;
69
+ data.value.forEach((p: any) => {
70
+ usedProviders.value[p.ProviderUUID] = true;
71
+ });
72
+ }
73
+ eventBus.emit("main-loading", false);
74
+ };
75
+ onMounted(() => {
76
+ getOAuth2User();
77
+ getOAuth2Providers();
78
+ });
79
+ </script>
80
+
81
+ <template>
82
+ <div class="flex flex-col gap-3">
83
+ <DefaultModal id="providers" :title="$t('providers_modal_title')">
84
+ <template v-for="provider in providersData" :key="provider.UUID">
85
+ <div
86
+ class="flex items-center gap-3"
87
+ v-if="!usedProviders[provider.UUID]"
88
+ >
89
+ <button
90
+ @click="
91
+ () => {
92
+ getOAuth2Redirect(provider.UUID);
93
+ }
94
+ "
95
+ class="flex border border-fv-neutral-300 dark:border-fv-neutral-700 shadow items-center gap-2 justify-start btn neutral defaults w-full mx-auto !font-semibold"
96
+ :style="`background: ${
97
+ provider.Data.Button.button['background-color']
98
+ }; color: ${$getContrastingTextColor(
99
+ provider.Data.Button.button['background-color'],
100
+ )}`"
101
+ >
102
+ <img
103
+ :key="`${provider.Data.Button.label}oauth`"
104
+ class="h-12 w-12 block p-2 mr-3"
105
+ :alt="provider.Data.Button.info.Name"
106
+ :src="provider.Data.Button.button.logo"
107
+ />
108
+ <div>
109
+ {{
110
+ $t("user_flow_signin_with", {
111
+ provider: provider.Data.Button.name,
112
+ })
113
+ }}
114
+ </div>
115
+ </button>
116
+ </div>
117
+ </template>
118
+ </DefaultModal>
119
+ <h2 class="h3 flex items-center justify-between">
120
+ <span>{{ $t("oauth2_providers_title") }}</span>
121
+ <button
122
+ class="btn primary medium !py-1 !px-3"
123
+ @click="
124
+ () => {
125
+ $eventBus.emit('providersModal', true);
126
+ }
127
+ "
128
+ >
129
+ {{ $t("add_oauth2_con_cta") }}
130
+ </button>
131
+ </h2>
132
+ <p
133
+ class="text-red-900 dark:text-red-300 text-sm bg-red-200/[.2] dark:bg-red-900/[.2] p-2 rounded shadow"
134
+ v-if="
135
+ $route.query.error &&
136
+ $route.query.error === 'user_oauth2_connection_exists'
137
+ "
138
+ >
139
+ {{ $t("oauth2_error_user_oauth2_connection_exists") }}
140
+ </p>
141
+ <div v-if="data && data.length == 0">
142
+ <p>{{ $t("providers_empty") }}</p>
143
+ </div>
144
+ <div
145
+ v-for="provider in data"
146
+ class="flex items-center gap-3"
147
+ :key="provider.ProviderUUID"
148
+ >
149
+ <img
150
+ :src="provider.Provider.Button.button.logo"
151
+ class="w-14 h-14 p-1 rounded-full"
152
+ :style="`background-color: ${provider.Provider.Button.button['background-color']}`"
153
+ />
154
+ <div>
155
+ <h3 class="text-xl">
156
+ {{ provider.Provider.Button.name }}
157
+ <small class="text-xs">({{ provider.ServiceID }})</small>
158
+ </h3>
159
+ <div class="flex gap-2 mt-1">
160
+ <button
161
+ class="btn danger small"
162
+ @click="
163
+ () => {
164
+ deleteOAuth2Connection(provider.ProviderUUID);
165
+ }
166
+ "
167
+ >
168
+ {{ $t("remove_oauth2_con_cta") }}
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </template>
@@ -29,6 +29,7 @@ const classes = computed(() => `${baseClasses} ${colorClasses.value}`);
29
29
  @click.prevent="props.handleClick"
30
30
  :class="classes"
31
31
  role="menuitem"
32
+ type="button"
32
33
  >
33
34
  <slot></slot>
34
35
  </button>
@@ -26,18 +26,20 @@ const props = withDefaults(
26
26
  onClose?: Function;
27
27
  closeIcon?: Object;
28
28
  gridHeight?: number;
29
- mode: "mason" | "grid" | "button" | "hidden";
29
+ mode: "mason" | "grid" | "button" | "hidden" | "custom";
30
30
  paging?: APIPaging | undefined;
31
31
  buttonText?: string;
32
32
  buttonType?: string;
33
33
  modelValue: number;
34
34
  borderColor?: Function;
35
35
  imageLoader: string;
36
- videoComponent?: Component;
36
+ videoComponent?: Component | string;
37
+ imageComponent?: Component | string;
37
38
  isVideo?: Function;
38
39
  }>(),
39
40
  {
40
41
  modelValue: 0,
42
+ imageComponent: "img",
41
43
  mode: "grid",
42
44
  gridHeight: 4,
43
45
  closeIcon: () => h(XCircleIcon),
@@ -199,7 +201,7 @@ onUnmounted(() => {
199
201
  class="flex h-[100vh] relative flex-grow items-center justify-center gap-2"
200
202
  >
201
203
  <div
202
- class="hidden lg:relative lg:flex w-10 flex-shrink-0 items-center justify-center"
204
+ class="hidden lg:relative lg:flex w-10 flex-shrink-0 items-center justify-center flex-0"
203
205
  >
204
206
  <button
205
207
  class="btn p-1 rounded-full"
@@ -232,13 +234,23 @@ onUnmounted(() => {
232
234
  <img
233
235
  class="shadow max-w-full h-auto object-contain max-h-[85vh]"
234
236
  :src="modelValueSrc"
235
- v-if="modelValueSrc"
237
+ v-if="modelValueSrc && imageComponent == 'img'"
236
238
  @touchstart="touchStart"
237
239
  @touchend="touchEnd"
238
240
  />
241
+ <component
242
+ v-else-if="modelValueSrc && imageComponent"
243
+ :is="imageComponent"
244
+ :image="modelValueSrc.image"
245
+ :variant="modelValueSrc.variant"
246
+ :alt="modelValueSrc.alt"
247
+ class="shadow max-w-full h-auto object-contain max-h-[85vh]"
248
+ />
239
249
  </template>
240
250
  </div>
241
- <div class="flex-0 py-2 flex items-center justify-center">
251
+ <div
252
+ class="flex-0 py-2 flex items-center justify-center max-w-full w-full"
253
+ >
242
254
  <slot :value="images[modelValue]"></slot>
243
255
  </div>
244
256
  </div>
@@ -299,6 +311,16 @@ onUnmounted(() => {
299
311
  images[i - 1],
300
312
  )}`"
301
313
  :src="getThumbnailUrl(images[i - 1])"
314
+ v-if="imageComponent == 'img'"
315
+ />
316
+ <component
317
+ v-else
318
+ @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
319
+ :is="imageComponent"
320
+ :image="getThumbnailUrl(images[i - 1]).image"
321
+ :variant="getThumbnailUrl(images[i - 1]).variant"
322
+ :alt="getThumbnailUrl(images[i - 1]).alt"
323
+ class="h-auto max-w-full rounded-lg cursor-pointer shadow"
302
324
  />
303
325
  </div>
304
326
  </div>
@@ -307,12 +329,14 @@ onUnmounted(() => {
307
329
  </DialogPanel>
308
330
  </Dialog>
309
331
  </TransitionRoot>
310
- <template v-if="mode == 'grid' || mode == 'mason'">
332
+ <template v-if="mode == 'grid' || mode == 'mason' || mode == 'custom'">
311
333
  <div
312
- class="grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4"
313
334
  :class="{
314
- 'items-start': mode == 'mason',
315
- 'items-center': mode == 'grid',
335
+ 'grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-start':
336
+ mode == 'mason',
337
+ 'grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-center':
338
+ mode == 'grid',
339
+ 'custom-grid': mode == 'custom',
316
340
  }"
317
341
  >
318
342
  <template v-for="i in images.length" :key="`g_${id}_${i}`">
@@ -326,9 +350,18 @@ onUnmounted(() => {
326
350
  <img
327
351
  @click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
328
352
  class="h-auto max-w-full rounded-lg cursor-pointer"
329
- v-if="i + j - 2 < images.length"
353
+ v-if="i + j - 2 < images.length && imageComponent == 'img'"
330
354
  :src="getThumbnailUrl(images[i + j - 2])"
331
355
  />
356
+ <component
357
+ v-else-if="i + j - 2 < images.length"
358
+ :is="imageComponent"
359
+ :image="getThumbnailUrl(images[i + j - 2]).image"
360
+ :variant="getThumbnailUrl(images[i + j - 2]).variant"
361
+ :alt="getThumbnailUrl(images[i + j - 2]).alt"
362
+ class="h-auto max-w-full rounded-lg cursor-pointer"
363
+ @click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
364
+ />
332
365
  </div>
333
366
  </template>
334
367
  </div>
@@ -338,6 +371,16 @@ onUnmounted(() => {
338
371
  @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
339
372
  class="h-auto max-w-full rounded-lg cursor-pointer"
340
373
  :src="getThumbnailUrl(images[i - 1])"
374
+ v-if="imageComponent == 'img'"
375
+ />
376
+ <component
377
+ v-else-if="imageComponent"
378
+ :is="imageComponent"
379
+ :image="getThumbnailUrl(images[i - 1]).image"
380
+ :variant="getThumbnailUrl(images[i - 1]).variant"
381
+ :alt="getThumbnailUrl(images[i - 1]).alt"
382
+ class="h-auto max-w-full rounded-lg cursor-pointer"
383
+ @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
341
384
  />
342
385
  </div>
343
386
  </template>
@@ -145,6 +145,7 @@ defineExpose({ focus, blur, getInputRef });
145
145
  }"
146
146
  v-model="model"
147
147
  :autocomplete="autocomplete"
148
+ :placeholder="placeholder"
148
149
  :disabled="disabled"
149
150
  :aria-describedby="help ? `${id}-help` : id"
150
151
  class="bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg focus:ring-fv-primary-500 focus:border-fv-primary-500 block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500"
@@ -71,12 +71,11 @@ onUnmounted(() => {
71
71
  v-if="title"
72
72
  >
73
73
  <slot name="before"></slot>
74
- <DialogTitle
74
+ <h2
75
75
  class="text-xl font-semibold text-fv-neutral-900 dark:text-white"
76
76
  v-if="title"
77
- >
78
- {{ title }}
79
- </DialogTitle>
77
+ v-html="title"
78
+ />
80
79
  <button
81
80
  @click="setModal(false)"
82
81
  class="text-fv-neutral-400 bg-transparent hover:bg-fv-neutral-200 hover:text-fv-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center dark:hover:bg-fv-neutral-600 dark:hover:text-white"
@@ -20,8 +20,10 @@ const props = withDefaults(
20
20
  items: APIPaging;
21
21
  id: string;
22
22
  hash?: string;
23
+ showLegend?: boolean;
23
24
  }>(),
24
25
  {
26
+ showLegend: true,
25
27
  hash: "",
26
28
  },
27
29
  );
@@ -128,88 +130,97 @@ useFyHead({
128
130
  v-if="items && items.page_max > 1 && items.page_no"
129
131
  >
130
132
  <div class="paging-container">
131
- <nav
132
- aria-label="Pagination"
133
- class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
134
- >
135
- <a
136
- href="javascript:void(0);"
137
- @click="prev()"
138
- v-if="items.page_no >= 2"
139
- class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
140
- >
141
- <span class="sr-only">{{ $t("previous_paging") }}</span>
142
- <ChevronLeftIcon class="w-4 h-4" />
143
- </a>
144
- <a
145
- v-if="items.page_no - 2 > 1"
146
- class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
147
- href="javascript:void(0);"
148
- @click="page(1)"
149
- >
150
- 1
151
- </a>
152
- <span
153
- v-if="items.page_no - 2 > 2"
154
- class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-700 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-700 dark:hover:text-fv-neutral-100"
155
- >
156
- ...
157
- </span>
158
- <template v-for="i in 2">
159
- <a
160
- v-if="items.page_no - (3 - i) >= 1"
161
- class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
162
- href="javascript:void(0);"
163
- :key="`${i}-sm`"
164
- @click="page(items.page_no - (3 - i))"
165
- >
166
- {{ items.page_no - (3 - i) }}
167
- </a>
168
- </template>
169
- <a
170
- href="#"
171
- aria-current="page"
172
- class="z-10 relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium font-bold bg-fv-primary-50 border-fv-primary-500 text-fv-primary-600 hover:text-fv-neutral-600 dark:bg-fv-primary-900 dark:border-fv-primary-500 dark:text-fv-primary-200 dark:hover:text-fv-neutral-100"
173
- >
174
- {{ items.page_no }}
175
- </a>
176
- <template v-for="i in 2">
177
- <a
178
- v-if="items.page_no + i <= items.page_max"
179
- class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
180
- href="javascript:void(0);"
181
- :key="`${i}-md`"
182
- @click="page(items.page_no + i)"
183
- >
184
- {{ items.page_no + i }}
185
- </a>
186
- </template>
187
- <span
188
- v-if="items.page_no + 2 < items.page_max - 1"
189
- class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-700 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-700 dark:hover:text-fv-neutral-100"
190
- >
191
- ...
192
- </span>
193
- <a
194
- v-if="items.page_no + 2 < items.page_max"
195
- class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
196
- href="javascript:void(0);"
197
- @click="page(items.page_max)"
198
- >
199
- {{ items.page_max }}
200
- </a>
201
- <a
202
- href="javascript:void(0);"
203
- @click="next()"
204
- v-if="items.page_no < items.page_max - 1"
205
- class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
206
- >
207
- <span class="sr-only">{{ $t("next_paging") }}</span>
208
- <ChevronRightIcon class="w-4 h-4" />
209
- </a>
133
+ <nav aria-label="Pagination">
134
+ <ul class="flex items-center -space-x-px h-8 text-sm">
135
+ <li v-if="items.page_no >= 2">
136
+ <a
137
+ href="javascript:void(0);"
138
+ @click="prev()"
139
+ class="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
140
+ >
141
+ <span class="sr-only">{{ $t("previous_paging") }}</span>
142
+ <ChevronLeftIcon class="w-4 h-4" />
143
+ </a>
144
+ </li>
145
+ <li v-if="items.page_no - 2 > 1">
146
+ <a
147
+ class="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
148
+ href="javascript:void(0);"
149
+ @click="page(1)"
150
+ >
151
+ 1
152
+ </a>
153
+ </li>
154
+ <li v-if="items.page_no - 2 > 2">
155
+ <div
156
+ v-if="items.page_no - 2 > 2"
157
+ class="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
158
+ >
159
+ ...
160
+ </div>
161
+ </li>
162
+ <template v-for="i in 2">
163
+ <li v-if="items.page_no - (3 - i) >= 1" :key="`${i}-sm`">
164
+ <a
165
+ class="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
166
+ href="javascript:void(0);"
167
+ @click="page(items.page_no - (3 - i))"
168
+ >
169
+ {{ items.page_no - (3 - i) }}
170
+ </a>
171
+ </li>
172
+ </template>
173
+ <li>
174
+ <a
175
+ href="#"
176
+ aria-current="page"
177
+ class="z-10 flex items-center justify-center px-3 h-8 leading-tight text-primary-600 border border-primary-300 bg-primary-50 hover:bg-primary-100 hover:text-primary-700 dark:border-fv-neutral-700 dark:bg-fv-neutral-700 dark:text-white"
178
+ >
179
+ {{ items.page_no }}
180
+ </a>
181
+ </li>
182
+ <template v-for="i in 2">
183
+ <li :key="`${i}-md`" v-if="items.page_no + i <= items.page_max">
184
+ <a
185
+ class="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
186
+ href="javascript:void(0);"
187
+ @click="page(items.page_no + i)"
188
+ >
189
+ {{ items.page_no + i }}
190
+ </a>
191
+ </li>
192
+ </template>
193
+ <li v-if="items.page_no + 2 < items.page_max - 1">
194
+ <div
195
+ class="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
196
+ >
197
+ ...
198
+ </div>
199
+ </li>
200
+ <li v-if="items.page_no + 2 < items.page_max">
201
+ <a
202
+ class="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
203
+ href="javascript:void(0);"
204
+ @click="page(items.page_max)"
205
+ >
206
+ {{ items.page_max }}
207
+ </a>
208
+ </li>
209
+ <li v-if="items.page_no < items.page_max - 1">
210
+ <a
211
+ href="javascript:void(0);"
212
+ @click="next()"
213
+ class="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
214
+ >
215
+ <span class="sr-only">{{ $t("next_paging") }}</span>
216
+ <ChevronRightIcon class="w-4 h-4" />
217
+ </a>
218
+ </li>
219
+ </ul>
210
220
  </nav>
211
221
  <p
212
- class="text-xs text-center italic mt-0.5 text-fv-neutral-700 dark:text-fv-neutral-300"
222
+ class="text-xs text-fv-neutral-700 dark:text-fv-neutral-400 pt-0.5"
223
+ v-if="showLegend"
213
224
  >
214
225
  {{
215
226
  $t("global_paging", {
@@ -5,7 +5,7 @@
5
5
  @keydown.delete.prevent="removeLastTag"
6
6
  @keydown.enter.prevent="addTag"
7
7
  >
8
- <span v-for="(tag, index) in tags" :key="index" :class="`tag ${color}`">
8
+ <span v-for="(tag, index) in model" :key="index" :class="`tag ${color}`">
9
9
  {{ tag }}
10
10
  <button type="button" @click.prevent="removeTag(index)">
11
11
  <svg
@@ -37,7 +37,7 @@
37
37
  </template>
38
38
 
39
39
  <script setup lang="ts">
40
- import { ref, watch, onMounted } from "vue";
40
+ import { ref, computed, onMounted } from "vue";
41
41
  type colorType = "blue" | "red" | "green" | "purple" | "orange" | "neutral";
42
42
 
43
43
  const props = withDefaults(
@@ -59,17 +59,15 @@ const props = withDefaults(
59
59
  },
60
60
  );
61
61
 
62
- const emit = defineEmits(["update:modelValue"]);
63
- const tags = ref([...props.modelValue]);
64
62
  const textInput = ref<HTMLElement>();
65
63
 
66
- watch(
67
- tags,
68
- (newTags) => {
69
- emit("update:modelValue", newTags);
64
+ const emit = defineEmits(["update:modelValue"]);
65
+ const model = computed({
66
+ get: () => props.modelValue,
67
+ set: (items) => {
68
+ emit("update:modelValue", items);
70
69
  },
71
- { deep: true },
72
- );
70
+ });
73
71
 
74
72
  onMounted(() => {
75
73
  if (props.autofocus) {
@@ -92,23 +90,32 @@ const addTag = () => {
92
90
  .split(separatorsRegex)
93
91
  .map((tag: string) => tag.trim())
94
92
  .filter((tag: string) => tag.length > 0);
95
- tags.value.push(...newTags);
93
+ model.value.push(...newTags);
96
94
  textInput.value.innerText = "";
97
95
  };
98
96
 
99
97
  const removeTag = (index: number) => {
100
- tags.value.splice(index, 1);
98
+ model.value.splice(index, 1);
101
99
  focusInput();
102
100
  };
103
101
 
104
102
  const removeLastTag = () => {
105
103
  if (!textInput.value) return;
106
-
107
104
  if (textInput.value.innerText === "") {
108
- tags.value.pop();
105
+ model.value.pop();
106
+ } else {
107
+ const currentLength = textInput.value.innerText.length;
108
+ textInput.value.innerText = textInput.value.innerText.slice(0, -1);
109
+
110
+ const range = document.createRange();
111
+ const sel = window.getSelection();
112
+ range.selectNodeContents(textInput.value);
113
+ range.collapse(false);
114
+ if (!sel) return;
115
+ sel.removeAllRanges();
116
+ sel.addRange(range);
109
117
  }
110
118
  };
111
-
112
119
  const focusInput = () => {
113
120
  if (!textInput.value) return;
114
121
 
@@ -1,5 +1,5 @@
1
1
  import { RestMethod, RestParams, getMode, rest, stringHash } from "@fy-/fws-js";
2
- import { useRestStore } from "../stores/rest";
2
+ import { useServerRouter } from "../stores/serverRouter";
3
3
  import { isServerRendered } from "./ssr";
4
4
  import { useEventBus } from "./event-bus";
5
5
 
@@ -31,7 +31,7 @@ export function useRest(): <ResultType extends APIResult>(
31
31
  method: RestMethod,
32
32
  params?: RestParams,
33
33
  ) => Promise<ResultType> {
34
- const restStore = useRestStore();
34
+ const serverRouter = useServerRouter();
35
35
  const eventBus = useEventBus();
36
36
 
37
37
  return async <ResultType extends APIResult>(
@@ -39,13 +39,24 @@ export function useRest(): <ResultType extends APIResult>(
39
39
  method: RestMethod,
40
40
  params?: RestParams,
41
41
  ): Promise<ResultType> => {
42
- const requestHash = stringHash(url + method + JSON.stringify(params));
42
+ let urlForHash: string = url;
43
+ try {
44
+ const urlParse = new URL(url);
45
+ urlForHash = urlParse.pathname + urlParse.search;
46
+ } catch (error) {
47
+ urlForHash = url;
48
+ }
49
+
50
+ const requestHash = stringHash(
51
+ urlForHash + method + JSON.stringify(params),
52
+ );
43
53
  if (isServerRendered()) {
44
- const hasResult = restStore.getResult(requestHash);
54
+ const hasResult = serverRouter.getResult(requestHash);
45
55
  if (hasResult !== undefined) {
46
56
  const result = hasResult as ResultType;
47
- restStore.removeResult(requestHash);
57
+ serverRouter.removeResult(requestHash);
48
58
  if (result.result === "error") {
59
+ eventBus.emit("main-loading", false);
49
60
  eventBus.emit("rest-error", result);
50
61
  return Promise.reject(result);
51
62
  }
@@ -56,18 +67,23 @@ export function useRest(): <ResultType extends APIResult>(
56
67
  try {
57
68
  const restResult: ResultType = await rest(url, method, params);
58
69
  if (getMode() === "ssr") {
59
- restStore.addResult(
70
+ serverRouter.addResult(
60
71
  requestHash,
61
72
  JSON.parse(JSON.stringify(restResult)),
62
73
  );
63
74
  }
75
+ if (restResult.result === "error") {
76
+ eventBus.emit("main-loading", false);
77
+ eventBus.emit("rest-error", restResult);
78
+ return Promise.reject(restResult);
79
+ }
64
80
  return Promise.resolve(restResult);
65
81
  } catch (error) {
66
82
  const restError: ResultType = error as ResultType;
67
83
  if (getMode() === "ssr") {
68
- restStore.addResult(requestHash, restError);
84
+ serverRouter.addResult(requestHash, restError);
69
85
  }
70
-
86
+ eventBus.emit("main-loading", false);
71
87
  eventBus.emit("rest-error", restError);
72
88
  return Promise.resolve(restError);
73
89
  }
@@ -30,15 +30,16 @@ export interface LazyHead {
30
30
  export const useSeo = (seo: Ref<LazyHead>, initial: boolean = false) => {
31
31
  const currentUrl = `${getURL().Scheme}://${getURL().Host}${getURL().Path}`;
32
32
  const currentLocale = seo.value.locale || getLocale();
33
-
33
+ const actualCurrentURL = computed(() => seo.value.canonical || currentUrl);
34
34
  useFyHead({
35
35
  title: computed(() => seo.value.title),
36
+ scripts: seo.value.scripts,
36
37
  links: computed(() => {
37
38
  const links = [];
38
39
 
39
40
  links.push({
40
41
  rel: "canonical",
41
- href: currentUrl,
42
+ href: actualCurrentURL.value,
42
43
  key: "canonical",
43
44
  });
44
45
 
@@ -8,7 +8,15 @@ const cropText = (str: string, ml = 100, end = "...") => {
8
8
  }
9
9
  return str;
10
10
  };
11
+ const getContrastingTextColor = (backgroundColor: string) => {
12
+ const r = parseInt(backgroundColor.substring(1, 3), 16);
13
+ const g = parseInt(backgroundColor.substring(3, 5), 16);
14
+ const b = parseInt(backgroundColor.substring(5, 7), 16);
11
15
 
16
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
17
+
18
+ return luminance > 0.5 ? "#000000" : "#FFFFFF";
19
+ };
12
20
  const formatBytes = (bytes: number, decimals = 2) => {
13
21
  if (!+bytes) return "0 Bytes";
14
22
 
@@ -76,4 +84,11 @@ const formatTimeago = (dt: Date | string | number) => {
76
84
  return formatDateTimeago(new Date(_dt), getLocale().replace("_", "-"));
77
85
  };
78
86
 
79
- export { cropText, formatBytes, formatDate, formatDatetime, formatTimeago };
87
+ export {
88
+ cropText,
89
+ formatBytes,
90
+ formatDate,
91
+ formatDatetime,
92
+ formatTimeago,
93
+ getContrastingTextColor,
94
+ };
package/env.d.ts CHANGED
@@ -4,5 +4,3 @@ declare module "*.vue" {
4
4
  const component: DefineComponent<{}, {}, any>;
5
5
  export default component;
6
6
  }
7
-
8
- declare module "./presets/Wind";
package/index.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  isServerRendered,
12
12
  } from "./composables/ssr";
13
13
  import { useSeo } from "./composables/seo";
14
- import { useUserStore, useUserCheck } from "./stores/user";
14
+ import { useUserStore, useUserCheck, useUserCheckAsync } from "./stores/user";
15
15
  import { ClientOnly } from "./components/ssr/ClientOnly";
16
16
  import {
17
17
  cropText,
@@ -19,6 +19,7 @@ import {
19
19
  formatDate,
20
20
  formatDatetime,
21
21
  formatTimeago,
22
+ getContrastingTextColor,
22
23
  } from "./composables/templating";
23
24
  import { useRest } from "./composables/rest";
24
25
  export * from "./stores/serverRouter";
@@ -48,7 +49,8 @@ import DataTable from "./components/fws/DataTable.vue";
48
49
  import FilterData from "./components/fws/FilterData.vue";
49
50
  import CmsArticleBoxed from "./components/fws/CmsArticleBoxed.vue";
50
51
  import CmsArticleSingle from "./components/fws/CmsArticleSingle.vue";
51
-
52
+ import UserOAuth2 from "./components/fws/UserOAuth2.vue";
53
+ import UserData from "./components/fws/UserData.vue";
52
54
  // Css
53
55
  import "./style.css";
54
56
 
@@ -71,6 +73,8 @@ function createFWS(): Plugin {
71
73
  app.config.globalProperties.$formatTimeago = formatTimeago;
72
74
  app.config.globalProperties.$formatDatetime = formatDatetime;
73
75
  app.config.globalProperties.$formatDate = formatDate;
76
+ app.config.globalProperties.$getContrastingTextColor =
77
+ getContrastingTextColor;
74
78
 
75
79
  app.component("ClientOnly", ClientOnly);
76
80
  }
@@ -87,6 +91,7 @@ declare module "vue" {
87
91
  $formatTimeago: typeof formatTimeago;
88
92
  $formatDatetime: typeof formatDatetime;
89
93
  $formatDate: typeof formatDate;
94
+ $getContrastingTextColor: typeof getContrastingTextColor;
90
95
  }
91
96
  export interface GlobalComponents {
92
97
  ClientOnly: typeof ClientOnly;
@@ -105,6 +110,7 @@ export {
105
110
  useSeo,
106
111
  useUserStore,
107
112
  useUserCheck,
113
+ useUserCheckAsync,
108
114
  useRest,
109
115
 
110
116
  // Components
@@ -130,8 +136,10 @@ export {
130
136
 
131
137
  // FWS
132
138
  UserFlow,
139
+ UserOAuth2,
133
140
  DataTable,
134
141
  FilterData,
135
142
  CmsArticleBoxed,
136
143
  CmsArticleSingle,
144
+ UserData,
137
145
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "0.3.4",
3
+ "version": "0.3.8",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,6 +5,7 @@ export interface ServerRouterState {
5
5
  _router: any | null;
6
6
  status: number;
7
7
  redirect?: string;
8
+ results: Record<number, any | undefined>;
8
9
  }
9
10
 
10
11
  export const useServerRouter = defineStore({
@@ -14,6 +15,7 @@ export const useServerRouter = defineStore({
14
15
  _router: null,
15
16
  status: 200,
16
17
  redirect: undefined,
18
+ results: {},
17
19
  }) as ServerRouterState,
18
20
  getters: {
19
21
  currentRoute: (state) => state._router!.currentRoute,
@@ -45,5 +47,17 @@ export const useServerRouter = defineStore({
45
47
  forward() {
46
48
  this._router?.go(1);
47
49
  },
50
+ addResult(id: number, result: any) {
51
+ this.results[id] = result;
52
+ },
53
+ hasResult(id: number) {
54
+ return this.results[id] !== undefined;
55
+ },
56
+ getResult(id: number) {
57
+ return this.results[id];
58
+ },
59
+ removeResult(id: number) {
60
+ delete this.results[id];
61
+ },
48
62
  },
49
63
  });
package/stores/user.ts CHANGED
@@ -23,7 +23,6 @@ export const useUserStore = defineStore({
23
23
  actions: {
24
24
  async refreshUser() {
25
25
  const user: APIResult = await rest("User:get", "GET").catch((err) => {
26
- console.log(err);
27
26
  this.setUser(null);
28
27
  });
29
28
  if (user.result === "success") {
@@ -48,6 +47,34 @@ export const useUserStore = defineStore({
48
47
  },
49
48
  });
50
49
 
50
+ export async function useUserCheckAsync(path = "/login", redirectLink = false) {
51
+ const userStore = useUserStore();
52
+ await userStore.refreshUser();
53
+ const isAuth = computed(() => userStore.isAuth);
54
+ const router = useServerRouter();
55
+
56
+ const checkUser = (route: RouteLocation) => {
57
+ if (route.meta.reqLogin) {
58
+ if (!isAuth.value) {
59
+ if (!redirectLink) router.push(path);
60
+ else {
61
+ router.status = 307;
62
+ router.push(`${path}?return_to=${route.path}`);
63
+ }
64
+ }
65
+ }
66
+ };
67
+
68
+ router._router.afterEach(async () => {
69
+ await userStore.refreshUser();
70
+ });
71
+ router._router.beforeEach((to: any) => {
72
+ if (to.fullPath != path) {
73
+ checkUser(to);
74
+ }
75
+ });
76
+ }
77
+
51
78
  export function useUserCheck(path = "/login", redirectLink = false) {
52
79
  const userStore = useUserStore();
53
80
  const isAuth = computed(() => userStore.isAuth);
package/stores/rest.ts DELETED
@@ -1,27 +0,0 @@
1
- import { defineStore } from "pinia";
2
- import { APIResult } from "../composables/rest";
3
-
4
- type SharedState = {
5
- results: Record<number, any | undefined>;
6
- };
7
-
8
- export const useRestStore = defineStore({
9
- id: "restStore",
10
- state: (): SharedState => ({
11
- results: {},
12
- }),
13
- actions: {
14
- addResult(id: number, result: any) {
15
- this.results[id] = result;
16
- },
17
- hasResult(id: number) {
18
- return this.results[id] !== undefined;
19
- },
20
- getResult(id: number) {
21
- return this.results[id];
22
- },
23
- removeResult(id: number) {
24
- delete this.results[id];
25
- },
26
- },
27
- });