diff --git a/frontend/.env.development b/frontend/.env.development
index eea1831..bb4d8a1 100644
--- a/frontend/.env.development
+++ b/frontend/.env.development
@@ -1,4 +1,6 @@
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_GOOGLE_CLIENT_ID=213344094492-9dbaet2gaschju3hj1sgv1umk0qpd833.apps.googleusercontent.com
VITE_FACEBOOK_APP_ID
\ No newline at end of file
diff --git a/frontend/.env.production b/frontend/.env.production
index 3eabb32..888b727 100644
--- a/frontend/.env.production
+++ b/frontend/.env.production
@@ -1,4 +1,6 @@
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_GOOGLE_CLIENT_ID=213344094492-7c83lqoh7mnjgadpeqo2lcs1krhbsnnd.apps.googleusercontent.com
AZURE_SUBSCRIPTION_ID=46feb20f-3ae1-495a-830b-a31f7b76483d
diff --git a/frontend/src/config.js b/frontend/src/config.js
new file mode 100644
index 0000000..934032b
--- /dev/null
+++ b/frontend/src/config.js
@@ -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;
\ No newline at end of file
diff --git a/frontend/src/views/creators/NameEditor.vue b/frontend/src/views/creators/NameEditor.vue
index 74d2d25..95bb84b 100644
--- a/frontend/src/views/creators/NameEditor.vue
+++ b/frontend/src/views/creators/NameEditor.vue
@@ -2,7 +2,8 @@
import {ref, onMounted, onUnmounted, computed} from "vue";
import {v7} from "uuid";
import {useClient} from "@/plugins/api.js";
-import { useI18n } from 'vue-i18n';
+import {useI18n} from 'vue-i18n';
+import config from '@/config';
const props = defineProps({
name: {
@@ -23,10 +24,11 @@ const emits = defineEmits([
]);
const name = ref(props.name);
-const { t } = useI18n();
+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());
@@ -36,12 +38,33 @@ 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 reservation state to "reserved"
if (isCurrentSlug.value) {
reservationState.value = "reserved";
@@ -67,7 +90,13 @@ const handleInput = () => {
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";
@@ -75,7 +104,7 @@ const handleInput = () => {
emits('update:name', currentName);
return;
}
-
+
checkNameAvailability(currentName);
}, 200);
};
@@ -100,9 +129,9 @@ const checkNameAvailability = async (nameToCheck) => {
currentController = controller;
await client.post(
- `/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`,
- { reservationId: reservationId.value },
- { signal: controller.signal }
+ `/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`,
+ {reservationId: reservationId.value},
+ {signal: controller.signal}
);
// Only process the response if this is still the current request
@@ -137,9 +166,13 @@ onUnmounted(() => {
variant="outlined"
:label="t('creator.name.label')"
v-model="name"
- outlined
@input="handleInput"
+ :error-messages="validationError"
>
+
+ {{ baseUrl }}
+
+
{
"en": {
"creator": {
"name": {
- "label": "Your creator handle"
+ "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"
+ "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"
+ "label": "Tu identificador de creador",
+ "errors": {
+ "required": "El identificador es obligatorio",
+ "invalid": "Solo se permiten letras, números y guiones"
+ }
}
}
}