221 lines
5.7 KiB
Vue
221 lines
5.7 KiB
Vue
<script setup>
|
|
import { ref, onMounted, onUnmounted, computed } from "vue";
|
|
import { v7 } from "uuid";
|
|
import { useClient } from "@/plugins/api.js";
|
|
import { useI18n } from 'vue-i18n';
|
|
import config from '@/config';
|
|
import { mdiCheckCircle, mdiCloseCircle } from '@mdi/js';
|
|
|
|
const props = defineProps({
|
|
name: {
|
|
required: true
|
|
},
|
|
creatorNameReservationId: {
|
|
required: true
|
|
},
|
|
originalSlug: {
|
|
type: String,
|
|
default: null
|
|
}
|
|
});
|
|
|
|
const emits = defineEmits([
|
|
'update:name',
|
|
'update:creatorNameReservationId'
|
|
]);
|
|
|
|
const name = ref(props.name);
|
|
const { t } = useI18n();
|
|
|
|
const isOperationPending = ref(false);
|
|
const reservationState = ref(null);
|
|
const validationError = ref('');
|
|
|
|
// Use the reservationId from props if provided, otherwise generate a new one
|
|
const reservationId = ref(props.creatorNameReservationId || v7());
|
|
|
|
// Check if the current name is the same as the original slug
|
|
const isCurrentSlug = computed(() => {
|
|
return props.originalSlug && name.value === props.originalSlug;
|
|
});
|
|
|
|
// Base URL for display
|
|
const baseUrl = computed(() => `${config.baseUrl}/@`);
|
|
|
|
// Validation function for the slug
|
|
const validateSlug = (slug) => {
|
|
if (!slug) {
|
|
validationError.value = t('creator.name.errors.required');
|
|
return false;
|
|
}
|
|
|
|
// Only allow letters, numbers, and hyphens
|
|
const validSlugRegex = /^[a-zA-Z0-9-]+$/;
|
|
if (!validSlugRegex.test(slug)) {
|
|
validationError.value = t('creator.name.errors.invalid');
|
|
return false;
|
|
}
|
|
|
|
validationError.value = '';
|
|
return true;
|
|
};
|
|
|
|
// Ensure we emit the reservationId on mount if we generated a new one
|
|
onMounted(() => {
|
|
if (!props.creatorNameReservationId) {
|
|
emits('update:creatorNameReservationId', reservationId.value);
|
|
}
|
|
|
|
// If the name is the same as the original slug, set the reservation state to "reserved"
|
|
if (isCurrentSlug.value) {
|
|
reservationState.value = "reserved";
|
|
}
|
|
});
|
|
|
|
// Request handling
|
|
let currentController = null;
|
|
let timeout = null;
|
|
let lastProcessedName = '';
|
|
|
|
const cancelCurrentRequest = () => {
|
|
if (currentController) {
|
|
currentController.abort();
|
|
currentController = null;
|
|
}
|
|
};
|
|
|
|
const handleInput = () => {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => {
|
|
const currentName = name.value;
|
|
if (currentName === lastProcessedName) {
|
|
return; // Skip if we've already processed this exact name
|
|
}
|
|
|
|
// Validate the slug
|
|
if (!validateSlug(currentName)) {
|
|
reservationState.value = "unavailable";
|
|
return;
|
|
}
|
|
|
|
// If the name is the same as the original slug, set reservation state to "reserved"
|
|
if (props.originalSlug && currentName === props.originalSlug) {
|
|
reservationState.value = "reserved";
|
|
lastProcessedName = currentName;
|
|
emits('update:name', currentName);
|
|
return;
|
|
}
|
|
|
|
checkNameAvailability(currentName);
|
|
}, 200);
|
|
};
|
|
|
|
const client = useClient();
|
|
const checkNameAvailability = async (nameToCheck) => {
|
|
if (!nameToCheck || nameToCheck.trim() === "") {
|
|
reservationState.value = null;
|
|
lastProcessedName = nameToCheck;
|
|
return;
|
|
}
|
|
|
|
// Cancel any ongoing request
|
|
cancelCurrentRequest();
|
|
|
|
try {
|
|
isOperationPending.value = true;
|
|
reservationState.value = "loading";
|
|
|
|
// Create a new request with cancellation token
|
|
const controller = new AbortController();
|
|
currentController = controller;
|
|
|
|
await client.post(
|
|
`/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`,
|
|
{ reservationId: reservationId.value },
|
|
{ signal: controller.signal }
|
|
);
|
|
|
|
// Only process the response if this is still the current request
|
|
if (currentController === controller) {
|
|
reservationState.value = "reserved";
|
|
lastProcessedName = nameToCheck;
|
|
emits('update:name', nameToCheck);
|
|
}
|
|
} catch (error) {
|
|
// Only process the error if this is still the current request and it's not an abort error
|
|
if (currentController && error.name !== 'AbortError') {
|
|
reservationState.value = "unavailable";
|
|
lastProcessedName = nameToCheck;
|
|
}
|
|
} finally {
|
|
if (currentController) {
|
|
isOperationPending.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Cleanup on component unmount
|
|
onUnmounted(() => {
|
|
cancelCurrentRequest();
|
|
clearTimeout(timeout);
|
|
});
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<v-text-field variant="outlined" :label="t('creator.name.label')" v-model="name" @input="handleInput"
|
|
:error-messages="validationError">
|
|
<template #prepend-inner>
|
|
<span class="text-nowrap font-sans text-gray-400">{{ baseUrl }}</span>
|
|
</template>
|
|
|
|
<template #append-inner>
|
|
<v-progress-circular v-if="reservationState === 'loading'" indeterminate size="24" width="3"
|
|
color="grey"></v-progress-circular>
|
|
|
|
<v-icon v-else-if="reservationState === 'reserved'" color="green" :icon="mdiCheckCircle" />
|
|
<v-icon v-else-if="reservationState === 'unavailable'" color="red" :icon="mdiCloseCircle" />
|
|
</template>
|
|
</v-text-field>
|
|
</template>
|
|
|
|
<style scoped></style>
|
|
|
|
<i18n>
|
|
{
|
|
"en": {
|
|
"creator": {
|
|
"name": {
|
|
"label": "Your creator handle",
|
|
"errors": {
|
|
"required": "Creator handle is required",
|
|
"invalid": "Only letters, numbers, and hyphens are allowed"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"fr": {
|
|
"creator": {
|
|
"name": {
|
|
"label": "Votre identifiant de créateur",
|
|
"errors": {
|
|
"required": "L'identifiant est requis",
|
|
"invalid": "Seules les lettres, chiffres et tirets sont autorisés"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"es": {
|
|
"creator": {
|
|
"name": {
|
|
"label": "Tu identificador de creador",
|
|
"errors": {
|
|
"required": "El identificador es obligatorio",
|
|
"invalid": "Solo se permiten letras, números y guiones"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</i18n>
|