@fy-/fws-vue 2.3.50 → 2.3.52
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.
|
@@ -48,6 +48,9 @@ const state = reactive({
|
|
|
48
48
|
PublicBio: userData.value?.UserProfile?.PublicBio || false,
|
|
49
49
|
PublicBirthdate: userData.value?.UserProfile?.PublicBirthdate || false,
|
|
50
50
|
},
|
|
51
|
+
usernameUpdate: {
|
|
52
|
+
Username: userData.value?.UserProfile?.Username || '',
|
|
53
|
+
},
|
|
51
54
|
})
|
|
52
55
|
watchEffect(() => {
|
|
53
56
|
state.userData = {
|
|
@@ -78,6 +81,11 @@ const rules = {
|
|
|
78
81
|
PublicBio: {},
|
|
79
82
|
PublicBirthdate: {},
|
|
80
83
|
},
|
|
84
|
+
usernameUpdate: {
|
|
85
|
+
Username: {
|
|
86
|
+
required,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
81
89
|
}
|
|
82
90
|
const v$ = useVuelidate(rules, state)
|
|
83
91
|
|
|
@@ -111,6 +119,36 @@ const cropResult = reactive({
|
|
|
111
119
|
blobURL: '',
|
|
112
120
|
})
|
|
113
121
|
const uploader = ref(new Uploader())
|
|
122
|
+
const canUpdateUsername = computed(() => {
|
|
123
|
+
// If user and UserProfile and UserProfile.UsernameChangedAt > 30 days
|
|
124
|
+
if (userData.value?.UserProfile?.UsernameChangedAt?.unixms) {
|
|
125
|
+
const date = new Date(userData.value?.UserProfile?.UsernameChangedAt.unixms)
|
|
126
|
+
const now = new Date()
|
|
127
|
+
const diff = Math.abs(now.getTime() - date.getTime())
|
|
128
|
+
const diffDays = Math.ceil(diff / (1000 * 3600 * 24))
|
|
129
|
+
return diffDays > 30
|
|
130
|
+
}
|
|
131
|
+
return true
|
|
132
|
+
})
|
|
133
|
+
const lastUsernameError = ref('')
|
|
134
|
+
async function updateUsername() {
|
|
135
|
+
eventBus.emit('main-loading', true)
|
|
136
|
+
if (await v$.value.usernameUpdate.$validate()) {
|
|
137
|
+
const data = { ...state.usernameUpdate }
|
|
138
|
+
const response = await rest('User/_Username', 'PATCH', data).catch((err) => {
|
|
139
|
+
eventBus.emit('main-loading', false)
|
|
140
|
+
lastUsernameError.value = err.message
|
|
141
|
+
})
|
|
142
|
+
if (response && response.result === 'success') {
|
|
143
|
+
if (props.onCompleted) {
|
|
144
|
+
props.onCompleted(response)
|
|
145
|
+
}
|
|
146
|
+
eventBus.emit('user:refresh', true)
|
|
147
|
+
eventBus.emit('updateUsernameModal', false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
eventBus.emit('main-loading', false)
|
|
151
|
+
}
|
|
114
152
|
|
|
115
153
|
async function getCropResult() {
|
|
116
154
|
if (!cropper) return
|
|
@@ -161,6 +199,30 @@ function selectFile(e: Event) {
|
|
|
161
199
|
|
|
162
200
|
<template>
|
|
163
201
|
<form class="space-y-4" @submit.prevent="patchUser">
|
|
202
|
+
<DefaultModal id="updateUsername" :title="$t('fws_username_update_title')">
|
|
203
|
+
<div class="flex flex-col gap-4">
|
|
204
|
+
<DefaultInput
|
|
205
|
+
id="usernameFWS"
|
|
206
|
+
v-model="state.usernameUpdate.Username"
|
|
207
|
+
type="text"
|
|
208
|
+
:label="$t('fws_username_label')"
|
|
209
|
+
:help="$t('fws_username_help')"
|
|
210
|
+
:error-vuelidate="v$.usernameUpdate.Username.$errors"
|
|
211
|
+
:disabled="!canUpdateUsername"
|
|
212
|
+
/>
|
|
213
|
+
<div v-if="lastUsernameError" class="text-xs text-red-500">
|
|
214
|
+
{{ lastUsernameError }}
|
|
215
|
+
</div>
|
|
216
|
+
<div class="flex justify-end pt-2">
|
|
217
|
+
<button type="submit" class="btn defaults primary flex items-center gap-2" @click="updateUsername">
|
|
218
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
219
|
+
<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" />
|
|
220
|
+
</svg>
|
|
221
|
+
{{ $t("fws_save_user_cta") }}
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</DefaultModal>
|
|
164
226
|
<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
227
|
<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
228
|
{{ $t('fws_profile_heading') || $t('fws_your_profile') }}
|
|
@@ -221,6 +283,18 @@ function selectFile(e: Event) {
|
|
|
221
283
|
:error-vuelidate="v$.userData.Username.$errors"
|
|
222
284
|
:disabled="userData?.UserProfile?.HasUsernameAndSlug ? true : false"
|
|
223
285
|
/>
|
|
286
|
+
<div v-if="!canUpdateUsername" class="text-xs text-fv-neutral-500 dark:text-fv-neutral-400 mt-1">
|
|
287
|
+
{{ $t('fws_username_update_help') }}
|
|
288
|
+
</div>
|
|
289
|
+
<div v-else>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
class="btn small primary mt-1"
|
|
293
|
+
@click="$eventBus.emit('updateUsernameModal', true)"
|
|
294
|
+
>
|
|
295
|
+
{{ $t('fws_username_update_cta') }}
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
224
298
|
</div>
|
|
225
299
|
</div>
|
|
226
300
|
|
|
@@ -6,7 +6,7 @@ import { getURL, hasFW } from '@fy-/fws-js'
|
|
|
6
6
|
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/vue/24/solid'
|
|
7
7
|
import { useServerHead } from '@unhead/vue'
|
|
8
8
|
import { useDebounceFn } from '@vueuse/core'
|
|
9
|
-
import { computed, onMounted, shallowRef, watch } from 'vue'
|
|
9
|
+
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
|
10
10
|
import { useRoute } from 'vue-router'
|
|
11
11
|
import { useEventBus } from '../../composables/event-bus'
|
|
12
12
|
import { useServerRouter } from '../../stores/serverRouter'
|
|
@@ -32,6 +32,10 @@ const history = useServerRouter()
|
|
|
32
32
|
const isMounted = shallowRef(false)
|
|
33
33
|
const pageWatcher = shallowRef<WatchStopHandle>()
|
|
34
34
|
|
|
35
|
+
// Jump to page functionality
|
|
36
|
+
const jumpPageValue = ref('')
|
|
37
|
+
const showJumpInput = ref(false)
|
|
38
|
+
|
|
35
39
|
// Memoize the hash string to avoid repeated computation
|
|
36
40
|
const hashString = computed(() => props.hash !== '' ? `#${props.hash}` : undefined)
|
|
37
41
|
|
|
@@ -73,6 +77,45 @@ const prev = useDebounceFn(() => {
|
|
|
73
77
|
})
|
|
74
78
|
}, 300)
|
|
75
79
|
|
|
80
|
+
// Jump to page functionality
|
|
81
|
+
const jumpToPage = useDebounceFn(() => {
|
|
82
|
+
const targetPage = Number.parseInt(jumpPageValue.value)
|
|
83
|
+
|
|
84
|
+
if (Number.isNaN(targetPage) || !isNewPage(targetPage)) {
|
|
85
|
+
jumpPageValue.value = ''
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newQuery = { ...route.query }
|
|
90
|
+
newQuery.page = targetPage.toString()
|
|
91
|
+
|
|
92
|
+
history.push({
|
|
93
|
+
path: history.currentRoute.path,
|
|
94
|
+
query: newQuery,
|
|
95
|
+
hash: hashString.value,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
jumpPageValue.value = ''
|
|
99
|
+
showJumpInput.value = false
|
|
100
|
+
}, 300)
|
|
101
|
+
|
|
102
|
+
function toggleJumpInput() {
|
|
103
|
+
showJumpInput.value = !showJumpInput.value
|
|
104
|
+
if (!showJumpInput.value) {
|
|
105
|
+
jumpPageValue.value = ''
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleJumpInputKeydown(event: KeyboardEvent) {
|
|
110
|
+
if (event.key === 'Enter') {
|
|
111
|
+
jumpToPage()
|
|
112
|
+
}
|
|
113
|
+
else if (event.key === 'Escape') {
|
|
114
|
+
showJumpInput.value = false
|
|
115
|
+
jumpPageValue.value = ''
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
// Extract route generation to a reusable function to reduce duplicated code
|
|
77
120
|
function page(page: number): RouteLocationRaw {
|
|
78
121
|
if (!isNewPage(page)) {
|
|
@@ -250,12 +293,26 @@ onMounted(() => {
|
|
|
250
293
|
{{ items.page_no + 1 }}
|
|
251
294
|
</router-link>
|
|
252
295
|
</li>
|
|
253
|
-
<li v-if="items.page_no + 1 < items.page_max - 1"
|
|
296
|
+
<li v-if="items.page_no + 1 < items.page_max - 1">
|
|
254
297
|
<div
|
|
255
|
-
|
|
298
|
+
v-if="!showJumpInput"
|
|
299
|
+
class="flex items-center justify-center w-5 md:w-6 h-7 md:h-8 text-fv-neutral-500 hover:text-primary-600 dark:text-fv-neutral-400 dark:hover:text-white cursor-pointer transition-all"
|
|
300
|
+
title="Jump to page"
|
|
301
|
+
@click="toggleJumpInput"
|
|
256
302
|
>
|
|
257
303
|
<span class="text-[10px] md:text-xs font-medium">•••</span>
|
|
258
304
|
</div>
|
|
305
|
+
<input
|
|
306
|
+
v-else
|
|
307
|
+
v-model="jumpPageValue"
|
|
308
|
+
type="number"
|
|
309
|
+
:min="1"
|
|
310
|
+
:max="items.page_max"
|
|
311
|
+
class="w-8 md:w-10 h-7 md:h-8 text-xs text-center text-fv-neutral-700 bg-white/80 border border-fv-neutral-300 rounded-md shadow-sm focus:ring-2 focus:ring-primary-400/40 focus:border-primary-400 focus:outline-none dark:bg-fv-neutral-800 dark:text-fv-neutral-300 dark:border-fv-neutral-600 dark:focus:ring-primary-500/40 dark:focus:border-primary-500 transition-all"
|
|
312
|
+
:placeholder="items.page_max.toString().length > 2 ? '···' : '••'"
|
|
313
|
+
@keydown="handleJumpInputKeydown"
|
|
314
|
+
@blur="showJumpInput = false; jumpPageValue = ''"
|
|
315
|
+
>
|
|
259
316
|
</li>
|
|
260
317
|
<li v-if="items.page_no + 1 < items.page_max">
|
|
261
318
|
<router-link
|