@fy-/fws-vue 0.3.78 → 0.4.0

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.
@@ -3,18 +3,22 @@ import useVuelidate from "@vuelidate/core";
3
3
  import DefaultInput from "../ui/DefaultInput.vue";
4
4
  import { useUserStore } from "../../stores/user";
5
5
  import { useRest } from "../../composables/rest";
6
+ import { useEventBus } from "../../composables/event-bus";
6
7
  import { computed, reactive, watchEffect } from "vue";
7
- import { required } from "@vuelidate/validators";
8
8
  const rest = useRest();
9
9
  const userStore = useUserStore();
10
10
  const userData = computed(() => userStore.user);
11
+ const eventBus = useEventBus();
11
12
  const state = reactive({
12
13
  userData: {
13
14
  Firstname: userData.value?.Firstname || "",
14
15
  Lastname: userData.value?.Lastname || "",
15
16
  Phone: userData.value?.Phone || "",
16
- Bio: userData.value?.Bio || "",
17
- Username: userData.value?.Username || "",
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,
18
22
  },
19
23
  });
20
24
  watchEffect(() => {
@@ -22,45 +26,41 @@ watchEffect(() => {
22
26
  Firstname: userData.value?.Firstname || "",
23
27
  Lastname: userData.value?.Lastname || "",
24
28
  Phone: userData.value?.Phone || "",
25
- Bio: userData.value?.Bio || "",
26
- Username: userData.value?.Username || "",
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,
27
34
  };
28
35
  });
29
36
  const rules = {
30
37
  userData: {
31
- Username: {
32
- required: required,
33
- },
34
38
  Firstname: {},
35
39
  Lastname: {},
36
40
  Phone: {},
37
41
  Bio: {},
42
+ AcceptedTerms: {},
43
+ EnabledNotifications: {},
44
+ EnabledEmails: {},
45
+ EnabledTrainingFromMyData: {},
38
46
  },
39
47
  };
40
48
  const v$ = useVuelidate(rules, state);
41
49
 
42
50
  const patchUser = async () => {
51
+ eventBus.emit("main-loading", true);
43
52
  if (await v$.value.userData.$validate()) {
44
53
  const response = await rest("User", "PATCH", state.userData);
45
54
  if (response && response.result == "success") {
46
- window.location.reload();
55
+ eventBus.emit("user:refresh", true);
47
56
  }
48
57
  }
58
+ eventBus.emit("main-loading", false);
49
59
  };
50
60
  </script>
51
61
 
52
62
  <template>
53
63
  <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
64
  <DefaultInput
65
65
  id="firstnameFWS"
66
66
  v-model="state.userData.Firstname"
@@ -88,6 +88,40 @@ const patchUser = async () => {
88
88
  :help="$t('fws_phone_help')"
89
89
  :error-vuelidate="v$.userData.Phone.$errors"
90
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
+
91
125
  <div class="flex">
92
126
  <button type="submit" class="btn defaults primary">
93
127
  {{ $t("fws_save_user_cta") }}
@@ -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>
@@ -0,0 +1,105 @@
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, ref } from "vue";
8
+ import { required } from "@vuelidate/validators";
9
+
10
+ const rest = useRest();
11
+ const userStore = useUserStore();
12
+ const userData = computed(() => userStore.user);
13
+ const eventBus = useEventBus();
14
+ const state = reactive({
15
+ userData: {
16
+ Username: userData.value?.UserProfile?.Username || "",
17
+ Gender: userData.value?.UserProfile?.Gender || "",
18
+ Birthdate: userData.value?.UserProfile?.Birthdate || "",
19
+ },
20
+ });
21
+ watchEffect(() => {
22
+ state.userData = {
23
+ Username: userData.value?.UserProfile?.Username || "",
24
+ Gender: userData.value?.UserProfile?.Gender || "",
25
+ Birthdate: userData.value?.UserProfile?.Birthdate
26
+ ? new Date(userData.value?.UserProfile?.Birthdate.unixms)
27
+ .toISOString()
28
+ .split("T")[0]
29
+ : "",
30
+ };
31
+ });
32
+ const rules = {
33
+ userData: {
34
+ Username: {
35
+ required: required,
36
+ },
37
+ Gender: {
38
+ required: required,
39
+ },
40
+ Birthdate: {
41
+ required: required,
42
+ },
43
+ },
44
+ };
45
+ const v$ = useVuelidate(rules, state);
46
+
47
+ const patchUser = async () => {
48
+ eventBus.emit("main-loading", true);
49
+ if (await v$.value.userData.$validate()) {
50
+ const data = { ...state.userData };
51
+ const birtdate = new Date(`${data.Birthdate}T00:00:00Z`);
52
+ const birtdateAtUnixms = birtdate.getTime();
53
+ // @ts-ignore
54
+ data.Birthdate = birtdateAtUnixms;
55
+ const response = await rest("User/_Profile", "PATCH", data);
56
+ if (response && response.result == "success") {
57
+ eventBus.emit("user:refresh", true);
58
+ }
59
+ }
60
+ eventBus.emit("main-loading", false);
61
+ };
62
+ </script>
63
+
64
+ <template>
65
+ <form @submit.prevent="patchUser">
66
+ <DefaultInput
67
+ id="usernameFWS"
68
+ v-model="state.userData.Username"
69
+ class="mb-4"
70
+ type="text"
71
+ :label="$t('fws_username_label')"
72
+ :help="$t('fws_username_help')"
73
+ :error-vuelidate="v$.userData.Username.$errors"
74
+ :disabled="userData?.UserProfile?.HasUsernameAndSlug ? true : false"
75
+ />
76
+ <DefaultInput
77
+ id="genderFWS"
78
+ v-model="state.userData.Gender"
79
+ class="mb-4"
80
+ type="select"
81
+ :options="[
82
+ ['female', $t('fws_persona_phys_appearance_opt_female')],
83
+ ['male', $t('fws_persona_phys_appearance_opt_male')],
84
+ ['non-binary', $t('fws_persona_phys_appearance_opt_non_binary')],
85
+ ]"
86
+ :label="$t('fws_gender_label')"
87
+ :error-vuelidate="v$.userData.Gender.$errors"
88
+ />
89
+ <!-- @vue-skip -->
90
+ <DefaultInput
91
+ id="birthdateFWS"
92
+ v-model="state.userData.Birthdate"
93
+ class="mb-4"
94
+ type="date"
95
+ :label="$t('fws_birthdate_label')"
96
+ :error-vuelidate="v$.userData.Birthdate.$errors"
97
+ />
98
+
99
+ <div class="flex">
100
+ <button type="submit" class="btn defaults primary">
101
+ {{ $t("fws_save_user_cta") }}
102
+ </button>
103
+ </div>
104
+ </form>
105
+ </template>
@@ -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: seo.value.canonical || currentUrl,
42
+ href: actualCurrentURL.value,
42
43
  key: "canonical",
43
44
  });
44
45
 
package/index.ts CHANGED
@@ -51,6 +51,8 @@ import CmsArticleBoxed from "./components/fws/CmsArticleBoxed.vue";
51
51
  import CmsArticleSingle from "./components/fws/CmsArticleSingle.vue";
52
52
  import UserOAuth2 from "./components/fws/UserOAuth2.vue";
53
53
  import UserData from "./components/fws/UserData.vue";
54
+ import UserProfile from "./components/fws/UserProfile.vue";
55
+ import UserProfileStrict from "./components/fws/UserProfileStrict.vue";
54
56
  // Css
55
57
  import "./style.css";
56
58
 
@@ -142,4 +144,6 @@ export {
142
144
  CmsArticleBoxed,
143
145
  CmsArticleSingle,
144
146
  UserData,
147
+ UserProfile,
148
+ UserProfileStrict,
145
149
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "0.3.78",
3
+ "version": "0.4.0",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "repository": {
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;
@@ -52,7 +53,11 @@ export async function useUserCheckAsync(path = "/login", redirectLink = false) {
52
53
  await userStore.refreshUser();
53
54
  const isAuth = computed(() => userStore.isAuth);
54
55
  const router = useServerRouter();
56
+ const eventBus = useEventBus();
55
57
 
58
+ eventBus.on("user:refresh", async () => {
59
+ await userStore.refreshUser();
60
+ });
56
61
  const checkUser = (route: RouteLocation) => {
57
62
  if (route.meta.reqLogin) {
58
63
  if (!isAuth.value) {
@@ -79,7 +84,11 @@ export function useUserCheck(path = "/login", redirectLink = false) {
79
84
  const userStore = useUserStore();
80
85
  const isAuth = computed(() => userStore.isAuth);
81
86
  const router = useServerRouter();
87
+ const eventBus = useEventBus();
82
88
 
89
+ eventBus.on("user:refresh", async () => {
90
+ await userStore.refreshUser();
91
+ });
83
92
  const checkUser = (route: RouteLocation) => {
84
93
  if (route.meta.reqLogin) {
85
94
  if (!isAuth.value) {