Improve the name-editor

This commit is contained in:
2025-04-19 03:49:27 -04:00
parent 98c598f3c6
commit 300ee124d8
4 changed files with 68 additions and 12 deletions

View File

@@ -1,4 +1,6 @@
VITE_API_URL=https://localhost:5001/ VITE_API_URL=https://localhost:5001/
VITE_APP_BASE_URL=http://localhost:5173
VITE_APP_API_URL=http://localhost:5173/api
VITE_STRIPE_API_KEY=pk_test_51OoveVDrRyqXtNdB2st1NgA8WQA9rhgGaf3q7bCpAOoQyyRS30HMCzGeHba7meVGCSPfb1BVWmOTmFOcr9MkKf5H00bLu5MqsS VITE_STRIPE_API_KEY=pk_test_51OoveVDrRyqXtNdB2st1NgA8WQA9rhgGaf3q7bCpAOoQyyRS30HMCzGeHba7meVGCSPfb1BVWmOTmFOcr9MkKf5H00bLu5MqsS
VITE_GOOGLE_CLIENT_ID=213344094492-9dbaet2gaschju3hj1sgv1umk0qpd833.apps.googleusercontent.com VITE_GOOGLE_CLIENT_ID=213344094492-9dbaet2gaschju3hj1sgv1umk0qpd833.apps.googleusercontent.com
VITE_FACEBOOK_APP_ID VITE_FACEBOOK_APP_ID

View File

@@ -1,4 +1,6 @@
VITE_API_URL=https://hutopy-backend-api.azurewebsites.net VITE_API_URL=https://hutopy-backend-api.azurewebsites.net
VITE_APP_BASE_URL=https://hutopy.ca
VITE_APP_API_URL=https://hutopy.ca/api
VITE_STRIPE_API_KEY=51OoveVDrRyqXtNdBAxIo183PujtqFyU0xUMK9YNtIijcHeDlcLN6pqkZWHbgaBA0FHrwLMSoy3yVLN33NX8ExOxL00MSZwgJN7 VITE_STRIPE_API_KEY=51OoveVDrRyqXtNdBAxIo183PujtqFyU0xUMK9YNtIijcHeDlcLN6pqkZWHbgaBA0FHrwLMSoy3yVLN33NX8ExOxL00MSZwgJN7
VITE_GOOGLE_CLIENT_ID=213344094492-7c83lqoh7mnjgadpeqo2lcs1krhbsnnd.apps.googleusercontent.com VITE_GOOGLE_CLIENT_ID=213344094492-7c83lqoh7mnjgadpeqo2lcs1krhbsnnd.apps.googleusercontent.com
AZURE_SUBSCRIPTION_ID=46feb20f-3ae1-495a-830b-a31f7b76483d AZURE_SUBSCRIPTION_ID=46feb20f-3ae1-495a-830b-a31f7b76483d

7
frontend/src/config.js Normal file
View File

@@ -0,0 +1,7 @@
// Environment-specific configuration
const config = {
baseUrl: import.meta.env.VITE_APP_BASE_URL || 'https://hutopy.ca',
apiUrl: import.meta.env.VITE_APP_API_URL || 'https://hutopy.ca/api',
};
export default config;

View File

@@ -2,7 +2,8 @@
import {ref, onMounted, onUnmounted, computed} from "vue"; import {ref, onMounted, onUnmounted, computed} from "vue";
import {v7} from "uuid"; import {v7} from "uuid";
import {useClient} from "@/plugins/api.js"; import {useClient} from "@/plugins/api.js";
import { useI18n } from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import config from '@/config';
const props = defineProps({ const props = defineProps({
name: { name: {
@@ -23,10 +24,11 @@ const emits = defineEmits([
]); ]);
const name = ref(props.name); const name = ref(props.name);
const { t } = useI18n(); const {t} = useI18n();
const isOperationPending = ref(false); const isOperationPending = ref(false);
const reservationState = ref(null); const reservationState = ref(null);
const validationError = ref('');
// Use the reservationId from props if provided, otherwise generate a new one // Use the reservationId from props if provided, otherwise generate a new one
const reservationId = ref(props.creatorNameReservationId || v7()); const reservationId = ref(props.creatorNameReservationId || v7());
@@ -36,6 +38,27 @@ const isCurrentSlug = computed(() => {
return props.originalSlug && name.value === props.originalSlug; 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 // Ensure we emit the reservationId on mount if we generated a new one
onMounted(() => { onMounted(() => {
if (!props.creatorNameReservationId) { if (!props.creatorNameReservationId) {
@@ -68,6 +91,12 @@ const handleInput = () => {
return; // Skip if we've already processed this exact name 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 the name is the same as the original slug, set reservation state to "reserved"
if (props.originalSlug && currentName === props.originalSlug) { if (props.originalSlug && currentName === props.originalSlug) {
reservationState.value = "reserved"; reservationState.value = "reserved";
@@ -100,9 +129,9 @@ const checkNameAvailability = async (nameToCheck) => {
currentController = controller; currentController = controller;
await client.post( await client.post(
`/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`, `/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`,
{ reservationId: reservationId.value }, {reservationId: reservationId.value},
{ signal: controller.signal } {signal: controller.signal}
); );
// Only process the response if this is still the current request // Only process the response if this is still the current request
@@ -137,9 +166,13 @@ onUnmounted(() => {
variant="outlined" variant="outlined"
:label="t('creator.name.label')" :label="t('creator.name.label')"
v-model="name" v-model="name"
outlined
@input="handleInput" @input="handleInput"
:error-messages="validationError"
> >
<template #prepend-inner>
<span class="text-gray-400 font-sans">{{ baseUrl }}</span>
</template>
<template #append-inner> <template #append-inner>
<v-progress-circular <v-progress-circular
v-if="reservationState === 'loading'" v-if="reservationState === 'loading'"
@@ -164,21 +197,33 @@ onUnmounted(() => {
"en": { "en": {
"creator": { "creator": {
"name": { "name": {
"label": "Your creator handle" "label": "Your creator handle",
"errors": {
"required": "Creator handle is required",
"invalid": "Only letters, numbers, and hyphens are allowed"
}
} }
} }
}, },
"fr": { "fr": {
"creator": { "creator": {
"name": { "name": {
"label": "Votre identifiant de créateur" "label": "Votre identifiant de créateur",
"errors": {
"required": "L'identifiant est requis",
"invalid": "Seules les lettres, chiffres et tirets sont autorisés"
}
} }
} }
}, },
"es": { "es": {
"creator": { "creator": {
"name": { "name": {
"label": "Tu identificador de creador" "label": "Tu identificador de creador",
"errors": {
"required": "El identificador es obligatorio",
"invalid": "Solo se permiten letras, números y guiones"
}
} }
} }
} }