@fy-/fws-vue 0.3.5 → 0.3.9

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,131 @@
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 { useEventBus } from "../../composables/event-bus";
7
+ import { computed, reactive, watchEffect } from "vue";
8
+ const rest = useRest();
9
+ const userStore = useUserStore();
10
+ const userData = computed(() => userStore.user);
11
+ const eventBus = useEventBus();
12
+ const state = reactive({
13
+ userData: {
14
+ Firstname: userData.value?.Firstname || "",
15
+ Lastname: userData.value?.Lastname || "",
16
+ Phone: userData.value?.Phone || "",
17
+ AcceptedTerms: userData.value?.AcceptedTerms || false,
18
+ EnabledNotifications: userData.value?.EnabledNotifications || false,
19
+ EnabledEmails: userData.value?.EnabledEmails || false,
20
+ EnabledTrainingFromMyData:
21
+ userData.value?.EnabledTrainingFromMyData || false,
22
+ },
23
+ });
24
+ watchEffect(() => {
25
+ state.userData = {
26
+ Firstname: userData.value?.Firstname || "",
27
+ Lastname: userData.value?.Lastname || "",
28
+ Phone: userData.value?.Phone || "",
29
+ AcceptedTerms: userData.value?.AcceptedTerms || false,
30
+ EnabledNotifications: userData.value?.EnabledNotifications || false,
31
+ EnabledEmails: userData.value?.EnabledEmails || false,
32
+ EnabledTrainingFromMyData:
33
+ userData.value?.EnabledTrainingFromMyData || false,
34
+ };
35
+ });
36
+ const rules = {
37
+ userData: {
38
+ Firstname: {},
39
+ Lastname: {},
40
+ Phone: {},
41
+ Bio: {},
42
+ AcceptedTerms: {},
43
+ EnabledNotifications: {},
44
+ EnabledEmails: {},
45
+ EnabledTrainingFromMyData: {},
46
+ },
47
+ };
48
+ const v$ = useVuelidate(rules, state);
49
+
50
+ const patchUser = async () => {
51
+ eventBus.emit("main-loading", true);
52
+ if (await v$.value.userData.$validate()) {
53
+ const response = await rest("User", "PATCH", state.userData);
54
+ if (response && response.result == "success") {
55
+ eventBus.emit("user:refresh", true);
56
+ }
57
+ }
58
+ eventBus.emit("main-loading", false);
59
+ };
60
+ </script>
61
+
62
+ <template>
63
+ <form @submit.prevent="patchUser">
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
+ <DefaultInput
92
+ id="acceptedTermsFWS"
93
+ v-if="!userData?.AcceptedTerms"
94
+ v-model:checkbox-value="state.userData.AcceptedTerms"
95
+ type="toggle"
96
+ :label="$t('fws_accepted_terms_label')"
97
+ :help="$t('fws_accepted_terms_help')"
98
+ :error-vuelidate="v$.userData.AcceptedTerms.$errors"
99
+ />
100
+ <DefaultInput
101
+ id="enabledNotificationsFWS"
102
+ v-model:checkbox-value="state.userData.EnabledNotifications"
103
+ type="toggle"
104
+ :label="$t('fws_enabled_notifications_label')"
105
+ :help="$t('fws_enabled_notifications_help')"
106
+ :error-vuelidate="v$.userData.EnabledNotifications.$errors"
107
+ />
108
+ <DefaultInput
109
+ id="enabledEmailsFWS"
110
+ v-model:checkbox-value="state.userData.EnabledEmails"
111
+ type="toggle"
112
+ :label="$t('fws_enabled_emails_label')"
113
+ :help="$t('fws_enabled_emails_help')"
114
+ :error-vuelidate="v$.userData.EnabledEmails.$errors"
115
+ />
116
+ <DefaultInput
117
+ id="enabledTrainingFromMyDataFWS"
118
+ v-model:checkbox-value="state.userData.EnabledTrainingFromMyData"
119
+ type="toggle"
120
+ :label="$t('fws_enabled_training_from_my_data_label')"
121
+ :help="$t('fws_enabled_training_from_my_data_help')"
122
+ :error-vuelidate="v$.userData.EnabledTrainingFromMyData.$errors"
123
+ />
124
+
125
+ <div class="flex">
126
+ <button type="submit" class="btn defaults primary">
127
+ {{ $t("fws_save_user_cta") }}
128
+ </button>
129
+ </div>
130
+ </form>
131
+ </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>
@@ -0,0 +1,246 @@
1
+ <script setup lang="ts">
2
+ import useVuelidate from "@vuelidate/core";
3
+ import DefaultInput from "../ui/DefaultInput.vue";
4
+ import DefaultModal from "../ui/DefaultModal.vue";
5
+ import { useUserStore } from "../../stores/user";
6
+ import { useRest } from "../../composables/rest";
7
+ import { useEventBus } from "../../composables/event-bus";
8
+ import { computed, reactive, watchEffect, ref } from "vue";
9
+ import { required } from "@vuelidate/validators";
10
+ import VuePictureCropper, { cropper } from "vue-picture-cropper";
11
+ import { Uploader } from "@fy-/fws-js";
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ imageDomain?: string;
16
+ }>(),
17
+ {
18
+ imageDomain: "https://s.nocachenocry.com",
19
+ },
20
+ );
21
+ const rest = useRest();
22
+ const userStore = useUserStore();
23
+ const userData = computed(() => userStore.user);
24
+ const eventBus = useEventBus();
25
+ const state = reactive({
26
+ userData: {
27
+ Username: userData.value?.UserProfile?.Username || "",
28
+ Gender: userData.value?.UserProfile?.Gender || "",
29
+ Bio: userData.value?.UserProfile?.Bio || "",
30
+ Birthdate: userData.value?.UserProfile?.Birthdate || "",
31
+ PublicGender: userData.value?.UserProfile?.PublicGender || false,
32
+ PublicBio: userData.value?.UserProfile?.PublicBio || false,
33
+ PublicBirthdate: userData.value?.UserProfile?.PublicBirthdate || false,
34
+ },
35
+ });
36
+ watchEffect(() => {
37
+ state.userData = {
38
+ Username: userData.value?.UserProfile?.Username || "",
39
+ Gender: userData.value?.UserProfile?.Gender || "",
40
+ Bio: userData.value?.UserProfile?.Bio || "",
41
+ Birthdate: userData.value?.UserProfile?.Birthdate
42
+ ? new Date(userData.value?.UserProfile?.Birthdate.unixms)
43
+ .toISOString()
44
+ .split("T")[0]
45
+ : "",
46
+ PublicGender: userData.value?.UserProfile?.PublicGender || false,
47
+ PublicBio: userData.value?.UserProfile?.PublicBio || false,
48
+ PublicBirthdate: userData.value?.UserProfile?.PublicBirthdate || false,
49
+ };
50
+ });
51
+ const rules = {
52
+ userData: {
53
+ Username: {
54
+ required: required,
55
+ },
56
+ Gender: {},
57
+ Bio: {},
58
+ Birthdate: {},
59
+ PublicGender: {},
60
+ PublicBio: {},
61
+ PublicBirthdate: {},
62
+ },
63
+ };
64
+ const v$ = useVuelidate(rules, state);
65
+
66
+ const patchUser = async () => {
67
+ eventBus.emit("main-loading", true);
68
+ if (await v$.value.userData.$validate()) {
69
+ const data = { ...state.userData };
70
+ const birtdate = new Date(`${data.Birthdate}T00:00:00Z`);
71
+ const birtdateAtUnixms = birtdate.getTime();
72
+ // @ts-ignore
73
+ data.Birthdate = birtdateAtUnixms;
74
+ const response = await rest("User/_Profile", "PATCH", data);
75
+ if (response && response.result == "success") {
76
+ eventBus.emit("user:refresh", true);
77
+ }
78
+ }
79
+ eventBus.emit("main-loading", false);
80
+ };
81
+ const uploadInput = ref<HTMLInputElement | null>(null);
82
+ const pic = ref<string>("");
83
+ const cropResult = reactive({
84
+ dataURL: "",
85
+ blobURL: "",
86
+ });
87
+ const uploader = ref(new Uploader());
88
+
89
+ async function getCropResult() {
90
+ if (!cropper) return;
91
+ const base64 = cropper.getDataURL({});
92
+ const blob: Blob | null = await cropper.getBlob();
93
+ if (!blob) return;
94
+ eventBus.emit("main-loading", true);
95
+
96
+ const file = await cropper.getFile({
97
+ fileName: "avatar-" + userData.value?.UUID,
98
+ });
99
+
100
+ cropResult.dataURL = base64;
101
+ cropResult.blobURL = URL.createObjectURL(blob);
102
+ if (file) {
103
+ uploader.value.addFile(file);
104
+ }
105
+ const fileUploadCallback = (response: any) => {
106
+ eventBus.emit("avCropModal", false);
107
+ eventBus.emit("main-loading", false);
108
+ eventBus.emit("user:refresh", true);
109
+ };
110
+ uploader.value.startUpload(`/_special/rest/User/_Avatar`, fileUploadCallback);
111
+ }
112
+
113
+ function selectFile(e: Event) {
114
+ pic.value = "";
115
+ cropResult.dataURL = "";
116
+ cropResult.blobURL = "";
117
+
118
+ const { files } = e.target as HTMLInputElement;
119
+ if (!files || !files.length) return;
120
+
121
+ const file = files[0];
122
+ const reader = new FileReader();
123
+ reader.readAsDataURL(file);
124
+ reader.onload = () => {
125
+ pic.value = String(reader.result);
126
+ eventBus.emit("avCropModal", true);
127
+ if (!uploadInput.value) return;
128
+ uploadInput.value.value = "";
129
+ };
130
+ }
131
+ </script>
132
+
133
+ <template>
134
+ <form @submit.prevent="patchUser">
135
+ <DefaultInput
136
+ id="usernameFWS"
137
+ v-model="state.userData.Username"
138
+ class="mb-4"
139
+ type="text"
140
+ :label="$t('fws_username_label')"
141
+ :help="$t('fws_username_help')"
142
+ :error-vuelidate="v$.userData.Username.$errors"
143
+ :disabled="userData?.UserProfile?.HasUsernameAndSlug ? true : false"
144
+ />
145
+ <div class="flex gap-2 items-center mb-4">
146
+ <img
147
+ :src="`${imageDomain}/${userData?.UserProfile?.AvatarUUID}?vars=format=png:resize=100x100`"
148
+ class="w-16 h-16 rounded-full flex-0 shrink-0 grow-0"
149
+ v-if="userData?.UserProfile?.AvatarUUID"
150
+ />
151
+ <div class="flex-1">
152
+ <label
153
+ class="block text-sm font-medium mb-2 text-neutral-900 dark:text-white"
154
+ for="file_input"
155
+ >{{ $t("fws_upload_av_label") }}</label
156
+ >
157
+ <input
158
+ class="block text-sm w-full text-neutral-900 border border-neutral-300 rounded-lg cursor-pointer bg-neutral-50 dark:text-neutral-400 focus:outline-none dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400"
159
+ ref="uploadInput"
160
+ type="file"
161
+ accept="image/jpg, image/jpeg, image/png, image/gif"
162
+ @change="selectFile"
163
+ />
164
+ </div>
165
+ </div>
166
+ <DefaultModal id="avCrop" :title="$t('fws_crop_av_title')">
167
+ <button @click="getCropResult" class="btn defaults primary">
168
+ {{ $t("fws_crop_av_cta") }}
169
+ </button>
170
+ <div class="max-h-[80vh]">
171
+ <VuePictureCropper
172
+ :boxStyle="{
173
+ width: 'auto',
174
+ height: 'auto',
175
+ backgroundColor: '#f8f8f8',
176
+ margin: 'auto',
177
+ }"
178
+ :img="pic"
179
+ :options="{
180
+ viewMode: 1,
181
+ dragMode: 'crop',
182
+ aspectRatio: 1 / 1,
183
+ }"
184
+ class="max-h-[70vh] w-full"
185
+ />
186
+ </div>
187
+ </DefaultModal>
188
+ <DefaultInput
189
+ id="genderFWS"
190
+ v-model="state.userData.Gender"
191
+ class="mb-4"
192
+ type="select"
193
+ :options="[
194
+ ['female', $t('fws_persona_phys_appearance_opt_female')],
195
+ ['male', $t('fws_persona_phys_appearance_opt_male')],
196
+ ['non-binary', $t('fws_persona_phys_appearance_opt_non_binary')],
197
+ ]"
198
+ :label="$t('fws_gender_label')"
199
+ :error-vuelidate="v$.userData.Gender.$errors"
200
+ />
201
+ <!-- @vue-skip -->
202
+ <DefaultInput
203
+ id="birthdateFWS"
204
+ v-model="state.userData.Birthdate"
205
+ class="mb-4"
206
+ type="date"
207
+ :label="$t('fws_birthdate_label')"
208
+ :error-vuelidate="v$.userData.Birthdate.$errors"
209
+ />
210
+ <DefaultInput
211
+ id="bioFWS"
212
+ v-model="state.userData.Bio"
213
+ class="mb-4"
214
+ type="text"
215
+ :label="$t('fws_bio_label')"
216
+ :error-vuelidate="v$.userData.Bio.$errors"
217
+ />
218
+ <DefaultInput
219
+ id="publicGenderFWS"
220
+ v-model:checkbox-value="state.userData.PublicGender"
221
+ type="toggle"
222
+ :label="$t('fws_public_gender')"
223
+ :error-vuelidate="v$.userData.PublicGender.$errors"
224
+ />
225
+ <DefaultInput
226
+ id="publicBioFWS"
227
+ v-model:checkbox-value="state.userData.PublicBio"
228
+ type="toggle"
229
+ :label="$t('fws_public_bio')"
230
+ :error-vuelidate="v$.userData.PublicBio.$errors"
231
+ />
232
+ <DefaultInput
233
+ id="publicBirthdateFWS"
234
+ v-model:checkbox-value="state.userData.PublicBirthdate"
235
+ type="toggle"
236
+ :label="$t('fws_public_birthdate')"
237
+ :error-vuelidate="v$.userData.PublicBirthdate.$errors"
238
+ />
239
+
240
+ <div class="flex">
241
+ <button type="submit" class="btn defaults primary">
242
+ {{ $t("fws_save_user_cta") }}
243
+ </button>
244
+ </div>
245
+ </form>
246
+ </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>
@@ -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", {
@@ -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,9 @@ 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";
54
+ import UserProfile from "./components/fws/UserProfile.vue";
52
55
  // Css
53
56
  import "./style.css";
54
57
 
@@ -71,6 +74,8 @@ function createFWS(): Plugin {
71
74
  app.config.globalProperties.$formatTimeago = formatTimeago;
72
75
  app.config.globalProperties.$formatDatetime = formatDatetime;
73
76
  app.config.globalProperties.$formatDate = formatDate;
77
+ app.config.globalProperties.$getContrastingTextColor =
78
+ getContrastingTextColor;
74
79
 
75
80
  app.component("ClientOnly", ClientOnly);
76
81
  }
@@ -87,6 +92,7 @@ declare module "vue" {
87
92
  $formatTimeago: typeof formatTimeago;
88
93
  $formatDatetime: typeof formatDatetime;
89
94
  $formatDate: typeof formatDate;
95
+ $getContrastingTextColor: typeof getContrastingTextColor;
90
96
  }
91
97
  export interface GlobalComponents {
92
98
  ClientOnly: typeof ClientOnly;
@@ -105,6 +111,7 @@ export {
105
111
  useSeo,
106
112
  useUserStore,
107
113
  useUserCheck,
114
+ useUserCheckAsync,
108
115
  useRest,
109
116
 
110
117
  // Components
@@ -130,8 +137,11 @@ export {
130
137
 
131
138
  // FWS
132
139
  UserFlow,
140
+ UserOAuth2,
133
141
  DataTable,
134
142
  FilterData,
135
143
  CmsArticleBoxed,
136
144
  CmsArticleSingle,
145
+ UserData,
146
+ UserProfile,
137
147
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "0.3.5",
3
+ "version": "0.3.9",
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
@@ -5,6 +5,7 @@ import { rest } from "@fy-/fws-js";
5
5
  import { computed } from "vue";
6
6
  import { RouteLocation, useRouter } from "vue-router";
7
7
  import { useServerRouter } from "./serverRouter";
8
+ import { useEventBus } from "../composables/event-bus";
8
9
 
9
10
  export type UserStore = {
10
11
  user: User | null;
@@ -23,7 +24,6 @@ export const useUserStore = defineStore({
23
24
  actions: {
24
25
  async refreshUser() {
25
26
  const user: APIResult = await rest("User:get", "GET").catch((err) => {
26
- console.log(err);
27
27
  this.setUser(null);
28
28
  });
29
29
  if (user.result === "success") {
@@ -48,11 +48,47 @@ export const useUserStore = defineStore({
48
48
  },
49
49
  });
50
50
 
51
+ export async function useUserCheckAsync(path = "/login", redirectLink = false) {
52
+ const userStore = useUserStore();
53
+ await userStore.refreshUser();
54
+ const isAuth = computed(() => userStore.isAuth);
55
+ const router = useServerRouter();
56
+ const eventBus = useEventBus();
57
+
58
+ eventBus.on("user:refresh", async () => {
59
+ await userStore.refreshUser();
60
+ });
61
+ const checkUser = (route: RouteLocation) => {
62
+ if (route.meta.reqLogin) {
63
+ if (!isAuth.value) {
64
+ if (!redirectLink) router.push(path);
65
+ else {
66
+ router.status = 307;
67
+ router.push(`${path}?return_to=${route.path}`);
68
+ }
69
+ }
70
+ }
71
+ };
72
+
73
+ router._router.afterEach(async () => {
74
+ await userStore.refreshUser();
75
+ });
76
+ router._router.beforeEach((to: any) => {
77
+ if (to.fullPath != path) {
78
+ checkUser(to);
79
+ }
80
+ });
81
+ }
82
+
51
83
  export function useUserCheck(path = "/login", redirectLink = false) {
52
84
  const userStore = useUserStore();
53
85
  const isAuth = computed(() => userStore.isAuth);
54
86
  const router = useServerRouter();
87
+ const eventBus = useEventBus();
55
88
 
89
+ eventBus.on("user:refresh", async () => {
90
+ await userStore.refreshUser();
91
+ });
56
92
  const checkUser = (route: RouteLocation) => {
57
93
  if (route.meta.reqLogin) {
58
94
  if (!isAuth.value) {
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
- });