feat(i18n): deprecate Spanish language support in the frontend

This commit is contained in:
2025-08-26 15:58:06 -04:00
parent d5ca6b9972
commit 262f21d157
41 changed files with 4148 additions and 4212 deletions

View File

@@ -948,39 +948,6 @@
"phoneNumber": "Numéro de téléphone",
"title": "Titre",
"removeStripe": "Retirer Stripe"
},
"es": {
"personalInfo": "Información Personal",
"fullName": "Nombre Completo",
"alias": "Alias",
"email": "Correo Electrónico",
"changePassword": "Actualizar contraseña",
"creatorInfo": "Información del Creador",
"dangerZone": "Zona de Peligro",
"dangerZoneWarning": "Las acciones a continuación pueden tener impactos significativos en tu página de creador. Por favor, procede con precaución.",
"deleteWarning": "¿Estás seguro de que quieres eliminar tu página de creador? Esta acción no se puede deshacer.",
"restoreWarning": "¿Estás seguro de que quieres restaurar tu página de creador? Esto hará que tu página sea visible nuevamente.",
"deleteCreatorPage": "Eliminar Página de Creador",
"restoreCreatorPage": "Restaurar Página de Creador",
"stripeAccountId": "ID de Cuenta Stripe",
"socialNetworks": "Redes Sociales",
"handle": "Identificador del creador",
"qrCode": "Código QR",
"qrCodeDescription": "¡Imprime este código QR para compartir tu Hutopy con el mundo! Perfecto para tarjetas de presentación, redes sociales y materiales promocionales.",
"downloadQRCode": "Descargar Código QR",
"payment-information": "Información de Pago",
"stripeStatus": "Estado de Stripe",
"configured": "Configurado",
"notConfigured": "No Configurado",
"needsMoreInfo": "Requiere Más Información",
"pendingVerification": "Verificación Pendiente",
"continueStripeSetup": "Continuar Configuración de Stripe",
"reviewStripe": "Revisar Stripe",
"notSet": "No Establecido",
"configureStripe": "Connectar Stripe",
"phoneNumber": "Número de teléfono",
"title": "Título",
"removeStripe": "Eliminar Stripe"
}
}
</i18n>

View File

@@ -1,66 +1,58 @@
<template>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card dialog">
<div class="card-content">
<v-text-field
v-model="alias"
:label="t('label')"
variant="outlined"
></v-text-field>
</div>
<div class="card-title">
{{ t('title') }}
<div class="card-actions">
<button
class="secondary"
@click="requestClose"
>
{{ t('cancel') }}
</button>
<button
class="primary"
@click="requestSave"
>
{{ t('save') }}
</button>
</div>
</div>
<div class="card-content">
<v-text-field
variant="outlined"
v-model="alias"
:label="t('label')"
></v-text-field>
</div>
<div class="card-actions">
<button class="secondary"
@click="requestClose">
{{ t('cancel') }}
</button>
<button class="primary"
@click="requestSave">
{{ t('save') }}
</button>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps(['alias'])
const emit = defineEmits(['close', 'save'])
const { t } = useI18n();
const props = defineProps(['alias']);
const emit = defineEmits(['close', 'save']);
const alias = ref(props.alias)
const alias = ref(props.alias);
const requestClose = () => emit('close')
const requestSave = () => emit('save', alias.value)
const requestClose = () => emit('close');
const requestSave = () => emit('save', alias.value);
</script>
<i18n>
{
"en": {
"title": "Alias",
"label": "Your alias"
},
"fr": {
"title": "Alias",
"label": "Votre alias"
},
"es": {
"title": "Alias",
"label": "Tu alias"
}
"en": {
"title": "Alias",
"label": "Your alias"
},
"fr": {
"title": "Alias",
"label": "Votre alias"
}
}
</i18n>

View File

@@ -1,162 +1,177 @@
<template>
<div class="card dialog">
<div class="card dialog">
<div class="card-title">
{{ t('changePassword') }}
</div>
<div class="card-title">
{{ t('changePassword') }}
<div class="card-content">
<p class="description mb-4">{{ t('passwordDescription') }}</p>
<v-text-field
v-model="newPassword"
:hint="t('passwordRequirements')"
:label="t('newPassword')"
:type="showNewPassword ? 'text' : 'password'"
required
variant="outlined"
>
<template v-slot:append-inner>
<v-icon
:icon="showNewPassword ? mdiEyeOff : mdiEye"
class="visibility-toggle"
size="small"
@click="showNewPassword = !showNewPassword"
/>
</template>
</v-text-field>
<v-text-field
v-model="confirmPassword"
:label="t('confirmPassword')"
:type="showConfirmPassword ? 'text' : 'password'"
required
variant="outlined"
>
<template v-slot:append-inner>
<v-icon
:icon="showNewPassword ? mdiEyeOff : mdiEye"
class="visibility-toggle"
size="small"
@click="showConfirmPassword = !showConfirmPassword"
/>
</template>
</v-text-field>
<div
v-if="errorMessage"
class="error-message mb-4"
>
{{ errorMessage }}
</div>
<div class="card-actions">
<button
class="secondary"
@click="$emit('closeRequested')"
>
{{ t('cancel') }}
</button>
<button
:disabled="isLoading"
class="primary"
@click="handleChangePassword"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
<div class="card-content">
<p class="description mb-4">{{ t('passwordDescription') }}</p>
<v-text-field v-model="newPassword" :label="t('newPassword')" :type="showNewPassword ? 'text' : 'password'"
variant="outlined" required :hint="t('passwordRequirements')">
<template v-slot:append-inner>
<v-icon @click="showNewPassword = !showNewPassword" class="visibility-toggle" size="small"
:icon="showNewPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
<v-text-field v-model="confirmPassword" :label="t('confirmPassword')"
:type="showConfirmPassword ? 'text' : 'password'" variant="outlined" required>
<template v-slot:append-inner>
<v-icon @click="showConfirmPassword = !showConfirmPassword" class="visibility-toggle" size="small"
:icon="showNewPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
<div v-if="errorMessage" class="error-message mb-4">
{{ errorMessage }}
</div>
<div class="card-actions">
<button class="secondary" @click="$emit('closeRequested')">
{{ t('cancel') }}
</button>
<button class="primary" @click="handleChangePassword" :disabled="isLoading">
{{ t('save') }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useAuthStore } from '@/stores/authStore.js';
import { useI18n } from 'vue-i18n';
import { mdiEye, mdiEyeOff } from '@mdi/js';
import { ref } from 'vue';
import { useAuthStore } from '@/stores/authStore.js';
import { useI18n } from 'vue-i18n';
import { mdiEye, mdiEyeOff } from '@mdi/js';
const { t } = useI18n();
const authStore = useAuthStore();
const emit = defineEmits(['closeRequested']);
const { t } = useI18n();
const authStore = useAuthStore();
const emit = defineEmits(['closeRequested']);
const newPassword = ref('');
const confirmPassword = ref('');
const isLoading = ref(false);
const errorMessage = ref('');
const showNewPassword = ref(false);
const showConfirmPassword = ref(false);
const newPassword = ref('');
const confirmPassword = ref('');
const isLoading = ref(false);
const errorMessage = ref('');
const showNewPassword = ref(false);
const showConfirmPassword = ref(false);
async function handleChangePassword() {
// Clear previous error
errorMessage.value = '';
async function handleChangePassword() {
// Clear previous error
errorMessage.value = '';
// Validate passwords match
if (newPassword.value !== confirmPassword.value) {
errorMessage.value = t('passwordsDoNotMatch');
return;
}
// Validate passwords match
if (newPassword.value !== confirmPassword.value) {
errorMessage.value = t('passwordsDoNotMatch');
return;
}
// Validate password length
if (newPassword.value.length < 8) {
errorMessage.value = t('passwordTooShort');
return;
}
// Validate password length
if (newPassword.value.length < 8) {
errorMessage.value = t('passwordTooShort');
return;
}
isLoading.value = true;
isLoading.value = true;
try {
// Pass empty string for current password since we're already authenticated
// This will use the set-password endpoint for OAuth users
await authStore.changePassword(newPassword.value);
try {
// Pass empty string for current password since we're already authenticated
// This will use the set-password endpoint for OAuth users
await authStore.changePassword(newPassword.value);
// Success - close dialog
emit('closeRequested');
// Success - close dialog
emit('closeRequested');
// You could also emit a success event if needed
// emit('success');
} catch (error) {
console.error('Failed to change password:', error);
// Use error message from response if available, or the error message itself, or fallback
errorMessage.value = error.response?.data || error.message || t('passwordUpdateFailed');
} finally {
isLoading.value = false;
}
}
// You could also emit a success event if needed
// emit('success');
} catch (error) {
console.error('Failed to change password:', error);
// Use error message from response if available, or the error message itself, or fallback
errorMessage.value = error.response?.data || error.message || t('passwordUpdateFailed');
} finally {
isLoading.value = false;
}
}
</script>
<style scoped>
.dialog {
@apply max-w-md mx-auto;
}
.dialog {
@apply max-w-md mx-auto;
}
.error-message {
@apply text-red-500 text-sm mt-2;
}
.error-message {
@apply text-red-500 text-sm mt-2;
}
.visibility-toggle {
@apply cursor-pointer;
@apply transition-opacity duration-300;
@apply opacity-60 hover:opacity-100;
@apply absolute right-2 top-1/2 transform -translate-y-1/2;
@apply z-10;
}
.visibility-toggle {
@apply cursor-pointer;
@apply transition-opacity duration-300;
@apply opacity-60 hover:opacity-100;
@apply absolute right-2 top-1/2 transform -translate-y-1/2;
@apply z-10;
}
/* Override Vuetify's default padding to accommodate our icon */
:deep(.v-field__append-inner) {
padding-inline-start: 0;
}
/* Override Vuetify's default padding to accommodate our icon */
:deep(.v-field__append-inner) {
padding-inline-start: 0;
}
</style>
<i18n>
{
"en": {
"changePassword": "Update Password",
"newPassword": "New Password",
"confirmPassword": "Confirm New Password",
"passwordRequirements": "Password must be at least 8 characters",
"passwordDescription": "Updating your password allows you to log in directly with your email and password.",
"save": "Save",
"cancel": "Cancel",
"passwordsDoNotMatch": "New passwords do not match",
"passwordTooShort": "Password must be at least 8 characters long",
"passwordUpdateFailed": "Failed to update password. Please try again."
},
"fr": {
"changePassword": "Modifier le mot de passe",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le nouveau mot de passe",
"passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères",
"passwordDescription": "La modification de votre mot de passe vous permet de vous connecter directement avec votre email et mot de passe.",
"save": "Enregistrer",
"cancel": "Annuler",
"passwordsDoNotMatch": "Les nouveaux mots de passe ne correspondent pas",
"passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères",
"passwordUpdateFailed": "Échec de la mise à jour du mot de passe. Veuillez réessayer."
},
"es": {
"changePassword": "Actualizar contraseña",
"newPassword": "Nueva contraseña",
"confirmPassword": "Confirmar nueva contraseña",
"passwordRequirements": "La contraseña debe tener al menos 8 caracteres",
"passwordDescription": "La actualización de su contraseña le permite iniciar sesión directamente con su correo electrónico y contraseña.",
"save": "Guardar",
"cancel": "Cancelar",
"passwordsDoNotMatch": "Las nuevas contraseñas no coinciden",
"passwordTooShort": "La contraseña debe tener al menos 8 caracteres",
"passwordUpdateFailed": "Error al actualizar la contraseña. Por favor, inténtelo de nuevo."
}
"en": {
"changePassword": "Update Password",
"newPassword": "New Password",
"confirmPassword": "Confirm New Password",
"passwordRequirements": "Password must be at least 8 characters",
"passwordDescription": "Updating your password allows you to log in directly with your email and password.",
"save": "Save",
"cancel": "Cancel",
"passwordsDoNotMatch": "New passwords do not match",
"passwordTooShort": "Password must be at least 8 characters long",
"passwordUpdateFailed": "Failed to update password. Please try again."
},
"fr": {
"changePassword": "Modifier le mot de passe",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le nouveau mot de passe",
"passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères",
"passwordDescription": "La modification de votre mot de passe vous permet de vous connecter directement avec votre email et mot de passe.",
"save": "Enregistrer",
"cancel": "Annuler",
"passwordsDoNotMatch": "Les nouveaux mots de passe ne correspondent pas",
"passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères",
"passwordUpdateFailed": "Échec de la mise à jour du mot de passe. Veuillez réessayer."
}
}
</i18n>

View File

@@ -1,82 +1,78 @@
<template>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card-content">
<v-text-field
v-model="email"
:label="t('label')"
variant="outlined"
></v-text-field>
</div>
<div class="card-content">
<v-text-field
v-model="email"
:label="t('label')"
variant="outlined"
></v-text-field>
</div>
<div class="card-actions">
<button class="secondary"
@click="cancel">
{{ t('cancel') }}
</button>
<button class="primary"
@click="save">
{{ t('save') }}
</button>
<div class="card-actions">
<button
class="secondary"
@click="cancel"
>
{{ t('cancel') }}
</button>
<button
class="primary"
@click="save"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue';
import {useClient} from "@/plugins/api.js";
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useClient } from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
email: {
required: true,
type: String
}
});
const { t } = useI18n();
const props = defineProps({
email: {
required: true,
type: String,
},
});
const emits = defineEmits(['closeRequested']);
const emits = defineEmits(['closeRequested']);
const email = ref(props.email);
const email = ref(props.email);
const client = useClient();
const save = async () => {
try {
await client.post(
`/api/users/email`,
{
email: email.value
});
const client = useClient();
const save = async () => {
try {
await client.post(`/api/users/email`, {
email: email.value,
});
emits('closeRequested');
} catch (error) {
console.error(error);
}
};
emits('closeRequested');
} catch (error) {
console.error(error);
}
};
const cancel = () => {
emits('closeRequested');
};
const cancel = () => {
emits('closeRequested');
};
</script>
<i18n>
{
"en": {
"title": "Change your Email",
"label": "Your email"
},
"fr": {
"title": "Changez votre Courriel",
"label": "Votre email"
},
"es": {
"title": "Cambia tu correo electrónico",
"label": "Tu correo electrónico"
}
"en": {
"title": "Change your Email",
"label": "Your email"
},
"fr": {
"title": "Changez votre Courriel",
"label": "Votre email"
}
}
</i18n>

View File

@@ -1,70 +1,69 @@
<script setup>
import {ref} from 'vue';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps(['firstname', 'lastname'])
const emit = defineEmits(['close', 'save'])
const { t } = useI18n();
const props = defineProps(['firstname', 'lastname']);
const emit = defineEmits(['close', 'save']);
const firstname = ref(props.firstname)
const lastname = ref(props.lastname)
const firstname = ref(props.firstname);
const lastname = ref(props.lastname);
const requestClose = () => emit('close')
const requestSave = () => emit('save', firstname.value, lastname.value)
const requestClose = () => emit('close');
const requestSave = () => emit('save', firstname.value, lastname.value);
</script>
<template>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card-content">
<v-text-field
variant="outlined"
v-model="firstname"
:label="t('firstname')"
></v-text-field>
</div>
<div class="card-content">
<v-text-field
v-model="firstname"
:label="t('firstname')"
variant="outlined"
></v-text-field>
</div>
<div class="card-content">
<v-text-field
variant="outlined"
v-model="lastname"
:label="t('lastname')"
></v-text-field>
</div>
<div class="card-content">
<v-text-field
v-model="lastname"
:label="t('lastname')"
variant="outlined"
></v-text-field>
</div>
<div class="card-actions">
<button class="secondary"
@click="requestClose">
{{ t('cancel') }}
</button>
<div class="card-actions">
<button
class="secondary"
@click="requestClose"
>
{{ t('cancel') }}
</button>
<button class="primary"
@click="requestSave">
{{ t('save') }}
</button>
<button
class="primary"
@click="requestSave"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</template>
<i18n>
{
"en": {
"title": "Full Name",
"firstname": "First Name",
"lastname": "Last Name"
},
"fr": {
"title": "Nom complet",
"firstname": "Prénom",
"lastname": "Nom"
},
"es": {
"title": "Nombre completo",
"firstname": "Nombre",
"lastname": "Apellido"
}
"en": {
"title": "Full Name",
"firstname": "First Name",
"lastname": "Last Name"
},
"fr": {
"title": "Nom complet",
"firstname": "Prénom",
"lastname": "Nom"
}
}
</i18n>

View File

@@ -1,174 +1,164 @@
<template>
<div class="card dialog">
<div class="card-title">{{ t('changeEmail') }}</div>
<div class="card-content">
<v-text-field
v-model="email"
class="w-full p-2"
:label="t('email')"
type="email"
variant="outlined"
:error-messages="emailErrors"
:rules="emailRules"
validate-on="blur"
/>
<v-alert
v-if="!!errorMessage"
outlined
type="error"
class="mt-4">
{{ errorMessage }}
</v-alert>
<div class="card dialog">
<div class="card-title">{{ t('changeEmail') }}</div>
<div class="card-content">
<v-text-field
v-model="email"
:error-messages="emailErrors"
:label="t('email')"
:rules="emailRules"
class="w-full p-2"
type="email"
validate-on="blur"
variant="outlined"
/>
<div class="card-actions">
<button class="secondary" @click="$emit('closeRequested')">
{{ t('cancel') }}
</button>
<button class="primary" @click="saveEmail" :disabled="!canSave || isLoading">
{{ t('save') }}
</button>
</div>
<v-alert
v-if="!!errorMessage"
class="mt-4"
outlined
type="error"
>
{{ errorMessage }}
</v-alert>
<div class="card-actions">
<button
class="secondary"
@click="$emit('closeRequested')"
>
{{ t('cancel') }}
</button>
<button
:disabled="!canSave || isLoading"
class="primary"
@click="saveEmail"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
const { t } = useI18n();
const client = useClient();
const creatorProfileStore = useCreatorProfileStore();
const { t } = useI18n();
const client = useClient();
const creatorProfileStore = useCreatorProfileStore();
const props = defineProps({
creator: {
type: Object,
required: true
}
});
const props = defineProps({
creator: {
type: Object,
required: true,
},
});
const email = ref(props.creator.presentation?.email || '');
const isLoading = ref(false);
const errorMessage = ref('');
const email = ref(props.creator.presentation?.email || '');
const isLoading = ref(false);
const errorMessage = ref('');
// Email validation
const isValidEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
// Email validation
const isValidEmail = email => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const emailRules = [
v => !!v || t('validation.emailRequired'),
v => isValidEmail(v) || t('validation.emailInvalid'),
];
const emailRules = [
v => !!v || t('validation.emailRequired'),
v => isValidEmail(v) || t('validation.emailInvalid'),
];
const emailErrors = computed(() => {
if (!email.value) {
return [t('validation.emailRequired')];
}
if (!isValidEmail(email.value)) {
return [t('validation.emailInvalid')];
}
return [];
});
const emailErrors = computed(() => {
if (!email.value) {
return [t('validation.emailRequired')];
}
if (!isValidEmail(email.value)) {
return [t('validation.emailInvalid')];
}
return [];
});
const canSave = computed(() => {
return email.value &&
isValidEmail(email.value) &&
email.value !== (props.creator.presentation?.email || '');
});
const canSave = computed(() => {
return email.value && isValidEmail(email.value) && email.value !== (props.creator.presentation?.email || '');
});
async function saveEmail() {
if (!props.creator.id) {
console.error("Creator ID is missing!");
return;
}
async function saveEmail() {
if (!props.creator.id) {
console.error('Creator ID is missing!');
return;
}
if (!canSave.value) {
return;
}
if (!canSave.value) {
return;
}
try {
isLoading.value = true;
errorMessage.value = '';
try {
isLoading.value = true;
errorMessage.value = '';
// Save email
await client.post(
`/api/creators/${props.creator.id}/email`,
{
email: email.value.trim()
}
);
// Save email
await client.post(`/api/creators/${props.creator.id}/email`, {
email: email.value.trim(),
});
// Refresh creator profile
await creatorProfileStore.fetchCreatorProfile();
// Refresh creator profile
await creatorProfileStore.fetchCreatorProfile();
// Close dialog
emit('closeRequested');
} catch (error) {
console.error("Error saving email:", error);
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
} else {
errorMessage.value = error?.response?.data?.message || error.message || t('errors.unexpected');
// Close dialog
emit('closeRequested');
} catch (error) {
console.error('Error saving email:', error);
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
} else {
errorMessage.value = error?.response?.data?.message || error.message || t('errors.unexpected');
}
} finally {
isLoading.value = false;
}
}
} finally {
isLoading.value = false;
}
}
const emit = defineEmits(['closeRequested']);
const emit = defineEmits(['closeRequested']);
</script>
<style scoped>
.dialog {
@apply max-w-md mx-auto;
}
.dialog {
@apply max-w-md mx-auto;
}
</style>
<i18n>
{
"en": {
"changeEmail": "Change Email",
"email": "Email",
"save": "Save",
"cancel": "Cancel",
"validation": {
"emailRequired": "Email is required",
"emailInvalid": "Please enter a valid email address"
"en": {
"changeEmail": "Change Email",
"email": "Email",
"save": "Save",
"cancel": "Cancel",
"validation": {
"emailRequired": "Email is required",
"emailInvalid": "Please enter a valid email address"
},
"errors": {
"unexpected": "An unexpected error occurred"
}
},
"errors": {
"unexpected": "An unexpected error occurred"
"fr": {
"changeEmail": "Modifier l'email",
"email": "Email",
"save": "Enregistrer",
"cancel": "Annuler",
"validation": {
"emailRequired": "L'email est requis",
"emailInvalid": "Veuillez entrer une adresse email valide"
},
"errors": {
"unexpected": "Une erreur inattendue s'est produite"
}
}
},
"fr": {
"changeEmail": "Modifier l'email",
"email": "Email",
"save": "Enregistrer",
"cancel": "Annuler",
"validation": {
"emailRequired": "L'email est requis",
"emailInvalid": "Veuillez entrer une adresse email valide"
},
"errors": {
"unexpected": "Une erreur inattendue s'est produite"
}
},
"es": {
"changeEmail": "Cambiar correo electrónico",
"email": "Correo electrónico",
"save": "Guardar",
"cancel": "Cancelar",
"validation": {
"emailRequired": "El correo electrónico es obligatorio",
"emailInvalid": "Por favor ingrese una dirección de correo electrónico válida"
},
"errors": {
"unexpected": "Se produjo un error inesperado"
}
}
}
</i18n>

View File

@@ -1,87 +1,82 @@
<script setup>
import {ref} from 'vue';
import {useClient} from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useClient } from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
creator: {
required: true
}
});
const { t } = useI18n();
const props = defineProps({
creator: {
required: true,
},
});
const emits = defineEmits(['closeRequested']);
const emits = defineEmits(['closeRequested']);
const name = ref(props.creator.name);
const name = ref(props.creator.name);
const client = useClient();
const client = useClient();
async function save() {
try {
await client.post(
`/api/creators/${props.creator.id}/name`,
{
name: name.value
async function save() {
try {
await client.post(`/api/creators/${props.creator.id}/name`, {
name: name.value,
});
props.creator.name = name.value;
emits('closeRequested');
} catch (error) {
console.error('Error saving title:', error);
}
);
}
props.creator.name = name.value;
emits('closeRequested');
} catch (error) {
console.error('Error saving title:', error);
}
}
const cancel = () => {
emits('closeRequested');
};
const cancel = () => {
emits('closeRequested');
};
</script>
<template>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card-content">
<v-text-field
v-model="name"
:label="t('label')"
outlined
variant="outlined"
></v-text-field>
<div class="card-content">
<v-text-field
v-model="name"
:label="t('label')"
outlined
variant="outlined"
></v-text-field>
<div class="card-actions">
<button class="secondary"
@click="cancel">
{{ t('cancel') }}
</button>
<button class="primary"
@click="save">
{{ t('save') }}
</button>
</div>
<div class="card-actions">
<button
class="secondary"
@click="cancel"
>
{{ t('cancel') }}
</button>
<button
class="primary"
@click="save"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
<style scoped></style>
<i18n>
{
"en": {
"title": "Change Name",
"label": "Your name"
},
"fr": {
"title": "Modifier le nom",
"label": "Votre nom"
},
"es": {
"title": "Cambiar nombre",
"label": "Tu nombre"
}
"en": {
"title": "Change Name",
"label": "Your name"
},
"fr": {
"title": "Modifier le nom",
"label": "Votre nom"
}
}
</i18n>

View File

@@ -1,246 +1,239 @@
<template>
<div class="card dialog">
<div class="card-title">{{ t('changePhoneNumber') }}</div>
<div class="card-content">
<v-text-field
v-model="displayPhoneNumber"
class="w-full p-2"
:label="t('phoneNumber')"
type="tel"
variant="outlined"
:error-messages="phoneErrors"
:rules="phoneRules"
validate-on="blur"
:placeholder="t('phonePlaceholder')"
@input="handlePhoneInput"
@keydown="handleKeydown"
maxlength="14"
/>
<v-alert
v-if="!!errorMessage"
outlined
type="error"
class="mt-4">
{{ errorMessage }}
</v-alert>
<div class="card dialog">
<div class="card-title">{{ t('changePhoneNumber') }}</div>
<div class="card-content">
<v-text-field
v-model="displayPhoneNumber"
:error-messages="phoneErrors"
:label="t('phoneNumber')"
:placeholder="t('phonePlaceholder')"
:rules="phoneRules"
class="w-full p-2"
maxlength="14"
type="tel"
validate-on="blur"
variant="outlined"
@input="handlePhoneInput"
@keydown="handleKeydown"
/>
<div class="card-actions">
<button class="secondary" @click="$emit('closeRequested')">
{{ t('cancel') }}
</button>
<button class="primary" @click="savePhoneNumber" :disabled="!canSave || isLoading">
{{ t('save') }}
</button>
</div>
<v-alert
v-if="!!errorMessage"
class="mt-4"
outlined
type="error"
>
{{ errorMessage }}
</v-alert>
<div class="card-actions">
<button
class="secondary"
@click="$emit('closeRequested')"
>
{{ t('cancel') }}
</button>
<button
:disabled="!canSave || isLoading"
class="primary"
@click="savePhoneNumber"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
const { t } = useI18n();
const client = useClient();
const creatorProfileStore = useCreatorProfileStore();
const { t } = useI18n();
const client = useClient();
const creatorProfileStore = useCreatorProfileStore();
const props = defineProps({
creator: {
type: Object,
required: true
}
});
const props = defineProps({
creator: {
type: Object,
required: true,
},
});
// Format existing phone number to display format
const formatPhoneForDisplay = (phone) => {
if (!phone) return '';
const digits = phone.replace(/\D/g, '');
if (digits.length === 10) {
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
return phone;
};
// Format existing phone number to display format
const formatPhoneForDisplay = phone => {
if (!phone) return '';
const digits = phone.replace(/\D/g, '');
if (digits.length === 10) {
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
return phone;
};
// Extract just the digits from formatted phone
const extractDigits = (formattedPhone) => {
return formattedPhone.replace(/\D/g, '');
};
// Extract just the digits from formatted phone
const extractDigits = formattedPhone => {
return formattedPhone.replace(/\D/g, '');
};
const displayPhoneNumber = ref(formatPhoneForDisplay(props.creator.presentation?.phoneNumber || ''));
const phoneDigits = ref(extractDigits(displayPhoneNumber.value));
const isLoading = ref(false);
const errorMessage = ref('');
const displayPhoneNumber = ref(formatPhoneForDisplay(props.creator.presentation?.phoneNumber || ''));
const phoneDigits = ref(extractDigits(displayPhoneNumber.value));
const isLoading = ref(false);
const errorMessage = ref('');
// Phone number formatting and validation
const formatPhoneNumber = (digits) => {
// Remove all non-digits
const cleaned = digits.replace(/\D/g, '');
// Apply formatting based on length
if (cleaned.length === 0) return '';
if (cleaned.length <= 3) return `(${cleaned}`;
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
};
// Phone number formatting and validation
const formatPhoneNumber = digits => {
// Remove all non-digits
const cleaned = digits.replace(/\D/g, '');
const handlePhoneInput = (event) => {
const input = event.target.value;
const digits = extractDigits(input);
// Limit to 10 digits
if (digits.length > 10) return;
phoneDigits.value = digits;
displayPhoneNumber.value = formatPhoneNumber(digits);
};
// Apply formatting based on length
if (cleaned.length === 0) return '';
if (cleaned.length <= 3) return `(${cleaned}`;
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
};
const handleKeydown = (event) => {
// Allow backspace, delete, tab, escape, enter
if ([8, 9, 27, 13, 46].includes(event.keyCode)) return;
// Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
if ((event.ctrlKey || event.metaKey) && [65, 67, 86, 88].includes(event.keyCode)) return;
// Allow arrow keys
if (event.keyCode >= 35 && event.keyCode <= 40) return;
// Only allow numbers (0-9)
if (event.keyCode < 48 || event.keyCode > 57) {
event.preventDefault();
}
};
const handlePhoneInput = event => {
const input = event.target.value;
const digits = extractDigits(input);
// Watch for changes to phoneDigits to update display
watch(phoneDigits, (newDigits) => {
displayPhoneNumber.value = formatPhoneNumber(newDigits);
});
// Limit to 10 digits
if (digits.length > 10) return;
const isValidPhoneNumber = (digits) => {
return digits.length === 10;
};
phoneDigits.value = digits;
displayPhoneNumber.value = formatPhoneNumber(digits);
};
const phoneRules = [
v => {
const digits = extractDigits(v);
return digits.length > 0 || t('validation.phoneRequired');
},
v => {
const digits = extractDigits(v);
return isValidPhoneNumber(digits) || t('validation.phoneInvalid');
},
];
const handleKeydown = event => {
// Allow backspace, delete, tab, escape, enter
if ([8, 9, 27, 13, 46].includes(event.keyCode)) return;
const phoneErrors = computed(() => {
if (phoneDigits.value.length === 0) {
return [t('validation.phoneRequired')];
}
if (!isValidPhoneNumber(phoneDigits.value)) {
return [t('validation.phoneInvalid')];
}
return [];
});
// Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
if ((event.ctrlKey || event.metaKey) && [65, 67, 86, 88].includes(event.keyCode)) return;
const canSave = computed(() => {
return phoneDigits.value.length === 10 &&
phoneDigits.value !== extractDigits(props.creator.presentation?.phoneNumber || '');
});
// Allow arrow keys
if (event.keyCode >= 35 && event.keyCode <= 40) return;
async function savePhoneNumber() {
if (!props.creator.id) {
console.error("Creator ID is missing!");
return;
}
// Only allow numbers (0-9)
if (event.keyCode < 48 || event.keyCode > 57) {
event.preventDefault();
}
};
if (!canSave.value) {
return;
}
// Watch for changes to phoneDigits to update display
watch(phoneDigits, newDigits => {
displayPhoneNumber.value = formatPhoneNumber(newDigits);
});
try {
isLoading.value = true;
errorMessage.value = '';
const isValidPhoneNumber = digits => {
return digits.length === 10;
};
// Save the formatted phone number
const formattedPhone = formatPhoneNumber(phoneDigits.value);
await client.post(
`/api/creators/${props.creator.id}/phone`,
{
phoneNumber: formattedPhone
}
);
const phoneRules = [
v => {
const digits = extractDigits(v);
return digits.length > 0 || t('validation.phoneRequired');
},
v => {
const digits = extractDigits(v);
return isValidPhoneNumber(digits) || t('validation.phoneInvalid');
},
];
// Refresh creator profile
await creatorProfileStore.fetchCreatorProfile();
// Close dialog
emit('closeRequested');
} catch (error) {
console.error("Error saving phone number:", error);
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
} else {
errorMessage.value = error?.response?.data?.message || error.message || t('errors.unexpected');
const phoneErrors = computed(() => {
if (phoneDigits.value.length === 0) {
return [t('validation.phoneRequired')];
}
if (!isValidPhoneNumber(phoneDigits.value)) {
return [t('validation.phoneInvalid')];
}
return [];
});
const canSave = computed(() => {
return (
phoneDigits.value.length === 10 &&
phoneDigits.value !== extractDigits(props.creator.presentation?.phoneNumber || '')
);
});
async function savePhoneNumber() {
if (!props.creator.id) {
console.error('Creator ID is missing!');
return;
}
if (!canSave.value) {
return;
}
try {
isLoading.value = true;
errorMessage.value = '';
// Save the formatted phone number
const formattedPhone = formatPhoneNumber(phoneDigits.value);
await client.post(`/api/creators/${props.creator.id}/phone`, {
phoneNumber: formattedPhone,
});
// Refresh creator profile
await creatorProfileStore.fetchCreatorProfile();
// Close dialog
emit('closeRequested');
} catch (error) {
console.error('Error saving phone number:', error);
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
} else {
errorMessage.value = error?.response?.data?.message || error.message || t('errors.unexpected');
}
} finally {
isLoading.value = false;
}
}
} finally {
isLoading.value = false;
}
}
const emit = defineEmits(['closeRequested']);
const emit = defineEmits(['closeRequested']);
</script>
<style scoped>
.dialog {
@apply max-w-md mx-auto;
}
.dialog {
@apply max-w-md mx-auto;
}
</style>
<i18n>
{
"en": {
"changePhoneNumber": "Change Phone Number",
"phoneNumber": "Phone Number",
"phonePlaceholder": "(555) 123-4567",
"save": "Save",
"cancel": "Cancel",
"validation": {
"phoneRequired": "Phone number is required",
"phoneInvalid": "Please enter a complete 10-digit phone number"
"en": {
"changePhoneNumber": "Change Phone Number",
"phoneNumber": "Phone Number",
"phonePlaceholder": "(555) 123-4567",
"save": "Save",
"cancel": "Cancel",
"validation": {
"phoneRequired": "Phone number is required",
"phoneInvalid": "Please enter a complete 10-digit phone number"
},
"errors": {
"unexpected": "An unexpected error occurred"
}
},
"errors": {
"unexpected": "An unexpected error occurred"
"fr": {
"changePhoneNumber": "Modifier le numéro de téléphone",
"phoneNumber": "Numéro de téléphone",
"phonePlaceholder": "(555) 123-4567",
"save": "Enregistrer",
"cancel": "Annuler",
"validation": {
"phoneRequired": "Le numéro de téléphone est requis",
"phoneInvalid": "Veuillez entrer un numéro de téléphone complet à 10 chiffres"
},
"errors": {
"unexpected": "Une erreur inattendue s'est produite"
}
}
},
"fr": {
"changePhoneNumber": "Modifier le numéro de téléphone",
"phoneNumber": "Numéro de téléphone",
"phonePlaceholder": "(555) 123-4567",
"save": "Enregistrer",
"cancel": "Annuler",
"validation": {
"phoneRequired": "Le numéro de téléphone est requis",
"phoneInvalid": "Veuillez entrer un numéro de téléphone complet à 10 chiffres"
},
"errors": {
"unexpected": "Une erreur inattendue s'est produite"
}
},
"es": {
"changePhoneNumber": "Cambiar número de teléfono",
"phoneNumber": "Número de teléfono",
"phonePlaceholder": "(555) 123-4567",
"save": "Guardar",
"cancel": "Cancelar",
"validation": {
"phoneRequired": "El número de teléfono es obligatorio",
"phoneInvalid": "Por favor ingrese un número de teléfono completo de 10 dígitos"
},
"errors": {
"unexpected": "Se produjo un error inesperado"
}
}
}
</i18n>

View File

@@ -1,119 +1,120 @@
<script setup>
import {computed, ref, watch} from 'vue';
import {useCreatorProfileStore} from '@/stores/creatorProfileStore.js';
import {useClient} from "@/plugins/api.js";
import NameEditor from "@/views/creators/NameEditor.vue";
import {useI18n} from 'vue-i18n';
import { computed, ref, watch } from 'vue';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
import { useClient } from '@/plugins/api.js';
import NameEditor from '@/views/creators/NameEditor.vue';
import { useI18n } from 'vue-i18n';
const props = defineProps({
creator: {
required: true
}
});
const emit = defineEmits(['closeRequested']);
const creatorProfileStore = useCreatorProfileStore();
const client = useClient();
const {t} = useI18n();
const newSlug = ref(props.creator.slug);
const slugReservationId = ref(undefined);
const isOperationPending = ref(false);
const errorMessage = ref('');
const isCurrentHandle = ref(false);
// Watch for changes to the new slug to check if it's the same as the current one
watch(newSlug, (newValue) => {
isCurrentHandle.value = newValue === props.creator.slug;
if (isCurrentHandle.value) {
slugReservationId.value = undefined;
}
});
const canSave = computed(() => slugReservationId.value !== undefined && !isCurrentHandle.value);
function handleSlugReservationIdChanged($event) {
slugReservationId.value = $event;
}
async function save() {
try {
isOperationPending.value = true;
errorMessage.value = '';
await client.put(`/api/creators/${props.creator.id}/slug`, {
slugReservationId: slugReservationId.value
const props = defineProps({
creator: {
required: true,
},
});
await creatorProfileStore.fetchCreatorProfile();
emit('closeRequested');
} catch (error) {
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || 'An unexpected error occurred.';
} else {
errorMessage.value = error?.response?.data?.message || error.message || 'An unexpected error occurred.';
}
} finally {
isOperationPending.value = false;
}
}
const emit = defineEmits(['closeRequested']);
const cancel = () => {
emit('closeRequested');
};
const creatorProfileStore = useCreatorProfileStore();
const client = useClient();
const { t } = useI18n();
const newSlug = ref(props.creator.slug);
const slugReservationId = ref(undefined);
const isOperationPending = ref(false);
const errorMessage = ref('');
const isCurrentHandle = ref(false);
// Watch for changes to the new slug to check if it's the same as the current one
watch(newSlug, newValue => {
isCurrentHandle.value = newValue === props.creator.slug;
if (isCurrentHandle.value) {
slugReservationId.value = undefined;
}
});
const canSave = computed(() => slugReservationId.value !== undefined && !isCurrentHandle.value);
function handleSlugReservationIdChanged($event) {
slugReservationId.value = $event;
}
async function save() {
try {
isOperationPending.value = true;
errorMessage.value = '';
await client.put(`/api/creators/${props.creator.id}/slug`, {
slugReservationId: slugReservationId.value,
});
await creatorProfileStore.fetchCreatorProfile();
emit('closeRequested');
} catch (error) {
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || 'An unexpected error occurred.';
} else {
errorMessage.value = error?.response?.data?.message || error.message || 'An unexpected error occurred.';
}
} finally {
isOperationPending.value = false;
}
}
const cancel = () => {
emit('closeRequested');
};
</script>
<template>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card-content">
<name-editor
v-model:name="newSlug"
:creator-name-reservation-id="slugReservationId"
:original-slug="creator.slug"
@update:creator-name-reservation-id="handleSlugReservationIdChanged"
></name-editor>
<v-alert
v-if="!!errorMessage"
class="mt-4"
outlined
type="error"
>
{{ errorMessage }}
</v-alert>
<div class="card-actions">
<button
class="secondary"
@click="cancel"
>
{{ t('cancel') }}
</button>
<button
:disabled="!canSave || isOperationPending"
class="primary"
@click="save"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
<div class="card-content">
<name-editor
v-model:name="newSlug"
:creator-name-reservation-id="slugReservationId"
@update:creator-name-reservation-id="handleSlugReservationIdChanged"
:original-slug="creator.slug"
></name-editor>
<v-alert
v-if="!!errorMessage"
outlined
type="error"
class="mt-4">
{{ errorMessage }}
</v-alert>
<div class="card-actions">
<button class="secondary"
@click="cancel">
{{ t('cancel') }}
</button>
<button class="primary"
@click="save"
:disabled="!canSave || isOperationPending">
{{ t('save') }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
</style>
<style scoped></style>
<i18n>
{
"en": {
"title": "Change Creator Handle"
},
"fr": {
"title": "Modifier l'identifiant du créateur"
},
"es": {
"title": "Cambiar identificador del creador"
}
"en": {
"title": "Change Creator Handle"
},
"fr": {
"title": "Modifier l'identifiant du créateur"
}
}
</i18n>
</i18n>

View File

@@ -1,86 +1,84 @@
<script setup>
import {useClient} from '@/plugins/api.js';
import {ref} from 'vue';
import { useI18n } from 'vue-i18n';
import {useCreatorProfileStore} from '@/stores/creatorProfileStore.js';
import { useClient } from '@/plugins/api.js';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
const props = defineProps({
creator: {
required: true,
},
});
const emits = defineEmits(['closeRequested']);
const stripeId = ref('');
const { t } = useI18n();
const creatorProfileStore = useCreatorProfileStore();
const client = useClient();
const save = async () => {
try {
await client.post(`/api/membership/stripe-account`, {
stripeAccountId: stripeId.value,
const props = defineProps({
creator: {
required: true,
},
});
await creatorProfileStore.fetchCreatorProfile();
emits('closeRequested');
} catch (error) {
console.error('Error saving stripe id:', error);
}
};
const emits = defineEmits(['closeRequested']);
const cancel = () => {
emits('closeRequested');
};
const stripeId = ref('');
const { t } = useI18n();
const creatorProfileStore = useCreatorProfileStore();
const client = useClient();
const save = async () => {
try {
await client.post(`/api/membership/stripe-account`, {
stripeAccountId: stripeId.value,
});
await creatorProfileStore.fetchCreatorProfile();
emits('closeRequested');
} catch (error) {
console.error('Error saving stripe id:', error);
}
};
const cancel = () => {
emits('closeRequested');
};
</script>
<template>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card-content">
<v-text-field
v-model="stripeId"
:label="t('label')"
outlined
variant="outlined"
></v-text-field>
<div class="card-content">
<v-text-field
v-model="stripeId"
:label="t('label')"
outlined
variant="outlined"
></v-text-field>
<div class="card-actions">
<button class="secondary"
@click="cancel">
{{ t('cancel') }}
</button>
<button class="primary"
@click="save">
{{ t('save') }}
</button>
</div>
<div class="card-actions">
<button
class="secondary"
@click="cancel"
>
{{ t('cancel') }}
</button>
<button
class="primary"
@click="save"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
<style scoped></style>
<i18n>
{
"en": {
"title": "Change Stripe ID",
"label": "Your Stripe ID"
},
"fr": {
"title": "Modifier l'ID Stripe",
"label": "Votre ID Stripe"
},
"es": {
"title": "Cambiar ID de Stripe",
"label": "Tu ID de Stripe"
}
"en": {
"title": "Change Stripe ID",
"label": "Your Stripe ID"
},
"fr": {
"title": "Modifier l'ID Stripe",
"label": "Votre ID Stripe"
}
}
</i18n>

View File

@@ -1,88 +1,82 @@
<script setup>
import {ref} from 'vue';
import {useClient} from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useClient } from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
const props = defineProps({
creator: {
required: true
}
});
const props = defineProps({
creator: {
required: true,
},
});
const emits = defineEmits(['closeRequested']);
const emits = defineEmits(['closeRequested']);
const title = ref(props.creator.title);
const { t } = useI18n();
const title = ref(props.creator.title);
const { t } = useI18n();
const client = useClient();
const client = useClient();
async function save() {
try {
await client.post(
`/api/creators/${props.creator.id}/title`,
{
title: title.value
async function save() {
try {
await client.post(`/api/creators/${props.creator.id}/title`, {
title: title.value,
});
props.creator.title = title.value;
emits('closeRequested');
} catch (error) {
console.error('Error saving title:', error);
}
);
}
props.creator.title = title.value;
emits('closeRequested');
} catch (error) {
console.error('Error saving title:', error);
}
}
const cancel = () => {
emits('closeRequested');
};
const cancel = () => {
emits('closeRequested');
};
</script>
<template>
<div class="card dialog">
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card-title">
{{ t('title') }}
<div class="card-content">
<v-text-field
v-model="title"
:label="t('label')"
outlined
variant="outlined"
></v-text-field>
<div class="card-actions">
<button
class="secondary"
@click="cancel"
>
{{ t('cancel') }}
</button>
<button
class="primary"
@click="save"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
<div class="card-content">
<v-text-field
v-model="title"
:label="t('label')"
outlined
variant="outlined"
></v-text-field>
<div class="card-actions">
<button class="secondary"
@click="cancel">
{{ t('cancel') }}
</button>
<button class="primary"
@click="save">
{{ t('save') }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
</style>
<style scoped></style>
<i18n>
{
"en": {
"title": "Change Title",
"label": "Your title"
},
"fr": {
"title": "Modifier le titre",
"label": "Votre titre"
},
"es": {
"title": "Cambiar título",
"label": "Tu título"
}
"en": {
"title": "Change Title",
"label": "Your title"
},
"fr": {
"title": "Modifier le titre",
"label": "Votre titre"
}
}
</i18n>

View File

@@ -1,210 +1,200 @@
<script setup>
import {ref} from 'vue'
import {useClient} from "@/plugins/api.js";
import X from "@/views/svg/X.vue";
import Tiktok from "@/views/svg/Tiktok.vue";
import Reddit from "@/views/svg/Reddit.vue";
import Web from "@/views/svg/Web.vue";
import Youtube from "@/views/svg/Youtube.vue";
import Linkedin from "@/views/svg/Linkedin.vue";
import Instagram from "@/views/svg/Instagram.vue";
import Facebook from "@/views/svg/Facebook.vue";
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useClient } from '@/plugins/api.js';
import X from '@/views/svg/X.vue';
import Tiktok from '@/views/svg/Tiktok.vue';
import Reddit from '@/views/svg/Reddit.vue';
import Web from '@/views/svg/Web.vue';
import Youtube from '@/views/svg/Youtube.vue';
import Linkedin from '@/views/svg/Linkedin.vue';
import Instagram from '@/views/svg/Instagram.vue';
import Facebook from '@/views/svg/Facebook.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
creator: {
required: true
}
})
const { t } = useI18n();
const props = defineProps({
creator: {
required: true,
},
});
const emits = defineEmits(['closeRequested'])
const emits = defineEmits(['closeRequested']);
const facebookUrl = ref(props.creator.socials.facebookUrl)
const instagramUrl = ref(props.creator.socials.instagramUrl)
const linkedInUrl = ref(props.creator.socials.linkedInUrl)
const redditUrl = ref(props.creator.socials.redditUrl)
const tikTokUrl = ref(props.creator.socials.tikTokUrl)
const websiteUrl = ref(props.creator.socials.websiteUrl)
const xUrl = ref(props.creator.socials.xUrl)
const youtubeUrl = ref(props.creator.socials.youtubeUrl)
const facebookUrl = ref(props.creator.socials.facebookUrl);
const instagramUrl = ref(props.creator.socials.instagramUrl);
const linkedInUrl = ref(props.creator.socials.linkedInUrl);
const redditUrl = ref(props.creator.socials.redditUrl);
const tikTokUrl = ref(props.creator.socials.tikTokUrl);
const websiteUrl = ref(props.creator.socials.websiteUrl);
const xUrl = ref(props.creator.socials.xUrl);
const youtubeUrl = ref(props.creator.socials.youtubeUrl);
const client = useClient()
const save = async () => {
try {
await client.post(
`/api/creators/${props.creator.id}/socials`,
{
"facebookUrl": facebookUrl.value || null,
"instagramUrl": instagramUrl.value || null,
"linkedInUrl": linkedInUrl.value || null,
"redditUrl": redditUrl.value || null,
"tikTokUrl": tikTokUrl.value || null,
"websiteUrl": websiteUrl.value || null,
"xUrl": xUrl.value || null,
"youtubeUrl": youtubeUrl.value || null,
})
const client = useClient();
const save = async () => {
try {
await client.post(`/api/creators/${props.creator.id}/socials`, {
facebookUrl: facebookUrl.value || null,
instagramUrl: instagramUrl.value || null,
linkedInUrl: linkedInUrl.value || null,
redditUrl: redditUrl.value || null,
tikTokUrl: tikTokUrl.value || null,
websiteUrl: websiteUrl.value || null,
xUrl: xUrl.value || null,
youtubeUrl: youtubeUrl.value || null,
});
props.creator.socials.facebookUrl = facebookUrl
props.creator.socials.instagramUrl = instagramUrl
props.creator.socials.linkedInUrl = linkedInUrl
props.creator.socials.redditUrl = redditUrl
props.creator.socials.tikTokUrl = tikTokUrl
props.creator.socials.websiteUrl = websiteUrl
props.creator.socials.xUrl = xUrl
props.creator.socials.youtubeUrl = youtubeUrl
props.creator.socials.facebookUrl = facebookUrl;
props.creator.socials.instagramUrl = instagramUrl;
props.creator.socials.linkedInUrl = linkedInUrl;
props.creator.socials.redditUrl = redditUrl;
props.creator.socials.tikTokUrl = tikTokUrl;
props.creator.socials.websiteUrl = websiteUrl;
props.creator.socials.xUrl = xUrl;
props.creator.socials.youtubeUrl = youtubeUrl;
emits('closeRequested')
} catch (error) {
console.error(error)
}
}
const cancel = () => {
emits('closeRequested')
}
emits('closeRequested');
} catch (error) {
console.error(error);
}
};
const cancel = () => {
emits('closeRequested');
};
</script>
<template>
<div class="card dialog">
<div class="card-title">
{{ t('title') }}
</div>
<div class="card dialog">
<div class="card-content">
<div class="editor-line">
<facebook class="social-icon"></facebook>
<input
v-model="facebookUrl"
:placeholder="t('facebook')"
class="input-field"
type="text"
/>
</div>
<div class="card-title">
{{ t('title') }}
<div class="editor-line">
<instagram class="social-icon"></instagram>
<input
v-model="instagramUrl"
:placeholder="t('instagram')"
class="input-field"
type="text"
/>
</div>
<div class="editor-line">
<linkedin class="social-icon"></linkedin>
<input
v-model="linkedInUrl"
:placeholder="t('linkedin')"
class="input-field"
type="text"
/>
</div>
<div class="editor-line">
<reddit class="social-icon"></reddit>
<input
v-model="redditUrl"
:placeholder="t('reddit')"
class="input-field"
type="text"
/>
</div>
<div class="editor-line">
<tiktok class="social-icon"></tiktok>
<input
v-model="tikTokUrl"
:placeholder="t('tiktok')"
class="input-field"
type="text"
/>
</div>
<div class="editor-line">
<web class="social-icon"></web>
<input
v-model="websiteUrl"
:placeholder="t('website')"
class="input-field"
type="text"
/>
</div>
<div class="editor-line">
<x class="social-icon"></x>
<input
v-model="xUrl"
:placeholder="t('x')"
class="input-field"
type="text"
/>
</div>
<div class="editor-line">
<youtube class="social-icon"></youtube>
<input
v-model="youtubeUrl"
:placeholder="t('youtube')"
class="input-field"
type="text"
/>
</div>
</div>
<div class="card-actions">
<button
class="secondary"
@click="cancel"
>
{{ t('cancel') }}
</button>
<button
class="primary"
@click="save"
>
{{ t('save') }}
</button>
</div>
</div>
<div class="card-content">
<div class="editor-line">
<facebook class="social-icon"></facebook>
<input
v-model="facebookUrl"
class="input-field"
:placeholder="t('facebook')"
type="text"
/>
</div>
<div class="editor-line">
<instagram class="social-icon"></instagram>
<input
v-model="instagramUrl"
class="input-field"
:placeholder="t('instagram')"
type="text"
/>
</div>
<div class="editor-line">
<linkedin class="social-icon"></linkedin>
<input
v-model="linkedInUrl"
class="input-field"
:placeholder="t('linkedin')"
type="text"
/>
</div>
<div class="editor-line">
<reddit class="social-icon"></reddit>
<input
v-model="redditUrl"
class="input-field"
:placeholder="t('reddit')"
type="text"
/>
</div>
<div class="editor-line">
<tiktok class="social-icon"></tiktok>
<input
v-model="tikTokUrl"
class="input-field"
:placeholder="t('tiktok')"
type="text"
/>
</div>
<div class="editor-line">
<web class="social-icon"></web>
<input
v-model="websiteUrl"
class="input-field"
:placeholder="t('website')"
type="text"
/>
</div>
<div class="editor-line">
<x class="social-icon"></x>
<input
v-model="xUrl"
class="input-field"
:placeholder="t('x')"
type="text"
/>
</div>
<div class="editor-line">
<youtube class="social-icon"></youtube>
<input
v-model="youtubeUrl"
class="input-field"
:placeholder="t('youtube')"
type="text"
/>
</div>
</div>
<div class="card-actions">
<button class="secondary"
@click="cancel">
{{ t('cancel') }}
</button>
<button class="primary"
@click="save">
{{ t('save') }}
</button>
</div>
</div>
</template>
<style scoped>
.editor-line {
@apply flex flex-row gap-4;
@apply items-center;
}
.editor-line {
@apply flex flex-row gap-4;
@apply items-center;
}
.social-icon {
@apply w-8 h-8;
}
.input-field {
@apply w-full p-[10px];
@apply rounded-sm;
@apply transition duration-200;
@apply ring-1 ring-[#6D6C70] focus:outline-none focus:ring-hutopySecondary;
@apply hover:ring-hutopyPrimary;
@apply placeholder:text-[#6D6C70]
}
.social-icon {
@apply w-8 h-8;
}
.input-field {
@apply w-full p-[10px];
@apply rounded-sm;
@apply transition duration-200;
@apply ring-1 ring-[#6D6C70] focus:outline-none focus:ring-hutopySecondary;
@apply hover:ring-hutopyPrimary;
@apply placeholder:text-[#6D6C70];
}
</style>
<i18n>
{
"en": {
"title": "Social Media Links"
},
"fr": {
"title": "Liens des réseaux sociaux"
},
"es": {
"title": "Enlaces de redes sociales"
}
"en": {
"title": "Social Media Links"
},
"fr": {
"title": "Liens des réseaux sociaux"
}
}
</i18n>