@fy-/fws-vue 2.3.36 → 2.3.38

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.
@@ -72,72 +72,96 @@ async function patchUser() {
72
72
  </script>
73
73
 
74
74
  <template>
75
- <form @submit.prevent="patchUser">
76
- <DefaultInput
77
- id="firstnameFWS"
78
- v-model="state.userData.Firstname"
79
- class="mb-4"
80
- type="text"
81
- :label="$t('fws_firstname_label')"
82
- :help="$t('fws_firstname_help')"
83
- :error-vuelidate="v$.userData.Firstname.$errors"
84
- />
85
- <DefaultInput
86
- id="lastnameFWS"
87
- v-model="state.userData.Lastname"
88
- class="mb-4"
89
- type="text"
90
- :label="$t('fws_lastname_label')"
91
- :help="$t('fws_lastname_help')"
92
- :error-vuelidate="v$.userData.Lastname.$errors"
93
- />
94
- <DefaultInput
95
- id="phoneFWS"
96
- v-model="state.userData.Phone"
97
- class="mb-4"
98
- type="text"
99
- :label="$t('fws_phone_label')"
100
- :help="$t('fws_phone_help')"
101
- :error-vuelidate="v$.userData.Phone.$errors"
102
- />
103
- <DefaultInput
104
- v-if="!userData?.AcceptedTerms"
105
- id="acceptedTermsFWS"
106
- v-model:checkbox-value="state.userData.AcceptedTerms"
107
- type="toggle"
108
- :label="$t('fws_accepted_terms_label')"
109
- :help="$t('fws_accepted_terms_help')"
110
- :error-vuelidate="v$.userData.AcceptedTerms.$errors"
111
- />
112
- <DefaultInput
113
- id="enabledNotificationsFWS"
114
- v-model:checkbox-value="state.userData.EnabledNotifications"
115
- type="toggle"
116
- :label="$t('fws_enabled_notifications_label')"
117
- :help="$t('fws_enabled_notifications_help')"
118
- :error-vuelidate="v$.userData.EnabledNotifications.$errors"
119
- />
120
- <DefaultInput
121
- id="enabledEmailsFWS"
122
- v-model:checkbox-value="state.userData.EnabledEmails"
123
- type="toggle"
124
- :label="$t('fws_enabled_emails_label')"
125
- :help="$t('fws_enabled_emails_help')"
126
- :error-vuelidate="v$.userData.EnabledEmails.$errors"
127
- />
128
- <DefaultInput
129
- id="enabledTrainingFromMyDataFWS"
130
- v-model:checkbox-value="state.userData.EnabledTrainingFromMyData"
131
- type="toggle"
132
- :label="$t('fws_enabled_training_from_my_data_label')"
133
- :help="$t('fws_enabled_training_from_my_data_help')"
134
- :error-vuelidate="v$.userData.EnabledTrainingFromMyData.$errors"
135
- />
75
+ <form class="space-y-4" @submit.prevent="patchUser">
76
+ <div class="bg-white dark:bg-fv-neutral-900 p-4 sm:p-6 rounded-lg shadow-sm border border-fv-neutral-200 dark:border-fv-neutral-700">
77
+ <h3 class="text-lg font-semibold text-fv-neutral-900 dark:text-white mb-4 sm:mb-5 pb-2 border-b border-fv-neutral-200 dark:border-fv-neutral-700">
78
+ {{ $t('fws_personal_data_title') || $t('fws_personal_information') }}
79
+ </h3>
136
80
 
137
- <div class="flex">
138
- <button type="submit" class="btn defaults primary">
139
- {{ $t("fws_save_user_cta") }}
140
- </button>
81
+ <!-- Personal Information Section -->
82
+ <div class="grid gap-4 md:grid-cols-2 mb-4 sm:mb-6">
83
+ <DefaultInput
84
+ id="firstnameFWS"
85
+ v-model="state.userData.Firstname"
86
+ type="text"
87
+ :label="$t('fws_firstname_label')"
88
+ :help="$t('fws_firstname_help')"
89
+ :error-vuelidate="v$.userData.Firstname.$errors"
90
+ />
91
+ <DefaultInput
92
+ id="lastnameFWS"
93
+ v-model="state.userData.Lastname"
94
+ type="text"
95
+ :label="$t('fws_lastname_label')"
96
+ :help="$t('fws_lastname_help')"
97
+ :error-vuelidate="v$.userData.Lastname.$errors"
98
+ />
99
+ <DefaultInput
100
+ id="phoneFWS"
101
+ v-model="state.userData.Phone"
102
+ type="text"
103
+ :label="$t('fws_phone_label')"
104
+ :help="$t('fws_phone_help')"
105
+ :error-vuelidate="v$.userData.Phone.$errors"
106
+ />
107
+ </div>
108
+
109
+ <!-- Terms & Preferences -->
110
+ <div class="pt-3 sm:pt-4 border-t border-fv-neutral-200 dark:border-fv-neutral-700">
111
+ <h4 class="font-medium text-fv-neutral-800 dark:text-white mb-2 sm:mb-3">
112
+ {{ $t('fws_preferences_and_settings') }}
113
+ </h4>
114
+
115
+ <div class="grid gap-2 sm:gap-3 p-1 sm:p-2">
116
+ <DefaultInput
117
+ v-if="!userData?.AcceptedTerms"
118
+ id="acceptedTermsFWS"
119
+ v-model:checkbox-value="state.userData.AcceptedTerms"
120
+ type="toggle"
121
+ :label="$t('fws_accepted_terms_label')"
122
+ :help="$t('fws_accepted_terms_help')"
123
+ :error-vuelidate="v$.userData.AcceptedTerms.$errors"
124
+ />
125
+
126
+ <div class="grid gap-3 md:grid-cols-2 my-1">
127
+ <DefaultInput
128
+ id="enabledNotificationsFWS"
129
+ v-model:checkbox-value="state.userData.EnabledNotifications"
130
+ type="toggle"
131
+ :label="$t('fws_enabled_notifications_label')"
132
+ :help="$t('fws_enabled_notifications_help')"
133
+ :error-vuelidate="v$.userData.EnabledNotifications.$errors"
134
+ />
135
+ <DefaultInput
136
+ id="enabledEmailsFWS"
137
+ v-model:checkbox-value="state.userData.EnabledEmails"
138
+ type="toggle"
139
+ :label="$t('fws_enabled_emails_label')"
140
+ :help="$t('fws_enabled_emails_help')"
141
+ :error-vuelidate="v$.userData.EnabledEmails.$errors"
142
+ />
143
+ </div>
144
+
145
+ <DefaultInput
146
+ id="enabledTrainingFromMyDataFWS"
147
+ v-model:checkbox-value="state.userData.EnabledTrainingFromMyData"
148
+ type="toggle"
149
+ :label="$t('fws_enabled_training_from_my_data_label')"
150
+ :help="$t('fws_enabled_training_from_my_data_help')"
151
+ :error-vuelidate="v$.userData.EnabledTrainingFromMyData.$errors"
152
+ />
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Save Button -->
157
+ <div class="flex justify-end mt-6">
158
+ <button type="submit" class="btn defaults primary flex items-center gap-2">
159
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
160
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
161
+ </svg>
162
+ {{ $t("fws_save_user_cta") }}
163
+ </button>
164
+ </div>
141
165
  </div>
142
166
  </form>
143
167
  </template>
@@ -14,6 +14,12 @@ const isAuth = computed(() => userStore.isAuth)
14
14
  const data = ref()
15
15
  const providersData = ref()
16
16
  const usedProviders = ref<Record<string, boolean>>({})
17
+
18
+ // Check if there are any available providers that aren't connected yet
19
+ const hasAvailableProviders = computed(() => {
20
+ if (!providersData.value || providersData.value.length === 0) return false
21
+ return providersData.value.some(provider => !usedProviders.value[provider.UUID])
22
+ })
17
23
  const props = defineProps({
18
24
  returnTo: {
19
25
  type: String,
@@ -81,93 +87,170 @@ onMounted(() => {
81
87
 
82
88
  <template>
83
89
  <div class="flex flex-col gap-3">
90
+ <!-- Provider Selection Modal -->
84
91
  <DefaultModal id="providers" :title="$t('providers_modal_title')">
85
- <template v-for="provider in providersData" :key="provider.UUID">
86
- <div
87
- v-if="!usedProviders[provider.UUID]"
88
- class="flex items-center gap-3"
89
- >
90
- <button
91
- 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"
92
- :style="`background: ${
93
- provider.Data.Button.button['background-color']
94
- }; color: ${$getContrastingTextColor(
95
- provider.Data.Button.button['background-color'],
96
- )}`"
97
- @click="
98
- () => {
99
- getOAuth2Redirect(provider.UUID);
100
- }
101
- "
102
- >
103
- <img
104
- :key="`${provider.Data.Button.label}oauth`"
105
- class="h-12 w-12 block p-2 mr-3"
106
- :alt="provider.Data.Button.info.Name"
107
- :src="provider.Data.Button.button.logo"
92
+ <div class="grid gap-4 p-1">
93
+ <!-- Check if there are available providers to connect -->
94
+ <div v-if="providersData && providersData.length > 0">
95
+ <div v-if="!hasAvailableProviders" class="text-center py-6">
96
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-fv-neutral-400 dark:text-fv-neutral-500 mb-3" viewBox="0 0 20 20" fill="currentColor">
97
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z" clip-rule="evenodd" />
98
+ </svg>
99
+ <p class="text-fv-neutral-700 dark:text-fv-neutral-300 mb-1 font-medium">
100
+ {{ $t('account_already_connected_to_all_providers') || 'All services are connected' }}
101
+ </p>
102
+ <p class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400">
103
+ {{ $t('no_more_available_providers') || 'Your account is already connected to all available services' }}
104
+ </p>
105
+ </div>
106
+
107
+ <template v-for="provider in providersData" v-else :key="provider.UUID">
108
+ <div
109
+ v-if="!usedProviders[provider.UUID]"
110
+ class="flex items-center"
108
111
  >
109
- <div>
110
- {{
111
- $t("user_flow_signin_with", {
112
- provider: provider.Data.Button.name,
113
- })
114
- }}
112
+ <button
113
+ class="flex border border-fv-neutral-300 dark:border-fv-neutral-700 rounded-lg shadow-sm hover:shadow items-center gap-2 justify-start w-full mx-auto !font-medium p-3 transition-all duration-200"
114
+ :style="`background: ${
115
+ provider.Data.Button.button['background-color']
116
+ }; color: ${$getContrastingTextColor(
117
+ provider.Data.Button.button['background-color'],
118
+ )}`"
119
+ @click="
120
+ () => {
121
+ getOAuth2Redirect(provider.UUID);
122
+ }
123
+ "
124
+ >
125
+ <img
126
+ :key="`${provider.Data.Button.label}oauth`"
127
+ class="h-10 w-10 block p-1.5 mr-3 rounded"
128
+ :alt="provider.Data.Button.info.Name"
129
+ :src="provider.Data.Button.button.logo"
130
+ >
131
+ <div class="text-base">
132
+ {{
133
+ $t("user_flow_signin_with", {
134
+ provider: provider.Data.Button.name,
135
+ })
136
+ }}
137
+ </div>
138
+ </button>
115
139
  </div>
116
- </button>
140
+ </template>
141
+ </div>
142
+
143
+ <!-- Loading or no providers available -->
144
+ <div v-else class="text-center py-4">
145
+ <p class="text-fv-neutral-500 dark:text-fv-neutral-400">
146
+ {{ $t('loading_providers') || 'Loading available services...' }}
147
+ </p>
117
148
  </div>
118
- </template>
149
+ </div>
119
150
  </DefaultModal>
120
- <h2 class="h3 flex items-center justify-between">
121
- <span>{{ $t("oauth2_providers_title") }}</span>
122
- <button
123
- class="btn primary medium !py-1 !px-3"
124
- @click="
125
- () => {
126
- $eventBus.emit('providersModal', true);
127
- }
128
- "
151
+
152
+ <!-- Main Container -->
153
+ <div class="bg-white dark:bg-fv-neutral-900 rounded-lg shadow-sm border border-fv-neutral-200 dark:border-fv-neutral-700 p-4 sm:p-6">
154
+ <!-- Header -->
155
+ <div class="flex items-center justify-between mb-4 sm:mb-6 pb-2 sm:pb-3 border-b border-fv-neutral-200 dark:border-fv-neutral-700">
156
+ <h2 class="text-lg font-semibold text-fv-neutral-900 dark:text-white">
157
+ {{ $t("oauth2_providers_title") }}
158
+ </h2>
159
+ <button
160
+ class="btn primary small flex items-center gap-2"
161
+ @click="
162
+ () => {
163
+ $eventBus.emit('providersModal', true);
164
+ }
165
+ "
166
+ >
167
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
168
+ <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
169
+ </svg>
170
+ {{ $t("add_oauth2_con_cta") }}
171
+ </button>
172
+ </div>
173
+
174
+ <!-- Error Message -->
175
+ <div
176
+ v-if="$route.query.error && $route.query.error === 'user_oauth2_connection_exists'"
177
+ class="mb-3 sm:mb-4 p-2 sm:p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
129
178
  >
130
- {{ $t("add_oauth2_con_cta") }}
131
- </button>
132
- </h2>
133
- <p
134
- v-if="
135
- $route.query.error
136
- && $route.query.error === 'user_oauth2_connection_exists'
137
- "
138
- class="text-red-900 dark:text-red-300 text-sm bg-red-200/[.2] dark:bg-red-900/[.2] p-2 rounded shadow"
139
- >
140
- {{ $t("oauth2_error_user_oauth2_connection_exists") }}
141
- </p>
142
- <div v-if="data && data.length === 0">
143
- <p>{{ $t("providers_empty") }}</p>
144
- </div>
145
- <div
146
- v-for="provider in data"
147
- :key="provider.ProviderUUID"
148
- class="flex items-center gap-3"
149
- >
150
- <img
151
- :src="provider.Provider.Button.button.logo"
152
- class="w-14 h-14 p-1 rounded-full"
153
- :style="`background-color: ${provider.Provider.Button.button['background-color']}`"
179
+ <div class="flex items-start">
180
+ <svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
181
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
182
+ </svg>
183
+ <p class="text-sm text-red-700 dark:text-red-300">
184
+ {{ $t("oauth2_error_user_oauth2_connection_exists") }}
185
+ </p>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Empty State -->
190
+ <div
191
+ v-if="data && data.length === 0"
192
+ class="py-6 sm:py-8 px-3 sm:px-4 text-center bg-fv-neutral-50 dark:bg-fv-neutral-700/30 rounded-lg border border-fv-neutral-200 dark:border-fv-neutral-700"
154
193
  >
155
- <div>
156
- <h3 class="text-xl">
157
- {{ provider.Provider.Button.name }}
158
- <small class="text-xs">({{ provider.ServiceID }})</small>
159
- </h3>
160
- <div class="flex gap-2 mt-1">
161
- <button
162
- class="btn danger small"
163
- @click="
164
- () => {
165
- deleteOAuth2Connection(provider.ProviderUUID);
166
- }
167
- "
168
- >
169
- {{ $t("remove_oauth2_con_cta") }}
170
- </button>
194
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-fv-neutral-400 dark:text-fv-neutral-500 mb-3" viewBox="0 0 20 20" fill="currentColor">
195
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
196
+ </svg>
197
+ <p class="text-fv-neutral-700 dark:text-fv-neutral-300">
198
+ {{ $t("providers_empty") }}
199
+ </p>
200
+ <button
201
+ class="btn primary small mt-3"
202
+ @click="
203
+ () => {
204
+ $eventBus.emit('providersModal', true);
205
+ }
206
+ "
207
+ >
208
+ {{ $t("add_oauth2_con_cta") }}
209
+ </button>
210
+ </div>
211
+
212
+ <!-- Connected Providers List -->
213
+ <div v-else class="grid gap-3 sm:gap-4">
214
+ <div
215
+ v-for="provider in data"
216
+ :key="provider.ProviderUUID"
217
+ class="flex items-center p-3 sm:p-4 bg-fv-neutral-50 dark:bg-fv-neutral-700/30 rounded-lg border border-fv-neutral-200 dark:border-fv-neutral-700 transition-all duration-200 hover:shadow-sm"
218
+ >
219
+ <div class="flex-shrink-0 mr-4">
220
+ <div
221
+ class="w-14 h-14 rounded-full flex items-center justify-center p-0.5"
222
+ :style="`background-color: ${provider.Provider.Button.button['background-color']}`"
223
+ >
224
+ <img
225
+ :src="provider.Provider.Button.button.logo"
226
+ class="w-10 h-10 object-contain"
227
+ :alt="provider.Provider.Button.name"
228
+ >
229
+ </div>
230
+ </div>
231
+ <div class="flex-1 min-w-0">
232
+ <h3 class="text-lg font-medium text-fv-neutral-900 dark:text-white truncate">
233
+ {{ provider.Provider.Button.name }}
234
+ </h3>
235
+ <p class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400 truncate">
236
+ {{ provider.ServiceID }}
237
+ </p>
238
+ </div>
239
+ <div class="ml-4">
240
+ <button
241
+ class="btn danger small flex items-center gap-1.5"
242
+ @click="
243
+ () => {
244
+ deleteOAuth2Connection(provider.ProviderUUID);
245
+ }
246
+ "
247
+ >
248
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
249
+ <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
250
+ </svg>
251
+ {{ $t("remove_oauth2_con_cta") }}
252
+ </button>
253
+ </div>
171
254
  </div>
172
255
  </div>
173
256
  </div>
@@ -160,118 +160,176 @@ function selectFile(e: Event) {
160
160
  </script>
161
161
 
162
162
  <template>
163
- <form @submit.prevent="patchUser">
164
- <DefaultInput
165
- id="usernameFWS"
166
- v-model="state.userData.Username"
167
- class="mb-4"
168
- type="text"
169
- :label="$t('fws_username_label')"
170
- :help="$t('fws_username_help')"
171
- :error-vuelidate="v$.userData.Username.$errors"
172
- :disabled="userData?.UserProfile?.HasUsernameAndSlug ? true : false"
173
- />
174
- <div class="flex gap-2 items-center mb-4">
175
- <img
176
- v-if="userData?.UserProfile?.AvatarUUID"
177
- :src="`${imageDomain}/${userData?.UserProfile?.AvatarUUID}?vars=format=png:resize=100x100`"
178
- class="w-16 h-16 rounded-full flex-0 shrink-0 grow-0"
179
- >
180
- <div class="flex-1">
181
- <label
182
- class="block text-sm font-medium mb-2 text-neutral-900 dark:text-white"
183
- for="file_input"
184
- >{{ $t("fws_upload_av_label") }}</label>
185
- <input
186
- ref="uploadInput"
187
- 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"
188
- type="file"
189
- accept="image/jpg, image/jpeg, image/png, image/gif"
190
- @change="selectFile"
191
- >
163
+ <form class="space-y-4" @submit.prevent="patchUser">
164
+ <div class="bg-white dark:bg-fv-neutral-900 p-4 sm:p-6 rounded-lg shadow-sm border border-fv-neutral-200 dark:border-fv-neutral-700">
165
+ <h3 class="text-lg font-semibold text-fv-neutral-900 dark:text-white mb-4 pb-2 border-b border-fv-neutral-200 dark:border-fv-neutral-700">
166
+ {{ $t('fws_profile_heading') || $t('fws_your_profile') }}
167
+ </h3>
168
+
169
+ <!-- Username and Avatar Section -->
170
+ <div class="grid md:grid-cols-2 gap-4 sm:gap-6 mb-4 sm:mb-6">
171
+ <!-- Avatar Upload -->
172
+ <div class="flex flex-col">
173
+ <h4 class="font-medium text-fv-neutral-800 dark:text-white text-sm mb-3">
174
+ {{ $t("fws_profile_image") }}
175
+ </h4>
176
+ <div class="flex items-center gap-3 sm:gap-4">
177
+ <div class="relative group">
178
+ <img
179
+ v-if="userData?.UserProfile?.AvatarUUID"
180
+ :src="`${imageDomain}/${userData?.UserProfile?.AvatarUUID}?vars=format=png:resize=100x100`"
181
+ class="w-20 h-20 rounded-full object-cover border-2 border-fv-neutral-200 dark:border-fv-neutral-700 shadow-sm"
182
+ alt="Profile Avatar"
183
+ >
184
+ <div v-else class="w-20 h-20 rounded-full bg-fv-neutral-200 dark:bg-fv-neutral-700 flex items-center justify-center">
185
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-fv-neutral-400 dark:text-fv-neutral-500" viewBox="0 0 20 20" fill="currentColor">
186
+ <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
187
+ </svg>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="flex-1">
192
+ <label
193
+ class="block text-sm font-medium mb-2 text-fv-neutral-800 dark:text-white"
194
+ for="file_input"
195
+ >{{ $t("fws_upload_av_label") }}</label>
196
+ <div class="relative">
197
+ <input
198
+ id="file_input"
199
+ ref="uploadInput"
200
+ class="block text-sm w-full text-fv-neutral-700 border border-fv-neutral-300 rounded-lg cursor-pointer bg-fv-neutral-50 dark:text-fv-neutral-400 focus:outline-none dark:bg-fv-neutral-700 dark:border-fv-neutral-600 file:mr-4 file:py-2 file:px-4 file:border-0 file:text-sm file:font-medium file:bg-fv-primary-50 file:text-fv-primary-600 dark:file:bg-fv-primary-900 dark:file:text-fv-primary-300 hover:file:bg-fv-primary-100 dark:hover:file:bg-fv-primary-800"
201
+ type="file"
202
+ accept="image/jpg, image/jpeg, image/png, image/gif"
203
+ @change="selectFile"
204
+ >
205
+ </div>
206
+ <p class="mt-1 text-xs text-fv-neutral-500 dark:text-fv-neutral-400">
207
+ {{ $t("fws_upload_av_help") || "JPG, JPEG, PNG or GIF (max 2MB)" }}
208
+ </p>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Username -->
214
+ <div class="flex flex-col justify-center">
215
+ <DefaultInput
216
+ id="usernameFWS"
217
+ v-model="state.userData.Username"
218
+ type="text"
219
+ :label="$t('fws_username_label')"
220
+ :help="$t('fws_username_help')"
221
+ :error-vuelidate="v$.userData.Username.$errors"
222
+ :disabled="userData?.UserProfile?.HasUsernameAndSlug ? true : false"
223
+ />
224
+ </div>
192
225
  </div>
193
- </div>
194
- <DefaultModal id="avCrop" :title="$t('fws_crop_av_title')">
195
- <button class="btn defaults primary" @click="getCropResult">
196
- {{ $t("fws_crop_av_cta") }}
197
- </button>
198
- <div class="max-h-[80vh]">
199
- <VuePictureCropper
200
- :box-style="{
201
- width: 'auto',
202
- height: 'auto',
203
- backgroundColor: '#f8f8f8',
204
- margin: 'auto',
205
- }"
206
- :img="pic"
207
- :options="{
208
- viewMode: 1,
209
- dragMode: 'crop',
210
- aspectRatio: 1 / 1,
211
- }"
212
- class="max-h-[70vh] w-full"
226
+
227
+ <!-- Cropper Modal -->
228
+ <DefaultModal id="avCrop" :title="$t('fws_crop_av_title')">
229
+ <div class="flex flex-col gap-4">
230
+ <div class="max-h-[70vh] overflow-hidden rounded-lg border border-fv-neutral-200 dark:border-fv-neutral-700">
231
+ <VuePictureCropper
232
+ :box-style="{
233
+ width: 'auto',
234
+ height: 'auto',
235
+ backgroundColor: '#f8f8f8',
236
+ margin: 'auto',
237
+ }"
238
+ :img="pic"
239
+ :options="{
240
+ viewMode: 1,
241
+ dragMode: 'crop',
242
+ aspectRatio: 1 / 1,
243
+ }"
244
+ class="max-h-[70vh] w-full"
245
+ />
246
+ </div>
247
+ <button class="btn defaults primary self-end" @click="getCropResult">
248
+ {{ $t("fws_crop_av_cta") }}
249
+ </button>
250
+ </div>
251
+ </DefaultModal>
252
+
253
+ <!-- Profile Information -->
254
+ <div class="grid md:grid-cols-2 gap-x-4 sm:gap-x-6 gap-y-3 sm:gap-y-4 mb-4 sm:mb-6">
255
+ <DefaultInput
256
+ id="genderFWS"
257
+ v-model="state.userData.Gender"
258
+ type="select"
259
+ :options="[
260
+ ['female', $t('fws_persona_phys_appearance_opt_female')],
261
+ ['male', $t('fws_persona_phys_appearance_opt_male')],
262
+ ['non-binary', $t('fws_persona_phys_appearance_opt_non_binary')],
263
+ ]"
264
+ :label="$t('fws_gender_label')"
265
+ :error-vuelidate="v$.userData.Gender.$errors"
266
+ />
267
+
268
+ <DefaultInput
269
+ id="birthdateFWS"
270
+ v-model="state.userData.Birthdate"
271
+ type="datepicker"
272
+ :disable-dates-under18="true"
273
+ :label="$t('fws_birthdate_label')"
274
+ :error-vuelidate="v$.userData.Birthdate.$errors"
275
+ />
276
+ </div>
277
+
278
+ <!-- Bio -->
279
+ <div class="mb-4 sm:mb-6">
280
+ <DefaultInput
281
+ id="bioFWS"
282
+ v-model="state.userData.Bio"
283
+ type="textarea"
284
+ :label="$t('fws_bio_label')"
285
+ :error-vuelidate="v$.userData.Bio.$errors"
286
+ :dp-options="{ counterMax: 1000 }"
213
287
  />
214
288
  </div>
215
- </DefaultModal>
216
- <DefaultInput
217
- id="genderFWS"
218
- v-model="state.userData.Gender"
219
- class="mb-4"
220
- type="select"
221
- :options="[
222
- ['female', $t('fws_persona_phys_appearance_opt_female')],
223
- ['male', $t('fws_persona_phys_appearance_opt_male')],
224
- ['non-binary', $t('fws_persona_phys_appearance_opt_non_binary')],
225
- ]"
226
- :label="$t('fws_gender_label')"
227
- :error-vuelidate="v$.userData.Gender.$errors"
228
- />
229
- <DefaultInput
230
- id="birthdateFWS"
231
- v-model="state.userData.Birthdate"
232
- class="mb-4"
233
- type="datepicker"
234
- :disable-dates-under18="true"
235
- :label="$t('fws_birthdate_label')"
236
- :error-vuelidate="v$.userData.Birthdate.$errors"
237
- />
238
- <DefaultInput
239
- id="bioFWS"
240
- v-model="state.userData.Bio"
241
- class="mb-4"
242
- type="textarea"
243
- :label="$t('fws_bio_label')"
244
- :error-vuelidate="v$.userData.Bio.$errors"
245
- />
246
- <template v-if="!hidePublic">
247
- <DefaultInput
248
- v-if="!hideGender"
249
- id="publicGenderFWS"
250
- v-model:checkbox-value="state.userData.PublicGender"
251
- type="toggle"
252
- :label="$t('fws_public_gender')"
253
- :error-vuelidate="v$.userData.PublicGender.$errors"
254
- />
255
- <DefaultInput
256
- id="publicBioFWS"
257
- v-model:checkbox-value="state.userData.PublicBio"
258
- type="toggle"
259
- :label="$t('fws_public_bio')"
260
- :error-vuelidate="v$.userData.PublicBio.$errors"
261
- />
262
- <DefaultInput
263
- v-if="!hideBirthdate"
264
- id="publicBirthdateFWS"
265
- v-model:checkbox-value="state.userData.PublicBirthdate"
266
- type="toggle"
267
- :label="$t('fws_public_birthdate')"
268
- :error-vuelidate="v$.userData.PublicBirthdate.$errors"
269
- />
270
- </template>
271
- <div class="flex">
272
- <button type="submit" class="btn defaults primary">
273
- {{ $t("fws_save_user_cta") }}
274
- </button>
289
+
290
+ <!-- Privacy Settings -->
291
+ <template v-if="!hidePublic">
292
+ <div class="pt-3 sm:pt-4 border-t border-fv-neutral-200 dark:border-fv-neutral-700 mb-3 sm:mb-4">
293
+ <h4 class="font-medium text-fv-neutral-800 dark:text-white mb-3">
294
+ {{ $t('fws_privacy_settings') }}
295
+ </h4>
296
+ <div class="grid gap-2">
297
+ <DefaultInput
298
+ v-if="!hideGender"
299
+ id="publicGenderFWS"
300
+ v-model:checkbox-value="state.userData.PublicGender"
301
+ type="toggle"
302
+ :label="$t('fws_public_gender')"
303
+ :error-vuelidate="v$.userData.PublicGender.$errors"
304
+ />
305
+ <DefaultInput
306
+ id="publicBioFWS"
307
+ v-model:checkbox-value="state.userData.PublicBio"
308
+ type="toggle"
309
+ :label="$t('fws_public_bio')"
310
+ :error-vuelidate="v$.userData.PublicBio.$errors"
311
+ />
312
+ <DefaultInput
313
+ v-if="!hideBirthdate"
314
+ id="publicBirthdateFWS"
315
+ v-model:checkbox-value="state.userData.PublicBirthdate"
316
+ type="toggle"
317
+ :label="$t('fws_public_birthdate')"
318
+ :error-vuelidate="v$.userData.PublicBirthdate.$errors"
319
+ />
320
+ </div>
321
+ </div>
322
+ </template>
323
+
324
+ <!-- Save Button -->
325
+ <div class="flex justify-end pt-2">
326
+ <button type="submit" class="btn defaults primary flex items-center gap-2">
327
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
328
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
329
+ </svg>
330
+ {{ $t("fws_save_user_cta") }}
331
+ </button>
332
+ </div>
275
333
  </div>
276
334
  </form>
277
335
  </template>
@@ -110,57 +110,68 @@ async function patchUser() {
110
110
  </script>
111
111
 
112
112
  <template>
113
- <form @submit.prevent="patchUser">
114
- <DefaultInput
115
- id="usernameFWS"
116
- v-model="state.userData.Username"
117
- class="mb-4"
118
- type="text"
119
- :label="$t('fws_username_label')"
120
- :help="$t('fws_username_help')"
121
- :error-vuelidate="v$.userData.Username.$errors"
122
- :disabled="userData?.UserProfile?.HasUsernameAndSlug ? true : false"
123
- />
124
- <DefaultInput
125
- id="birthdateFWS"
126
- v-model="state.userData.Birthdate"
127
- class="mb-4"
128
- :disable-dates-under18="true"
129
- type="datepicker"
130
- :label="$t('fws_birthdate_label')"
131
- :error-vuelidate="v$.userData.Birthdate.$errors"
132
- />
113
+ <form class="space-y-4" @submit.prevent="patchUser">
114
+ <div class="bg-white dark:bg-fv-neutral-900 p-4 sm:p-6 rounded-lg shadow-sm border border-fv-neutral-200 dark:border-fv-neutral-700">
115
+ <h3 class="text-lg font-semibold text-fv-neutral-900 dark:text-white mb-4 pb-2 border-b border-fv-neutral-200 dark:border-fv-neutral-700">
116
+ {{ $t('fws_user_profile_title') || $t('fws_profile_settings') }}
117
+ </h3>
133
118
 
134
- <DefaultInput
135
- id="genderFWS"
136
- v-model="state.userData.Gender"
137
- class="mb-4"
138
- type="select"
139
- :options="[
140
- ['female', $t('fws_persona_phys_appearance_opt_female')],
141
- ['male', $t('fws_persona_phys_appearance_opt_male')],
142
- ['non-binary', $t('fws_persona_phys_appearance_opt_non_binary')],
143
- ]"
144
- :label="$t('fws_gender_label')"
145
- :error-vuelidate="v$.userData.Gender.$errors"
146
- />
119
+ <div class="grid gap-4">
120
+ <DefaultInput
121
+ id="usernameFWS"
122
+ v-model="state.userData.Username"
123
+ type="text"
124
+ :label="$t('fws_username_label')"
125
+ :help="$t('fws_username_help')"
126
+ :error-vuelidate="v$.userData.Username.$errors"
127
+ :disabled="userData?.UserProfile?.HasUsernameAndSlug ? true : false"
128
+ />
147
129
 
148
- <DefaultInput
149
- id="acceptedTermsFWS"
150
- v-model:checkbox-value="state.userData.AcceptedTerms"
151
- type="toggle"
152
- :label="$t('fws_accepted_terms_label')"
153
- :help="$t('fws_accepted_terms_help')"
154
- :error-vuelidate="v$.userData.AcceptedTerms.$errors"
155
- />
156
- <p v-if="props.termsText" class="terms-box">
157
- {{ props.termsText }}
158
- </p>
130
+ <DefaultInput
131
+ id="birthdateFWS"
132
+ v-model="state.userData.Birthdate"
133
+ :disable-dates-under18="true"
134
+ type="datepicker"
135
+ :label="$t('fws_birthdate_label')"
136
+ :error-vuelidate="v$.userData.Birthdate.$errors"
137
+ />
159
138
 
160
- <div class="flex">
161
- <button type="submit" class="btn defaults primary">
162
- {{ $t("fws_save_user_cta") }}
163
- </button>
139
+ <DefaultInput
140
+ id="genderFWS"
141
+ v-model="state.userData.Gender"
142
+ type="select"
143
+ :options="[
144
+ ['female', $t('fws_persona_phys_appearance_opt_female')],
145
+ ['male', $t('fws_persona_phys_appearance_opt_male')],
146
+ ['non-binary', $t('fws_persona_phys_appearance_opt_non_binary')],
147
+ ]"
148
+ :label="$t('fws_gender_label')"
149
+ :error-vuelidate="v$.userData.Gender.$errors"
150
+ />
151
+
152
+ <div class="mt-2 pt-3 border-t border-fv-neutral-200 dark:border-fv-neutral-700">
153
+ <DefaultInput
154
+ id="acceptedTermsFWS"
155
+ v-model:checkbox-value="state.userData.AcceptedTerms"
156
+ type="toggle"
157
+ :label="$t('fws_accepted_terms_label')"
158
+ :help="$t('fws_accepted_terms_help')"
159
+ :error-vuelidate="v$.userData.AcceptedTerms.$errors"
160
+ />
161
+ <p v-if="props.termsText" class="mt-2 p-2 sm:p-3 text-sm bg-fv-neutral-50 dark:bg-fv-neutral-700 rounded border border-fv-neutral-200 dark:border-fv-neutral-600 text-fv-neutral-700 dark:text-fv-neutral-300">
162
+ {{ props.termsText }}
163
+ </p>
164
+ </div>
165
+ </div>
166
+
167
+ <div class="mt-6 flex justify-end">
168
+ <button type="submit" class="btn defaults primary flex items-center gap-2">
169
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
170
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
171
+ </svg>
172
+ {{ $t("fws_save_user_cta") }}
173
+ </button>
174
+ </div>
164
175
  </div>
165
176
  </form>
166
177
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.3.36",
3
+ "version": "2.3.38",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",