Merge branch 'community'
# Conflicts: # frontend/src/views/main/Footer.vue # frontend/src/views/main/Landing.vue
This commit is contained in:
@@ -1,57 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<div class="shell-container">
|
<div class="shell-container">
|
||||||
|
|
||||||
<div class="shell-side">
|
<div class="shell-side">
|
||||||
<side-bar></side-bar>
|
<site-bar></site-bar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shell-view">
|
<div class="shell-view">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script async setup>
|
<script async setup>
|
||||||
import { mdiFileAccountOutline } from '@mdi/js';
|
import SiteBar from '@/views/main/SiteBar.vue';
|
||||||
import SideBar from "@/views/main/SideBar.vue";
|
import { useLanguageStore } from '@/stores/languageStore.js';
|
||||||
import { useLanguageStore } from "@/stores/languageStore.js";
|
import { watch } from 'vue';
|
||||||
import { watch } from "vue";
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
// Watch for language changes and update i18n locale
|
// Watch for language changes and update i18n locale
|
||||||
const languageStore = useLanguageStore();
|
const languageStore = useLanguageStore();
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
|
||||||
// Watch for changes to the language store
|
// Watch for changes to the language store
|
||||||
watch(() => languageStore.locale, (newLocale) => {
|
watch(
|
||||||
|
() => languageStore.locale,
|
||||||
|
newLocale => {
|
||||||
if (newLocale) {
|
if (newLocale) {
|
||||||
locale.value = newLocale;
|
locale.value = newLocale;
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.shell-container {
|
.shell-container {
|
||||||
@apply flex flex-col lg:flex-row;
|
@apply flex flex-col;
|
||||||
|
@apply w-full;
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
@apply bg-hBackground text-hOnBackground;
|
@apply bg-hBackground text-hOnBackground;
|
||||||
@apply min-h-screen h-full;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-side {
|
.shell-side {
|
||||||
@apply lg:fixed lg:max-h-screen;
|
|
||||||
@apply flex-shrink-0;
|
@apply flex-shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-view {
|
.shell-view {
|
||||||
@apply flex-grow;
|
@apply flex-grow;
|
||||||
@apply flex justify-center items-center;
|
@apply flex justify-center items-center;
|
||||||
@apply w-full;
|
|
||||||
@apply lg:ml-64;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
.banner-image {
|
|
||||||
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%; /* Augmenter la largeur de l'image */
|
|
||||||
max-height: 40vh; /* Réduire légèrement la hauteur de l'image */
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); /* Ajouter une ombre à l'image */
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-images-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0; /* Supprime l'espacement entre les éléments */
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-image-item-wrapper {
|
|
||||||
width: 76%; /* Définit la largeur du conteneur à 20% de la largeur de l'écran */
|
|
||||||
height: auto; /* Garde la hauteur d'origine */
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-image-item {
|
|
||||||
width: 100%; /* Définit la largeur de l'image à 100% du conteneur */
|
|
||||||
height: 300x; /* Hauteur fixe pour l'exemple, à ajuster selon vos besoins */
|
|
||||||
object-fit: cover; /* Coupe l'image pour remplir le conteneur tout en conservant les proportions */
|
|
||||||
}
|
|
||||||
.dropdown-menu {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-image-item:hover .dropdown-menu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-container {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
/* CSS pour ajuster la taille de l'image */
|
|
||||||
.img-small {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-Logo {
|
|
||||||
width: 200px;
|
|
||||||
height: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.text-custom {
|
|
||||||
color: #000000;
|
|
||||||
/* Couleur de texte spécifique */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CSS pour le texte du menu-left */
|
|
||||||
.menu-left a {
|
|
||||||
color: #000000;
|
|
||||||
/* Couleur du texte */
|
|
||||||
font-weight: bold;
|
|
||||||
/* Gras */
|
|
||||||
text-decoration: none;
|
|
||||||
/* Pas de soulignement */
|
|
||||||
font-size: 24px;
|
|
||||||
/* Taille de la police en pixels */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CSS pour le texte des liens du menu-center */
|
|
||||||
.menu-center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
/* Centrer les éléments horizontalement */
|
|
||||||
align-items: center;
|
|
||||||
/* Centrer les éléments verticalement */
|
|
||||||
flex: 1;
|
|
||||||
/* Utiliser tout l'espace disponible */
|
|
||||||
margin-right: 8%
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CSS pour le texte du menu-right */
|
|
||||||
.menu-right a {
|
|
||||||
color: #e4e4e4;
|
|
||||||
/* Couleur du texte */
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-custom {
|
|
||||||
background-color: #ffffff;
|
|
||||||
/* Définissez la couleur de fond souhaitée */
|
|
||||||
}
|
|
||||||
|
|
||||||
.textLogo {
|
|
||||||
font-size: 35px;
|
|
||||||
/* Taille de la police en pixels */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
margin-right: 5px;
|
|
||||||
/* Réduire la marge entre le logo et le texte */
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.profilePicture {
|
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
|
||||||
/* Ajouter une ombre à la photo */
|
|
||||||
border: 2px solid #a30e79;
|
|
||||||
/* Ajouter une bordure de 2px solide de couleur rouge (#f00) */
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-customdarker {
|
|
||||||
background-color: #ffffffa4;
|
|
||||||
/* Définissez la couleur de fond souhaitée */
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-aligned-column {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center-column {
|
|
||||||
flex: 3;
|
|
||||||
/* La colonne centrale occupe 3 fois plus d'espace que les autres */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.colum-aligncenter {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-center a:nth-child(1):hover svg {
|
|
||||||
color: rgba(163, 14, 121, 1);
|
|
||||||
/* Changer la couleur en rouge au survol */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pour le deuxième bouton */
|
|
||||||
.menu-center a:nth-child(2):hover svg {
|
|
||||||
color: rgba(163, 14, 121, 1);
|
|
||||||
/* Changer la couleur en bleu au survol */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pour le troisième bouton */
|
|
||||||
.menu-center a:nth-child(3):hover svg {
|
|
||||||
color: rgba(163, 14, 121, 1);
|
|
||||||
/* Changer la couleur en vert au survol */
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"save": "Guardar",
|
|
||||||
"cancel": "Cancelar",
|
|
||||||
"edit": "Editar",
|
|
||||||
"delete": "Eliminar",
|
|
||||||
"create": "Crear",
|
|
||||||
"apply": "Aplicar",
|
|
||||||
"preview": "Vista previa",
|
|
||||||
"label": "Etiqueta",
|
|
||||||
"confirm": "Confirmar",
|
|
||||||
"close": "Cerrar",
|
|
||||||
"accept": "Aceptar",
|
|
||||||
"loading": "Cargando...",
|
|
||||||
"error": "Error",
|
|
||||||
"success": "Éxito",
|
|
||||||
"changesSaved": "Cambios guardados con éxito",
|
|
||||||
"errorOccurred": "Ha ocurrido un error",
|
|
||||||
"name": "Nombre",
|
|
||||||
"email": "Correo electrónico",
|
|
||||||
"password": "Contraseña",
|
|
||||||
"description": "Descripción",
|
|
||||||
"title": "Título",
|
|
||||||
"image": "Imagen",
|
|
||||||
"file": "Archivo",
|
|
||||||
"required": "Este campo es obligatorio",
|
|
||||||
"invalidEmail": "Correo electrónico inválido",
|
|
||||||
"invalidPassword": "Contraseña inválida",
|
|
||||||
"facebook": "Facebook",
|
|
||||||
"instagram": "Instagram",
|
|
||||||
"linkedin": "LinkedIn",
|
|
||||||
"reddit": "Reddit",
|
|
||||||
"tiktok": "TikTok",
|
|
||||||
"x": "X (Twitter)",
|
|
||||||
"youtube": "YouTube",
|
|
||||||
"website": "Sitio web",
|
|
||||||
"errors": {
|
|
||||||
"unexpected": "Ha ocurrido un error inesperado",
|
|
||||||
"imageLoad": "Error al cargar la imagen",
|
|
||||||
"imageUpload": "Error al subir la imagen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,43 +5,60 @@ import { createPinia } from 'pinia';
|
|||||||
import 'vuetify/styles';
|
import 'vuetify/styles';
|
||||||
import { createVuetify } from 'vuetify';
|
import { createVuetify } from 'vuetify';
|
||||||
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg';
|
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg';
|
||||||
import { VDialog, VApp, VBtn, VProgressLinear, VProgressCircular, VIcon, VTextField, VSnackbar, VForm, VTextarea, VAlert } from 'vuetify/components';
|
import {
|
||||||
import { } from 'vuetify/directives';
|
VAlert,
|
||||||
|
VApp,
|
||||||
|
VBtn,
|
||||||
|
VDialog,
|
||||||
|
VForm,
|
||||||
|
VIcon,
|
||||||
|
VProgressCircular,
|
||||||
|
VProgressLinear,
|
||||||
|
VSnackbar,
|
||||||
|
VTextarea,
|
||||||
|
VTextField,
|
||||||
|
} from 'vuetify/components';
|
||||||
import vueGoogleOauth from 'vue3-google-login';
|
import vueGoogleOauth from 'vue3-google-login';
|
||||||
import { useAuthStore } from "@/stores/authStore.js";
|
import { useAuthStore } from '@/stores/authStore.js';
|
||||||
import { useUserProfileStore } from "@/stores/userProfileStore.js";
|
import { useUserProfileStore } from '@/stores/userProfileStore.js';
|
||||||
import { useCreatorProfileStore } from "@/stores/creatorProfileStore.js";
|
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
||||||
import Toast, { POSITION } from 'vue-toastification';
|
import Toast, { POSITION } from 'vue-toastification';
|
||||||
import 'vue-toastification/dist/index.css';
|
import 'vue-toastification/dist/index.css';
|
||||||
import './assets/main.css';
|
import './assets/main.css';
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import en from '@/locales/en.json';
|
||||||
|
import fr from '@/locales/fr.json';
|
||||||
|
|
||||||
const vuetify = createVuetify({
|
const vuetify = createVuetify({
|
||||||
components: {
|
components: {
|
||||||
VDialog, VApp, VBtn, VProgressLinear, VProgressCircular, VIcon, VTextField, VSnackbar, VForm, VTextarea, VAlert
|
VDialog,
|
||||||
},
|
VApp,
|
||||||
directives: {
|
VBtn,
|
||||||
|
VProgressLinear,
|
||||||
|
VProgressCircular,
|
||||||
|
VIcon,
|
||||||
|
VTextField,
|
||||||
|
VSnackbar,
|
||||||
|
VForm,
|
||||||
|
VTextarea,
|
||||||
|
VAlert,
|
||||||
},
|
},
|
||||||
|
directives: {},
|
||||||
icons: {
|
icons: {
|
||||||
defaultSet: 'mdi',
|
defaultSet: 'mdi',
|
||||||
aliases,
|
aliases,
|
||||||
sets: { mdi }
|
sets: { mdi },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n'
|
|
||||||
import en from '@/locales/en.json'
|
|
||||||
import fr from '@/locales/fr.json'
|
|
||||||
import es from '@/locales/es.json'
|
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'fr',
|
||||||
messages: {
|
messages: {
|
||||||
en: en,
|
en: en,
|
||||||
fr: fr,
|
fr: fr,
|
||||||
es: es
|
},
|
||||||
}
|
});
|
||||||
})
|
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia';
|
||||||
import { useSessionStorage } from '@vueuse/core'
|
import { useSessionStorage } from '@vueuse/core';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
export const useLanguageStore = defineStore(
|
const ALLOWED_LOCALES = ['en', 'fr'];
|
||||||
'language',
|
const DEFAULT_LOCALE = 'fr';
|
||||||
() => {
|
|
||||||
// Initialize with the stored value or default to 'fr'
|
|
||||||
const storedLocale = useSessionStorage('user-locale', 'fr')
|
|
||||||
|
|
||||||
// Get i18n instance
|
export const useLanguageStore = defineStore('language', () => {
|
||||||
const { locale } = useI18n()
|
const storedLocale = useSessionStorage('user-locale', DEFAULT_LOCALE);
|
||||||
|
|
||||||
// Set the initial locale from storage
|
// Get i18n instance (provided globally)
|
||||||
if (locale && storedLocale.value) {
|
const { locale } = useI18n();
|
||||||
locale.value = storedLocale.value
|
|
||||||
|
function sanitizeLocale(value) {
|
||||||
|
return ALLOWED_LOCALES.includes(value) ? value : DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize locale with a sanitized value
|
||||||
|
const initial = sanitizeLocale(storedLocale.value);
|
||||||
|
storedLocale.value = initial;
|
||||||
|
if (locale) {
|
||||||
|
locale.value = initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLocale(newLocale) {
|
function setLocale(newLocale) {
|
||||||
|
const next = sanitizeLocale(newLocale);
|
||||||
if (locale) {
|
if (locale) {
|
||||||
locale.value = newLocale
|
locale.value = next;
|
||||||
}
|
}
|
||||||
storedLocale.value = newLocale
|
storedLocale.value = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale: storedLocale,
|
locale: storedLocale,
|
||||||
setLocale
|
setLocale,
|
||||||
}
|
allowedLocales: ALLOWED_LOCALES,
|
||||||
}
|
};
|
||||||
)
|
});
|
||||||
|
|||||||
@@ -14,27 +14,38 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="email" class="form-label">{{ t('email') }}</label>
|
<label
|
||||||
|
class="form-label"
|
||||||
|
for="email"
|
||||||
|
>
|
||||||
|
{{ t('email') }}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
|
||||||
class="form-input"
|
class="form-input"
|
||||||
required
|
required
|
||||||
|
type="email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
|
||||||
class="primary w-full"
|
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
class="primary w-full"
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
<span v-if="isLoading" class="loading-spinner mr-2"></span>
|
<span
|
||||||
|
v-if="isLoading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
></span>
|
||||||
{{ t('resetPassword') }}
|
{{ t('resetPassword') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<router-link to="/login" class="text-sm text-blue-500">
|
<router-link
|
||||||
|
class="text-sm text-blue-500"
|
||||||
|
to="/login"
|
||||||
|
>
|
||||||
{{ t('backToLogin') }}
|
{{ t('backToLogin') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,12 +55,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success message -->
|
<!-- Success message -->
|
||||||
<div v-if="showSuccessMessage" class="notification success">
|
<div
|
||||||
|
v-if="showSuccessMessage"
|
||||||
|
class="notification success"
|
||||||
|
>
|
||||||
{{ t('resetEmailSent') }}
|
{{ t('resetEmailSent') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error message -->
|
<!-- Error message -->
|
||||||
<div v-if="showErrorMessage" class="notification error">
|
<div
|
||||||
|
v-if="showErrorMessage"
|
||||||
|
class="notification error"
|
||||||
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +105,7 @@ async function handleForgotPassword() {
|
|||||||
try {
|
try {
|
||||||
// Call password reset API
|
// Call password reset API
|
||||||
await clientApi.post('api/users/forgot-password', {
|
await clientApi.post('api/users/forgot-password', {
|
||||||
email: email.value.trim()
|
email: email.value.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
@@ -137,7 +154,9 @@ async function handleForgotPassword() {
|
|||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
@apply fixed bottom-4 right-4 p-4 mb-4 rounded-lg text-sm;
|
@apply fixed bottom-4 right-4 p-4 mb-4 rounded-lg text-sm;
|
||||||
animation: fade-in 0.3s ease-in, fade-out 0.3s ease-out 5s forwards;
|
animation:
|
||||||
|
fade-in 0.3s ease-in,
|
||||||
|
fade-out 0.3s ease-out 5s forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
@@ -194,16 +213,6 @@ async function handleForgotPassword() {
|
|||||||
"resetEmailSent": "Email de réinitialisation du mot de passe envoyé. Veuillez vérifier votre boîte de réception.",
|
"resetEmailSent": "Email de réinitialisation du mot de passe envoyé. Veuillez vérifier votre boîte de réception.",
|
||||||
"resetRequestFailed": "Échec de la demande de réinitialisation du mot de passe. Veuillez réessayer.",
|
"resetRequestFailed": "Échec de la demande de réinitialisation du mot de passe. Veuillez réessayer.",
|
||||||
"emailRequired": "L'email est requis."
|
"emailRequired": "L'email est requis."
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "¿Olvidaste tu contraseña?",
|
|
||||||
"description": "Por favor, introduce la dirección de correo electrónico de tu cuenta. Te enviaremos un enlace para restablecer tu contraseña.",
|
|
||||||
"email": "Correo electrónico",
|
|
||||||
"resetPassword": "Restablecer contraseña",
|
|
||||||
"backToLogin": "Volver al inicio de sesión",
|
|
||||||
"resetEmailSent": "Correo electrónico de restablecimiento de contraseña enviado. Por favor revise su bandeja de entrada.",
|
|
||||||
"resetRequestFailed": "No se pudo solicitar el restablecimiento de contraseña. Por favor, inténtelo de nuevo.",
|
|
||||||
"emailRequired": "El correo electrónico es obligatorio."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-full w-full items-center justify-center p-4">
|
<div class="flex min-h-full w-full items-center justify-center p-4">
|
||||||
|
|
||||||
<div class="flex w-full max-w-[512px] flex-col gap-10">
|
<div class="flex w-full max-w-[512px] flex-col gap-10">
|
||||||
<h1 class="login-text text-center text-2xl font-bold">
|
<h1 class="login-text text-center text-2xl font-bold">
|
||||||
{{ t('title') }}
|
{{ t('title') }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<google-login :callback="googleCallback" popup-type="TOKEN">
|
<google-login
|
||||||
|
:callback="googleCallback"
|
||||||
|
popup-type="TOKEN"
|
||||||
|
>
|
||||||
<button class="secondary">
|
<button class="secondary">
|
||||||
<v-icon class="mr-2" :icon="mdiGoogle" />
|
<v-icon
|
||||||
|
:icon="mdiGoogle"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
{{ t('continueWithGoogle') }}
|
{{ t('continueWithGoogle') }}
|
||||||
</button>
|
</button>
|
||||||
</google-login>
|
</google-login>
|
||||||
@@ -25,44 +29,73 @@
|
|||||||
<!-- Add email/password form -->
|
<!-- Add email/password form -->
|
||||||
<v-form @submit.prevent="handleLocalLogin">
|
<v-form @submit.prevent="handleLocalLogin">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<v-text-field v-model="email" :label="t('email')" type="email" required></v-text-field>
|
<v-text-field
|
||||||
|
v-model="email"
|
||||||
|
:label="t('email')"
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
<v-text-field v-model="password" :label="t('password')" :type="showPassword ? 'text' : 'password'" required>
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
:label="t('password')"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
required
|
||||||
|
>
|
||||||
<template v-slot:append-inner>
|
<template v-slot:append-inner>
|
||||||
<v-icon @click="showPassword = !showPassword" class="visibility-toggle" size="small"
|
<v-icon
|
||||||
:icon="showPassword ? mdiEyeOff : mdiEye" />
|
:icon="showPassword ? mdiEyeOff : mdiEye"
|
||||||
|
class="visibility-toggle"
|
||||||
|
size="small"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
|
|
||||||
<v-btn type="submit" color="primary" block>
|
<v-btn
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
{{ t('signIn') }}
|
{{ t('signIn') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a @click="forgotPassword" class="cursor-pointer text-sm text-blue-500">
|
<a
|
||||||
|
class="cursor-pointer text-sm text-blue-500"
|
||||||
|
@click="forgotPassword"
|
||||||
|
>
|
||||||
{{ t('forgotPassword') }}
|
{{ t('forgotPassword') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 text-center">
|
<div class="mt-2 text-center">
|
||||||
<a @click="resendVerification" class="cursor-pointer text-sm text-blue-500">
|
<a
|
||||||
|
class="cursor-pointer text-sm text-blue-500"
|
||||||
|
@click="resendVerification"
|
||||||
|
>
|
||||||
{{ t('resendVerification') }}
|
{{ t('resendVerification') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 text-center">
|
<div class="mt-4 text-center">
|
||||||
{{ t('noAccount') }}
|
{{ t('noAccount') }}
|
||||||
<router-link to="/register" class="text-blue-500">
|
<router-link
|
||||||
|
class="text-blue-500"
|
||||||
|
to="/register"
|
||||||
|
>
|
||||||
{{ t('register') }}
|
{{ t('register') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error notification -->
|
<!-- Error notification -->
|
||||||
<v-snackbar v-model="errorSnackBar" color="error">
|
<v-snackbar
|
||||||
|
v-model="errorSnackBar"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
{{ t('loginFailed') }}
|
{{ t('loginFailed') }}
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,11 +103,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { GoogleLogin } from "vue3-google-login";
|
import { GoogleLogin } from 'vue3-google-login';
|
||||||
import { useAuthStore } from '@/stores/authStore.js';
|
import { useAuthStore } from '@/stores/authStore.js';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { mdiGoogle, mdiEye, mdiEyeOff } from '@mdi/js';
|
import { mdiEye, mdiEyeOff, mdiGoogle } from '@mdi/js';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -88,8 +121,8 @@ const showPassword = ref(false);
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
returnUrl: {
|
returnUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '/landing'
|
default: '/landing',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleLocalLogin() {
|
async function handleLocalLogin() {
|
||||||
@@ -176,20 +209,6 @@ function resendVerification() {
|
|||||||
"register": "S'inscrire",
|
"register": "S'inscrire",
|
||||||
"loginFailed": "Échec de la connexion. Veuillez vérifier vos identifiants.",
|
"loginFailed": "Échec de la connexion. Veuillez vérifier vos identifiants.",
|
||||||
"continueWithGoogle": "Continuer avec Google"
|
"continueWithGoogle": "Continuer avec Google"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Iniciar sesión",
|
|
||||||
"alt": "Inicio de sesión",
|
|
||||||
"email": "Correo electrónico",
|
|
||||||
"password": "Contraseña",
|
|
||||||
"signIn": "Conéctate",
|
|
||||||
"forgotPassword": "¿Olvidó su contraseña?",
|
|
||||||
"resendVerification": "Reenviar correo de verificación",
|
|
||||||
"orContinueWith": "o",
|
|
||||||
"noAccount": "¿No tiene una cuenta?",
|
|
||||||
"register": "Registrarse",
|
|
||||||
"loginFailed": "Error de inicio de sesión. Por favor, compruebe sus credenciales.",
|
|
||||||
"continueWithGoogle": "Continuar con Google"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
{{ t('success.backToLogin') }}
|
{{ t('success.backToLogin') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
class="text-blue-500 hover:underline"
|
|
||||||
:to="{ path: '/verify-email', query: { email: userEmail } }"
|
:to="{ path: '/verify-email', query: { email: userEmail } }"
|
||||||
|
class="text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
{{ t('success.resendVerification') }}
|
{{ t('success.resendVerification') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -232,26 +232,6 @@
|
|||||||
"backToLogin": "Retour à la connexion",
|
"backToLogin": "Retour à la connexion",
|
||||||
"resendVerification": "Vous n'avez pas reçu l'email? Renvoyer la vérification"
|
"resendVerification": "Vous n'avez pas reçu l'email? Renvoyer la vérification"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Crea tu cuenta",
|
|
||||||
"alt": "Registro de Hutopy",
|
|
||||||
"name": "Nombre completo",
|
|
||||||
"email": "Correo electrónico",
|
|
||||||
"password": "Contraseña",
|
|
||||||
"confirmPassword": "Confirmar contraseña",
|
|
||||||
"passwordRequirements": "La contraseña debe tener al menos 8 caracteres",
|
|
||||||
"register": "Registrarse",
|
|
||||||
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
|
|
||||||
"signIn": "Iniciar sesión",
|
|
||||||
"passwordsDoNotMatch": "Las contraseñas no coinciden",
|
|
||||||
"registrationFailed": "El registro falló. Por favor, inténtelo de nuevo.",
|
|
||||||
"success": {
|
|
||||||
"title": "¡Registro exitoso!",
|
|
||||||
"message": "Por favor revisa tu correo electrónico para verificar tu cuenta. Hemos enviado un enlace de verificación a:",
|
|
||||||
"backToLogin": "Volver al inicio de sesión",
|
|
||||||
"resendVerification": "¿No recibiste el correo? Reenviar verificación"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -259,19 +259,6 @@
|
|||||||
"passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères",
|
"passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères",
|
||||||
"resetFailed": "Échec de la réinitialisation du mot de passe. Veuillez réessayer ou demander un nouveau lien de réinitialisation.",
|
"resetFailed": "Échec de la réinitialisation du mot de passe. Veuillez réessayer ou demander un nouveau lien de réinitialisation.",
|
||||||
"invalidResetLink": "Lien de réinitialisation invalide ou expiré. Veuillez demander une nouvelle réinitialisation de mot de passe."
|
"invalidResetLink": "Lien de réinitialisation invalide ou expiré. Veuillez demander une nouvelle réinitialisation de mot de passe."
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Restablecer su Contraseña",
|
|
||||||
"newPassword": "Nueva Contraseña",
|
|
||||||
"confirmPassword": "Confirmar Contraseña",
|
|
||||||
"passwordRequirements": "La contraseña debe tener al menos 8 caracteres",
|
|
||||||
"resetPassword": "Restablecer Contraseña",
|
|
||||||
"passwordResetSuccess": "¡Su contraseña ha sido restablecida con éxito!",
|
|
||||||
"proceedToLogin": "Proceder al Inicio de Sesión",
|
|
||||||
"passwordsDoNotMatch": "Las contraseñas no coinciden",
|
|
||||||
"passwordTooShort": "La contraseña debe tener al menos 8 caracteres",
|
|
||||||
"resetFailed": "Error al restablecer la contraseña. Inténtelo de nuevo o solicite un nuevo enlace de restablecimiento.",
|
|
||||||
"invalidResetLink": "Enlace de restablecimiento inválido o caducado. Solicite un nuevo restablecimiento de contraseña."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -2,57 +2,97 @@
|
|||||||
<div class="flex min-h-full w-full items-center justify-center p-4">
|
<div class="flex min-h-full w-full items-center justify-center p-4">
|
||||||
<div class="flex w-full max-w-[512px] flex-col gap-10 text-center">
|
<div class="flex w-full max-w-[512px] flex-col gap-10 text-center">
|
||||||
<!-- Loading state while verification is in progress -->
|
<!-- Loading state while verification is in progress -->
|
||||||
<div v-if="isLoading" class="flex flex-col items-center gap-4">
|
<div
|
||||||
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
v-if="isLoading"
|
||||||
|
class="flex flex-col items-center gap-4"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
></v-progress-circular>
|
||||||
<h2 class="text-xl font-medium">{{ t('verifying') }}</h2>
|
<h2 class="text-xl font-medium">{{ t('verifying') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success state -->
|
<!-- Success state -->
|
||||||
<div v-else-if="verificationSuccess" class="flex flex-col items-center gap-6">
|
<div
|
||||||
<v-icon icon="mdi-check-circle" color="green" size="64"></v-icon>
|
v-else-if="verificationSuccess"
|
||||||
|
class="flex flex-col items-center gap-6"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
color="green"
|
||||||
|
icon="mdi-check-circle"
|
||||||
|
size="64"
|
||||||
|
></v-icon>
|
||||||
<h1 class="text-2xl font-bold text-green-600">{{ t('success.title') }}</h1>
|
<h1 class="text-2xl font-bold text-green-600">{{ t('success.title') }}</h1>
|
||||||
<p>{{ t('success.message') }}</p>
|
<p>{{ t('success.message') }}</p>
|
||||||
<v-btn color="primary" @click="goToLogin">{{ t('success.goToLogin') }}</v-btn>
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="goToLogin"
|
||||||
|
>
|
||||||
|
{{ t('success.goToLogin') }}
|
||||||
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error state -->
|
<!-- Error state -->
|
||||||
<div v-else class="flex flex-col items-center gap-6">
|
<div
|
||||||
<v-icon icon="mdi-alert-circle" color="error" size="64"></v-icon>
|
v-else
|
||||||
|
class="flex flex-col items-center gap-6"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
color="error"
|
||||||
|
icon="mdi-alert-circle"
|
||||||
|
size="64"
|
||||||
|
></v-icon>
|
||||||
<h1 class="text-2xl font-bold text-red-600">{{ t('error.title') }}</h1>
|
<h1 class="text-2xl font-bold text-red-600">{{ t('error.title') }}</h1>
|
||||||
<p>{{ errorMessage || t('error.defaultMessage') }}</p>
|
<p>{{ errorMessage || t('error.defaultMessage') }}</p>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col gap-4 w-full">
|
<div class="mt-4 flex flex-col gap-4 w-full">
|
||||||
<v-btn color="primary" @click="goToLogin">{{ t('error.goToLogin') }}</v-btn>
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="goToLogin"
|
||||||
|
>
|
||||||
|
{{ t('error.goToLogin') }}
|
||||||
|
</v-btn>
|
||||||
<v-divider class="my-4"></v-divider>
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
<!-- Resend verification email section -->
|
<!-- Resend verification email section -->
|
||||||
<h2 class="text-xl font-medium">{{ t('resend.title') }}</h2>
|
<h2 class="text-xl font-medium">{{ t('resend.title') }}</h2>
|
||||||
<v-form @submit.prevent="handleResendVerification" class="w-full">
|
<v-form
|
||||||
|
class="w-full"
|
||||||
|
@submit.prevent="handleResendVerification"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="resendEmail"
|
v-model="resendEmail"
|
||||||
:label="t('resend.emailLabel')"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
:error-messages="resendEmailError"
|
:error-messages="resendEmailError"
|
||||||
|
:label="t('resend.emailLabel')"
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
type="submit"
|
|
||||||
color="secondary"
|
|
||||||
block
|
|
||||||
:loading="resendLoading"
|
:loading="resendLoading"
|
||||||
|
block
|
||||||
|
color="secondary"
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
{{ t('resend.button') }}
|
{{ t('resend.button') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<!-- Resend success message -->
|
<!-- Resend success message -->
|
||||||
<div v-if="resendSuccess" class="mt-2 p-3 bg-green-50 border border-green-200 rounded text-green-700 text-sm">
|
<div
|
||||||
|
v-if="resendSuccess"
|
||||||
|
class="mt-2 p-3 bg-green-50 border border-green-200 rounded text-green-700 text-sm"
|
||||||
|
>
|
||||||
{{ t('resend.success') }}
|
{{ t('resend.success') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Resend error message -->
|
<!-- Resend error message -->
|
||||||
<div v-if="resendError" class="mt-2 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">
|
<div
|
||||||
|
v-if="resendError"
|
||||||
|
class="mt-2 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm"
|
||||||
|
>
|
||||||
{{ resendError }}
|
{{ resendError }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,10 +104,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useClient } from '@/plugins/api.js';
|
import { useClient } from '@/plugins/api.js';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -131,7 +171,7 @@ async function handleResendVerification() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await clientApi.post('/api/users/resend-verification', {
|
await clientApi.post('/api/users/resend-verification', {
|
||||||
email: resendEmail.value.trim()
|
email: resendEmail.value.trim(),
|
||||||
});
|
});
|
||||||
resendSuccess.value = true;
|
resendSuccess.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -192,28 +232,6 @@ function goToLogin() {
|
|||||||
"error": "Échec de l'envoi de l'email de vérification. Veuillez réessayer.",
|
"error": "Échec de l'envoi de l'email de vérification. Veuillez réessayer.",
|
||||||
"invalidEmail": "Veuillez entrer une adresse email valide."
|
"invalidEmail": "Veuillez entrer une adresse email valide."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"verifying": "Verificando tu correo electrónico...",
|
|
||||||
"success": {
|
|
||||||
"title": "¡Correo electrónico verificado con éxito!",
|
|
||||||
"message": "Tu correo electrónico ha sido verificado. Ahora puedes iniciar sesión en tu cuenta.",
|
|
||||||
"goToLogin": "Ir al inicio de sesión"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"title": "Falló la verificación",
|
|
||||||
"defaultMessage": "No pudimos verificar tu correo electrónico. El enlace puede ser inválido o estar caducado.",
|
|
||||||
"missingParams": "Faltan parámetros de verificación requeridos.",
|
|
||||||
"goToLogin": "Ir al inicio de sesión"
|
|
||||||
},
|
|
||||||
"resend": {
|
|
||||||
"title": "Reenviar correo de verificación",
|
|
||||||
"emailLabel": "Correo electrónico",
|
|
||||||
"button": "Reenviar correo de verificación",
|
|
||||||
"success": "Correo de verificación enviado con éxito. Por favor revisa tu bandeja de entrada.",
|
|
||||||
"error": "Error al enviar el correo de verificación. Por favor, inténtelo de nuevo.",
|
|
||||||
"invalidEmail": "Por favor, introduce una dirección de correo electrónico válida."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -681,36 +681,6 @@
|
|||||||
"descriptionRequired": "La description est obligatoire"
|
"descriptionRequired": "La description est obligatoire"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"edit": "Editar",
|
|
||||||
"save": "Guardar",
|
|
||||||
"cancel": "Cancelar",
|
|
||||||
"creator": {
|
|
||||||
"sections": {
|
|
||||||
"about": {
|
|
||||||
"title": "Acerca de",
|
|
||||||
"description": "Descripción",
|
|
||||||
"contactInfo": "Información de contacto",
|
|
||||||
"characters": "caracteres",
|
|
||||||
"formattingHint": "Consejo: ¡Usa saltos de línea y emojis para hacer tu descripción más atractiva!"
|
|
||||||
},
|
|
||||||
"photos": {
|
|
||||||
"title": "Fotos",
|
|
||||||
"image": "Imagen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fields": {
|
|
||||||
"videoUrl": "URL del video",
|
|
||||||
"phoneNumber": "Número de teléfono",
|
|
||||||
"email": "Correo electrónico"
|
|
||||||
},
|
|
||||||
"validation": {
|
|
||||||
"invalidYoutubeUrl": "Por favor, introduce una URL de YouTube o un ID de video válido",
|
|
||||||
"descriptionTooLong": "La descripción no puede exceder los 2000 caracteres",
|
|
||||||
"descriptionRequired": "La descripción es obligatoria"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,31 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<!-- Banner Container with mouse events -->
|
<!-- Banner Container with mouse events -->
|
||||||
<div class="relative overflow-y-auto rounded-b-2xl" @mouseenter="showTint = isCurrentCreator"
|
<div
|
||||||
@mouseleave="showTint = false" @click="isCurrentCreator && openBannerEditor()">
|
class="relative overflow-y-auto rounded-b-2xl"
|
||||||
<img class="banner aspect-[4/1] w-full object-cover"
|
@click="isCurrentCreator && openBannerEditor()"
|
||||||
:src="brandingStore.value?.bannerUrl ?? '/images/placeholders/banner.png'" :alt="t('alt')">
|
@mouseenter="showTint = isCurrentCreator"
|
||||||
|
@mouseleave="showTint = false"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:alt="t('alt')"
|
||||||
|
:src="brandingStore.value?.bannerUrl ?? '/images/placeholders/banner.png'"
|
||||||
|
class="banner aspect-[4/1] w-full object-cover"
|
||||||
|
/>
|
||||||
<!-- Tint Effect -->
|
<!-- Tint Effect -->
|
||||||
<div v-if="showTint" class="absolute inset-0 cursor-pointer bg-black/25">
|
<div
|
||||||
|
v-if="showTint"
|
||||||
|
class="absolute inset-0 cursor-pointer bg-black/25"
|
||||||
|
>
|
||||||
<!-- Top-right Icon -->
|
<!-- Top-right Icon -->
|
||||||
<div
|
<div
|
||||||
class="absolute right-4 top-4 flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg">
|
class="absolute right-4 top-4 flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg"
|
||||||
<v-icon large :icon="mdiPencil" />
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="mdiPencil"
|
||||||
|
large
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-dialog v-model="isDialogOpen" max-width="800px">
|
<v-dialog
|
||||||
<BannerEditor :creator="brandingStore.value" @closeRequested="() => isDialogOpen = false" />
|
v-model="isDialogOpen"
|
||||||
|
max-width="800px"
|
||||||
|
>
|
||||||
|
<BannerEditor
|
||||||
|
:creator="brandingStore.value"
|
||||||
|
@closeRequested="() => (isDialogOpen = false)"
|
||||||
|
/>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import BannerEditor from "@/views/creators/BannerEditor.vue";
|
import BannerEditor from '@/views/creators/BannerEditor.vue';
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from 'vue';
|
||||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||||
import { useAuthStore } from "@/stores/authStore.js";
|
import { useAuthStore } from '@/stores/authStore.js';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { mdiPencil } from '@mdi/js';
|
import { mdiPencil } from '@mdi/js';
|
||||||
|
|
||||||
@@ -56,9 +76,6 @@ const isCurrentCreator = computed(() => {
|
|||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"alt": "Bannière du créateur"
|
"alt": "Bannière du créateur"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"alt": "Banner del creador"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,64 +1,111 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="album-editor">
|
<div class="album-editor">
|
||||||
|
|
||||||
<h2 class="mb-4 text-xl font-semibold">
|
<h2 class="mb-4 text-xl font-semibold">
|
||||||
{{ t('title') }}
|
{{ t('title') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Drop zone with photos -->
|
<!-- Drop zone with photos -->
|
||||||
<div class="drop-zone" @dragover.prevent @drop.prevent="handleDrop" @click="triggerFileInput">
|
<div
|
||||||
|
class="drop-zone"
|
||||||
|
@click="triggerFileInput"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
>
|
||||||
<!-- Upload prompt -->
|
<!-- Upload prompt -->
|
||||||
<div class="drop-zone-content">
|
<div class="drop-zone-content">
|
||||||
<v-icon size="large" :icon="mdiPlus" />
|
<v-icon
|
||||||
|
:icon="mdiPlus"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
<span class="mt-2 text-sm">{{ t('dropzoneText') }}</span>
|
<span class="mt-2 text-sm">{{ t('dropzoneText') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden file input -->
|
<!-- Hidden file input -->
|
||||||
<input type="file" ref="fileInput" @change="handleFileUpload" accept="image/*" multiple class="hidden" />
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
multiple
|
||||||
|
type="file"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Photos grid -->
|
<!-- Photos grid -->
|
||||||
<draggable v-model="localImages" class="photos-grid" item-key="id" @end="handleReorder" :filter="'.action-btn'"
|
<draggable
|
||||||
:prevent-on-filter="false">
|
v-model="localImages"
|
||||||
|
:filter="'.action-btn'"
|
||||||
|
:prevent-on-filter="false"
|
||||||
|
class="photos-grid"
|
||||||
|
item-key="id"
|
||||||
|
@end="handleReorder"
|
||||||
|
>
|
||||||
<template #item="{ element, index }">
|
<template #item="{ element, index }">
|
||||||
<div class="photo-wrapper">
|
<div class="photo-wrapper">
|
||||||
<div class="index-bubble">{{ index + 1 }}</div>
|
<div class="index-bubble">{{ index + 1 }}</div>
|
||||||
<img :src="element.image.originalUrl" :alt="'Image ' + (index + 1)" />
|
<img
|
||||||
|
:alt="'Image ' + (index + 1)"
|
||||||
|
:src="element.image.originalUrl"
|
||||||
|
/>
|
||||||
<!-- Processing spinner overlay -->
|
<!-- Processing spinner overlay -->
|
||||||
<div v-if="element.isProcessing" class="loading-overlay">
|
<div
|
||||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
v-if="element.isProcessing"
|
||||||
|
class="loading-overlay"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
></v-progress-circular>
|
||||||
<span class="mt-2 text-sm text-white">{{ t('processing') }}</span>
|
<span class="mt-2 text-sm text-white">{{ t('processing') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Upload spinner overlay -->
|
<!-- Upload spinner overlay -->
|
||||||
<div v-if="element.isUploading" class="loading-overlay uploading">
|
<div
|
||||||
<v-progress-circular indeterminate color="secondary"></v-progress-circular>
|
v-if="element.isUploading"
|
||||||
|
class="loading-overlay uploading"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
color="secondary"
|
||||||
|
indeterminate
|
||||||
|
></v-progress-circular>
|
||||||
<span class="mt-2 text-sm text-white">{{ t('uploading') }}</span>
|
<span class="mt-2 text-sm text-white">{{ t('uploading') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Left arrow -->
|
<!-- Left arrow -->
|
||||||
<button @click.stop="moveImage(index, 'up')" @touchstart.stop="moveImage(index, 'up')"
|
<button
|
||||||
class="action-btn left-btn" :disabled="index === 0" :title="t('moveLeft')">
|
:disabled="index === 0"
|
||||||
|
:title="t('moveLeft')"
|
||||||
|
class="action-btn left-btn"
|
||||||
|
@click.stop="moveImage(index, 'up')"
|
||||||
|
@touchstart.stop="moveImage(index, 'up')"
|
||||||
|
>
|
||||||
<v-icon :icon="mdiArrowLeft" />
|
<v-icon :icon="mdiArrowLeft" />
|
||||||
</button>
|
</button>
|
||||||
<!-- Right arrow -->
|
<!-- Right arrow -->
|
||||||
<button @click.stop="moveImage(index, 'down')" @touchstart.stop="moveImage(index, 'down')"
|
<button
|
||||||
class="action-btn right-btn" :disabled="index === localImages.length - 1" :title="t('moveRight')">
|
:disabled="index === localImages.length - 1"
|
||||||
|
:title="t('moveRight')"
|
||||||
|
class="action-btn right-btn"
|
||||||
|
@click.stop="moveImage(index, 'down')"
|
||||||
|
@touchstart.stop="moveImage(index, 'down')"
|
||||||
|
>
|
||||||
<v-icon :icon="mdiArrowRight" />
|
<v-icon :icon="mdiArrowRight" />
|
||||||
</button>
|
</button>
|
||||||
<!-- Delete button -->
|
<!-- Delete button -->
|
||||||
<button @click.stop="deleteImage(index)" touchstart.stop="deleteImage(index)" class="action-btn delete-btn"
|
<button
|
||||||
:title="t('delete')">
|
:title="t('delete')"
|
||||||
|
class="action-btn delete-btn"
|
||||||
|
touchstart.stop="deleteImage(index)"
|
||||||
|
@click.stop="deleteImage(index)"
|
||||||
|
>
|
||||||
<v-icon :icon="mdiDelete" />
|
<v-icon :icon="mdiDelete" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { v7 } from 'uuid';
|
import { v7 } from 'uuid';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
@@ -67,8 +114,8 @@ import { mdiArrowLeft, mdiArrowRight, mdiDelete, mdiPlus } from '@mdi/js';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
images: {
|
images: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:images']);
|
const emit = defineEmits(['update:images']);
|
||||||
@@ -83,7 +130,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleFiles(files) {
|
function handleFiles(files) {
|
||||||
console.log('handleFiles:', files)
|
console.log('handleFiles:', files);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.type.startsWith('image/')) {
|
if (file.type.startsWith('image/')) {
|
||||||
try {
|
try {
|
||||||
@@ -102,7 +149,7 @@ function handleFiles(files) {
|
|||||||
|
|
||||||
console.log('Processing image:', tempImage);
|
console.log('Processing image:', tempImage);
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = e => {
|
||||||
console.log('Image loaded:', e);
|
console.log('Image loaded:', e);
|
||||||
const index = localImages.value.findIndex(local => local.image.id === tempImage.image.id);
|
const index = localImages.value.findIndex(local => local.image.id === tempImage.image.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -118,6 +165,7 @@ function handleFiles(files) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrop(event) {
|
function handleDrop(event) {
|
||||||
console.log('Drop triggered');
|
console.log('Drop triggered');
|
||||||
const files = Array.from(event.dataTransfer.files);
|
const files = Array.from(event.dataTransfer.files);
|
||||||
@@ -140,7 +188,6 @@ function handleReorder() {
|
|||||||
emit('update:images', localImages.value);
|
emit('update:images', localImages.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function moveImage(index, direction) {
|
function moveImage(index, direction) {
|
||||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||||
if (newIndex >= 0 && newIndex < localImages.value.length) {
|
if (newIndex >= 0 && newIndex < localImages.value.length) {
|
||||||
@@ -155,7 +202,6 @@ function deleteImage(index) {
|
|||||||
localImages.value.splice(index, 1);
|
localImages.value.splice(index, 1);
|
||||||
emit('update:images', localImages.value);
|
emit('update:images', localImages.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -316,15 +362,6 @@ function deleteImage(index) {
|
|||||||
"moveLeft": "Déplacer à gauche",
|
"moveLeft": "Déplacer à gauche",
|
||||||
"moveRight": "Déplacer à droite",
|
"moveRight": "Déplacer à droite",
|
||||||
"delete": "Supprimer"
|
"delete": "Supprimer"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Album",
|
|
||||||
"dropzoneText": "Suelta una foto aquí para añadirla al álbum",
|
|
||||||
"processing": "Procesando...",
|
|
||||||
"uploading": "Subiendo...",
|
|
||||||
"moveLeft": "Mover a la izquierda",
|
|
||||||
"moveRight": "Mover a la derecha",
|
|
||||||
"delete": "Eliminar"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="hasImages" class="album-view">
|
<div
|
||||||
|
v-if="hasImages"
|
||||||
|
class="album-view"
|
||||||
|
>
|
||||||
<!-- Album Display -->
|
<!-- Album Display -->
|
||||||
<div class="image-grid">
|
<div class="image-grid">
|
||||||
<div v-for="(url, index) in displayedImages"
|
<div
|
||||||
|
v-for="(url, index) in displayedImages"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="image-wrapper"
|
class="image-wrapper"
|
||||||
@click="$emit('photo-click', index)">
|
@click="$emit('photo-click', index)"
|
||||||
<img :src="url"
|
>
|
||||||
|
<img
|
||||||
:alt="t('creator.sections.album.image')"
|
:alt="t('creator.sections.album.image')"
|
||||||
class="image"/>
|
:src="url"
|
||||||
|
class="image"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,15 +25,15 @@
|
|||||||
// Add 'photo-click' to emits
|
// Add 'photo-click' to emits
|
||||||
const emit = defineEmits(['photo-click']);
|
const emit = defineEmits(['photo-click']);
|
||||||
|
|
||||||
import { computed, ref, onMounted, onUnmounted } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
images: {
|
images: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
default: () => []
|
default: () => [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -99,7 +106,6 @@ const gridColumns = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
@@ -123,16 +129,6 @@ const gridColumns = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"creator": {
|
|
||||||
"sections": {
|
|
||||||
"album": {
|
|
||||||
"title": "Álbum de fotos",
|
|
||||||
"image": "Imagen del álbum"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -1,38 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-dialog v-model="dialog" fullscreen :scrim="true" transition="dialog-bottom-transition"
|
<v-dialog
|
||||||
@click:outside="closeViewer">
|
v-model="dialog"
|
||||||
<div class="album-viewer" @click.self="closeViewer">
|
:scrim="true"
|
||||||
|
fullscreen
|
||||||
|
transition="dialog-bottom-transition"
|
||||||
|
@click:outside="closeViewer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="album-viewer"
|
||||||
|
@click.self="closeViewer"
|
||||||
|
>
|
||||||
<!-- Main image container -->
|
<!-- Main image container -->
|
||||||
<div class="image-container">
|
<div class="image-container">
|
||||||
<img :src="currentImage" :alt="t('viewer.imageAlt', { index: currentIndex + 1 })" class="main-image" />
|
<img
|
||||||
|
:alt="t('viewer.imageAlt', { index: currentIndex + 1 })"
|
||||||
|
:src="currentImage"
|
||||||
|
class="main-image"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Navigation buttons -->
|
<!-- Navigation buttons -->
|
||||||
<button class="nav-btn left-btn" @click.stop="previousImage" :disabled="currentIndex === 0"
|
<button
|
||||||
:title="t('viewer.previous')">
|
:disabled="currentIndex === 0"
|
||||||
<v-icon size="large" color="white" :icon="mdiChevronLeft" />
|
:title="t('viewer.previous')"
|
||||||
|
class="nav-btn left-btn"
|
||||||
|
@click.stop="previousImage"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="mdiChevronLeft"
|
||||||
|
color="white"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="nav-btn right-btn" @click.stop="nextImage" :disabled="currentIndex === images.length - 1"
|
<button
|
||||||
:title="t('viewer.next')">
|
:disabled="currentIndex === images.length - 1"
|
||||||
<v-icon size="large" color="white" :icon="mdiChevronRight" />
|
:title="t('viewer.next')"
|
||||||
|
class="nav-btn right-btn"
|
||||||
|
@click.stop="nextImage"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="mdiChevronRight"
|
||||||
|
color="white"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Close button -->
|
<!-- Close button -->
|
||||||
<button class="close-btn" @click.stop="closeViewer" :title="t('viewer.close')">
|
<button
|
||||||
<v-icon size="large" color="white" :icon="mdiClose" />
|
:title="t('viewer.close')"
|
||||||
|
class="close-btn"
|
||||||
|
@click.stop="closeViewer"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="mdiClose"
|
||||||
|
color="white"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Image counter -->
|
<!-- Image counter -->
|
||||||
<div class="image-counter">
|
<div class="image-counter">{{ currentIndex + 1 }} / {{ images.length }}</div>
|
||||||
{{ currentIndex + 1 }} / {{ images.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { mdiChevronLeft, mdiChevronRight, mdiClose } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight, mdiClose } from '@mdi/js';
|
||||||
|
|
||||||
@@ -41,16 +75,16 @@ const { t } = useI18n();
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
startIndex: {
|
startIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
@@ -60,16 +94,22 @@ const currentIndex = ref(0);
|
|||||||
|
|
||||||
const currentImage = computed(() => props.images[currentIndex.value]);
|
const currentImage = computed(() => props.images[currentIndex.value]);
|
||||||
|
|
||||||
watch(() => props.modelValue, (newVal) => {
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
newVal => {
|
||||||
dialog.value = newVal;
|
dialog.value = newVal;
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
currentIndex.value = props.startIndex;
|
currentIndex.value = props.startIndex;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
watch(() => dialog.value, (newVal) => {
|
watch(
|
||||||
|
() => dialog.value,
|
||||||
|
newVal => {
|
||||||
emit('update:modelValue', newVal);
|
emit('update:modelValue', newVal);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function nextImage() {
|
function nextImage() {
|
||||||
if (currentIndex.value < props.images.length - 1) {
|
if (currentIndex.value < props.images.length - 1) {
|
||||||
@@ -162,14 +202,6 @@ function closeViewer() {
|
|||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"imageAlt": "Image {index}"
|
"imageAlt": "Image {index}"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"viewer": {
|
|
||||||
"previous": "Imagen anterior",
|
|
||||||
"next": "Imagen siguiente",
|
|
||||||
"close": "Cerrar",
|
|
||||||
"imageAlt": "Imagen {index}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useBrandingStore } from '@/stores/brandingStore.js';
|
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||||
import DonationButton from '@/views/creators/DonationButton.vue';
|
import DonationButton from '@/views/creators/DonationButton.vue';
|
||||||
import CreatorLogo from "@/views/creators/CreatorLogo.vue";
|
import CreatorLogo from '@/views/creators/CreatorLogo.vue';
|
||||||
import NameTitle from "@/views/creators/NameTitle.vue";
|
import NameTitle from '@/views/creators/NameTitle.vue';
|
||||||
import Linkedin from "@/views/svg/Linkedin.vue";
|
import Linkedin from '@/views/svg/Linkedin.vue';
|
||||||
import X from "@/views/svg/X.vue";
|
import X from '@/views/svg/X.vue';
|
||||||
import Facebook from "@/views/svg/Facebook.vue";
|
import Facebook from '@/views/svg/Facebook.vue';
|
||||||
import Instagram from "@/views/svg/Instagram.vue";
|
import Instagram from '@/views/svg/Instagram.vue';
|
||||||
import Tiktok from "@/views/svg/Tiktok.vue";
|
import Tiktok from '@/views/svg/Tiktok.vue';
|
||||||
import Reddit from "@/views/svg/Reddit.vue";
|
import Reddit from '@/views/svg/Reddit.vue';
|
||||||
import Youtube from "@/views/svg/Youtube.vue";
|
import Youtube from '@/views/svg/Youtube.vue';
|
||||||
import Web from "@/views/svg/Web.vue";
|
import Web from '@/views/svg/Web.vue';
|
||||||
import {useI18n} from 'vue-i18n'
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const brandingStore = useBrandingStore();
|
const brandingStore = useBrandingStore();
|
||||||
const baseURL = window.location.origin;
|
const baseURL = window.location.origin;
|
||||||
@@ -23,7 +23,6 @@ const {t} = useI18n();
|
|||||||
<!-- Container principal avec le profil -->
|
<!-- Container principal avec le profil -->
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<div class="bg-hPrimary text-hOnPrimary relative">
|
<div class="bg-hPrimary text-hOnPrimary relative">
|
||||||
|
|
||||||
<!-- Portrait that overlaps both sections -->
|
<!-- Portrait that overlaps both sections -->
|
||||||
<div class="absolute left-4 -bottom-2 z-10">
|
<div class="absolute left-4 -bottom-2 z-10">
|
||||||
<creator-logo />
|
<creator-logo />
|
||||||
@@ -31,8 +30,7 @@ const {t} = useI18n();
|
|||||||
|
|
||||||
<!-- Desktop version (visible only on écrans moyens et grands) -->
|
<!-- Desktop version (visible only on écrans moyens et grands) -->
|
||||||
<div class="social-info">
|
<div class="social-info">
|
||||||
<div class="w-36">
|
<div class="w-36"></div>
|
||||||
</div>
|
|
||||||
<div class="flex-grow flex flex-row">
|
<div class="flex-grow flex flex-row">
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<name-title></name-title>
|
<name-title></name-title>
|
||||||
@@ -48,74 +46,84 @@ const {t} = useI18n();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section pour les icônes de réseaux sociaux -->
|
<!-- Section pour les icônes de réseaux sociaux -->
|
||||||
<div
|
<div class="h-12 flex w-full items-center justify-center bg-hSecondary text-hOnSecondary relative">
|
||||||
class="h-12 flex w-full items-center justify-center bg-hSecondary text-hOnSecondary relative"
|
|
||||||
>
|
|
||||||
<div class="flex flex-row gap-10">
|
<div class="flex flex-row gap-10">
|
||||||
|
<a
|
||||||
<a v-if="brandingStore.value?.socials?.facebookUrl"
|
v-if="brandingStore.value?.socials?.facebookUrl"
|
||||||
:href="brandingStore.value?.socials?.facebookUrl"
|
:href="brandingStore.value?.socials?.facebookUrl"
|
||||||
|
:title="t('facebook')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('facebook')">
|
>
|
||||||
<facebook class="social-icon"></facebook>
|
<facebook class="social-icon"></facebook>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="brandingStore.value?.socials?.instagramUrl"
|
<a
|
||||||
|
v-if="brandingStore.value?.socials?.instagramUrl"
|
||||||
:href="brandingStore.value?.socials?.instagramUrl"
|
:href="brandingStore.value?.socials?.instagramUrl"
|
||||||
|
:title="t('instagram')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('instagram')">
|
>
|
||||||
<instagram class="social-icon"></instagram>
|
<instagram class="social-icon"></instagram>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="brandingStore.value?.socials?.linkedInUrl"
|
<a
|
||||||
|
v-if="brandingStore.value?.socials?.linkedInUrl"
|
||||||
:href="brandingStore.value?.socials?.linkedInUrl"
|
:href="brandingStore.value?.socials?.linkedInUrl"
|
||||||
|
:title="t('linkedin')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('linkedin')">
|
>
|
||||||
<linkedin class="social-icon"></linkedin>
|
<linkedin class="social-icon"></linkedin>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="brandingStore.value?.socials?.redditUrl"
|
<a
|
||||||
|
v-if="brandingStore.value?.socials?.redditUrl"
|
||||||
:href="brandingStore.value?.socials?.redditUrl"
|
:href="brandingStore.value?.socials?.redditUrl"
|
||||||
|
:title="t('reddit')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('reddit')">
|
>
|
||||||
<reddit class="social-icon"></reddit>
|
<reddit class="social-icon"></reddit>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="brandingStore.value?.socials?.tikTokUrl"
|
<a
|
||||||
|
v-if="brandingStore.value?.socials?.tikTokUrl"
|
||||||
:href="brandingStore.value?.socials?.tikTokUrl"
|
:href="brandingStore.value?.socials?.tikTokUrl"
|
||||||
|
:title="t('tiktok')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('tiktok')">
|
>
|
||||||
<tiktok class="social-icon"></tiktok>
|
<tiktok class="social-icon"></tiktok>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="brandingStore.value?.socials?.xUrl"
|
<a
|
||||||
|
v-if="brandingStore.value?.socials?.xUrl"
|
||||||
:href="brandingStore.value?.socials?.xUrl"
|
:href="brandingStore.value?.socials?.xUrl"
|
||||||
|
:title="t('x')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('x')">
|
>
|
||||||
<x class="social-icon"></x>
|
<x class="social-icon"></x>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="brandingStore.value?.socials?.youtubeUrl"
|
<a
|
||||||
|
v-if="brandingStore.value?.socials?.youtubeUrl"
|
||||||
:href="brandingStore.value?.socials?.youtubeUrl"
|
:href="brandingStore.value?.socials?.youtubeUrl"
|
||||||
|
:title="t('youtube')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('youtube')">
|
>
|
||||||
<youtube class="social-icon"></youtube>
|
<youtube class="social-icon"></youtube>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="brandingStore.value?.socials?.websiteUrl"
|
<a
|
||||||
|
v-if="brandingStore.value?.socials?.websiteUrl"
|
||||||
:href="brandingStore.value?.socials?.websiteUrl"
|
:href="brandingStore.value?.socials?.websiteUrl"
|
||||||
|
:title="t('website')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="t('website')">
|
>
|
||||||
<web class="social-icon"></web>
|
<web class="social-icon"></web>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -157,16 +165,6 @@ const {t} = useI18n();
|
|||||||
"x": "X (Twitter)",
|
"x": "X (Twitter)",
|
||||||
"youtube": "YouTube",
|
"youtube": "YouTube",
|
||||||
"website": "Site web"
|
"website": "Site web"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"facebook": "Facebook",
|
|
||||||
"instagram": "Instagram",
|
|
||||||
"linkedin": "LinkedIn",
|
|
||||||
"reddit": "Reddit",
|
|
||||||
"tiktok": "TikTok",
|
|
||||||
"x": "X (Twitter)",
|
|
||||||
"youtube": "YouTube",
|
|
||||||
"website": "Sitio web"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
<div class="file-input-container">
|
<div class="file-input-container">
|
||||||
<input
|
<input
|
||||||
type="file"
|
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
|
type="file"
|
||||||
@change="onFileSelected"
|
@change="onFileSelected"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -25,29 +25,38 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errorMessage" class="error-message">
|
<div
|
||||||
|
v-if="errorMessage"
|
||||||
|
class="error-message"
|
||||||
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showCropper" class="cropper-wrapper">
|
<div
|
||||||
|
v-if="showCropper"
|
||||||
|
class="cropper-wrapper"
|
||||||
|
>
|
||||||
<Cropper
|
<Cropper
|
||||||
ref="cropper"
|
ref="cropper"
|
||||||
:src="fileUrl"
|
|
||||||
:aspect-ratio="4"
|
:aspect-ratio="4"
|
||||||
|
:src="fileUrl"
|
||||||
:stencil-props="{
|
:stencil-props="{
|
||||||
aspectRatio: 4,
|
aspectRatio: 4,
|
||||||
class: 'banner-stencil'
|
class: 'banner-stencil',
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="image-preview-container"
|
<div
|
||||||
|
v-else
|
||||||
|
class="image-preview-container"
|
||||||
@click="startEditing"
|
@click="startEditing"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.prevent="handleDrop">
|
@drop.prevent="handleDrop"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
:src="fileUrl || fallbackUrl"
|
|
||||||
:alt="t('preview')"
|
:alt="t('preview')"
|
||||||
|
:src="fileUrl || fallbackUrl"
|
||||||
class="preview-image"
|
class="preview-image"
|
||||||
/>
|
/>
|
||||||
<div class="edit-overlay">
|
<div class="edit-overlay">
|
||||||
@@ -57,14 +66,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
|
:disabled="isUploading"
|
||||||
|
class="secondary"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
:disabled="isUploading">
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
|
:disabled="!selectedFile || isUploading"
|
||||||
|
class="primary"
|
||||||
@click="showCropper ? applyCrop() : publish()"
|
@click="showCropper ? applyCrop() : publish()"
|
||||||
:disabled="!selectedFile || isUploading">
|
>
|
||||||
<template v-if="isUploading">
|
<template v-if="isUploading">
|
||||||
<span class="loading-spinner"></span>
|
<span class="loading-spinner"></span>
|
||||||
{{ t('uploading') }} ({{ uploadProgress }}%)
|
{{ t('uploading') }} ({{ uploadProgress }}%)
|
||||||
@@ -78,154 +91,150 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue'
|
import { ref } from 'vue';
|
||||||
import {useClient} from '@/plugins/api.js'
|
import { useClient } from '@/plugins/api.js';
|
||||||
import { Cropper } from 'vue-advanced-cropper'
|
import { Cropper } from 'vue-advanced-cropper';
|
||||||
import 'vue-advanced-cropper/dist/style.css'
|
import 'vue-advanced-cropper/dist/style.css';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['closeRequested'])
|
const emits = defineEmits(['closeRequested']);
|
||||||
|
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null);
|
||||||
const selectedFile = ref(null)
|
const selectedFile = ref(null);
|
||||||
const fileUrl = ref(props.creator?.bannerUrl)
|
const fileUrl = ref(props.creator?.bannerUrl);
|
||||||
const fallbackUrl = '/images/hutopymedia/banners/hutopyul.png'
|
const fallbackUrl = '/images/hutopymedia/banners/hutopyul.png';
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('');
|
||||||
const showCropper = ref(false)
|
const showCropper = ref(false);
|
||||||
const cropper = ref(null)
|
const cropper = ref(null);
|
||||||
const isUploading = ref(false)
|
const isUploading = ref(false);
|
||||||
const uploadProgress = ref(0)
|
const uploadProgress = ref(0);
|
||||||
|
|
||||||
// Get translations for this component
|
// Get translations for this component
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
if (fileInput.value) {
|
if (fileInput.value) {
|
||||||
fileInput.value.value = '' // Reset the input value to ensure the change event fires
|
fileInput.value.value = ''; // Reset the input value to ensure the change event fires
|
||||||
fileInput.value.click()
|
fileInput.value.click();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onFileSelected = (event) => {
|
const onFileSelected = event => {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
selectedFile.value = file
|
selectedFile.value = file;
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = e => {
|
||||||
fileUrl.value = e.target.result
|
fileUrl.value = e.target.result;
|
||||||
showCropper.value = true
|
showCropper.value = true;
|
||||||
}
|
};
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
} else {
|
} else {
|
||||||
selectedFile.value = null
|
selectedFile.value = null;
|
||||||
fileUrl.value = null
|
fileUrl.value = null;
|
||||||
showCropper.value = false
|
showCropper.value = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
if (fileUrl.value && fileUrl.value.startsWith('data:')) {
|
if (fileUrl.value && fileUrl.value.startsWith('data:')) {
|
||||||
// Only try to load the image if it's a data URL (newly selected image)
|
// Only try to load the image if it's a data URL (newly selected image)
|
||||||
const blob = dataURLtoBlob(fileUrl.value)
|
const blob = dataURLtoBlob(fileUrl.value);
|
||||||
selectedFile.value = new File([blob], 'current-image.jpg', { type: 'image/jpeg' })
|
selectedFile.value = new File([blob], 'current-image.jpg', { type: 'image/jpeg' });
|
||||||
showCropper.value = true
|
showCropper.value = true;
|
||||||
} else {
|
} else {
|
||||||
// If no image is selected, using fallback, or have an existing uploaded image, trigger the file input
|
// If no image is selected, using fallback, or have an existing uploaded image, trigger the file input
|
||||||
triggerFileInput()
|
triggerFileInput();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function to convert data URL to blob
|
// Helper function to convert data URL to blob
|
||||||
const dataURLtoBlob = (dataURL) => {
|
const dataURLtoBlob = dataURL => {
|
||||||
const arr = dataURL.split(',')
|
const arr = dataURL.split(',');
|
||||||
const mime = arr[0].match(/:(.*?);/)[1]
|
const mime = arr[0].match(/:(.*?);/)[1];
|
||||||
const bstr = atob(arr[1])
|
const bstr = atob(arr[1]);
|
||||||
let n = bstr.length
|
let n = bstr.length;
|
||||||
const u8arr = new Uint8Array(n)
|
const u8arr = new Uint8Array(n);
|
||||||
while (n--) {
|
while (n--) {
|
||||||
u8arr[n] = bstr.charCodeAt(n)
|
u8arr[n] = bstr.charCodeAt(n);
|
||||||
}
|
|
||||||
return new Blob([u8arr], { type: mime })
|
|
||||||
}
|
}
|
||||||
|
return new Blob([u8arr], { type: mime });
|
||||||
|
};
|
||||||
|
|
||||||
const applyCrop = () => {
|
const applyCrop = () => {
|
||||||
if (!cropper.value) return
|
if (!cropper.value) return;
|
||||||
|
|
||||||
const canvas = cropper.value.getResult().canvas
|
const canvas = cropper.value.getResult().canvas;
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob(blob => {
|
||||||
const croppedFile = new File([blob], selectedFile.value.name, {
|
const croppedFile = new File([blob], selectedFile.value.name, {
|
||||||
type: selectedFile.value.type
|
type: selectedFile.value.type,
|
||||||
})
|
});
|
||||||
selectedFile.value = croppedFile
|
selectedFile.value = croppedFile;
|
||||||
fileUrl.value = canvas.toDataURL()
|
fileUrl.value = canvas.toDataURL();
|
||||||
showCropper.value = false
|
showCropper.value = false;
|
||||||
}, selectedFile.value.type)
|
}, selectedFile.value.type);
|
||||||
}
|
};
|
||||||
|
|
||||||
const client = useClient()
|
const client = useClient();
|
||||||
const publish = async () => {
|
const publish = async () => {
|
||||||
if (!selectedFile.value || isUploading.value) return
|
if (!selectedFile.value || isUploading.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isUploading.value = true
|
isUploading.value = true;
|
||||||
uploadProgress.value = 0
|
uploadProgress.value = 0;
|
||||||
const formData = new FormData()
|
const formData = new FormData();
|
||||||
formData.append('file', selectedFile.value)
|
formData.append('file', selectedFile.value);
|
||||||
|
|
||||||
const response = await client.post(
|
const response = await client.post(`/api/creators/${props.creator.id}/banner`, formData, {
|
||||||
`/api/creators/${props.creator.id}/banner`,
|
onUploadProgress: progressEvent => {
|
||||||
formData,
|
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
{
|
},
|
||||||
onUploadProgress: (progressEvent) => {
|
});
|
||||||
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
props.creator.bannerUrl = `${response.data.blobUrl}?t=${Date.now()}`
|
props.creator.bannerUrl = `${response.data.blobUrl}?t=${Date.now()}`;
|
||||||
fileUrl.value = props.creator.bannerUrl
|
fileUrl.value = props.creator.bannerUrl;
|
||||||
emits('closeRequested')
|
emits('closeRequested');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
errorMessage.value = t('errors.imageUpload')
|
errorMessage.value = t('errors.imageUpload');
|
||||||
} finally {
|
} finally {
|
||||||
isUploading.value = false
|
isUploading.value = false;
|
||||||
uploadProgress.value = 0
|
uploadProgress.value = 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
showCropper.value = false
|
showCropper.value = false;
|
||||||
// Reset to original state if we were editing
|
// Reset to original state if we were editing
|
||||||
if (props.creator?.bannerUrl) {
|
if (props.creator?.bannerUrl) {
|
||||||
fileUrl.value = props.creator.bannerUrl
|
fileUrl.value = props.creator.bannerUrl;
|
||||||
selectedFile.value = null
|
selectedFile.value = null;
|
||||||
} else {
|
} else {
|
||||||
fileUrl.value = fallbackUrl
|
fileUrl.value = fallbackUrl;
|
||||||
selectedFile.value = null
|
selectedFile.value = null;
|
||||||
}
|
|
||||||
emits('closeRequested')
|
|
||||||
}
|
}
|
||||||
|
emits('closeRequested');
|
||||||
|
};
|
||||||
|
|
||||||
// Add drop handler
|
// Add drop handler
|
||||||
const handleDrop = (event) => {
|
const handleDrop = event => {
|
||||||
const file = event.dataTransfer.files[0]
|
const file = event.dataTransfer.files[0];
|
||||||
if (file && file.type.startsWith('image/')) {
|
if (file && file.type.startsWith('image/')) {
|
||||||
selectedFile.value = file
|
selectedFile.value = file;
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = e => {
|
||||||
fileUrl.value = e.target.result
|
fileUrl.value = e.target.result;
|
||||||
showCropper.value = true
|
showCropper.value = true;
|
||||||
}
|
};
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -363,13 +372,6 @@ const handleDrop = (event) => {
|
|||||||
"chooseImage": "Choisir une image",
|
"chooseImage": "Choisir une image",
|
||||||
"clickToEdit": "Cliquez pour modifier",
|
"clickToEdit": "Cliquez pour modifier",
|
||||||
"uploading": "Téléchargement"
|
"uploading": "Téléchargement"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Editor de banner",
|
|
||||||
"description": "Sube o edita tu imagen de banner de perfil. El tamaño recomendado es de 1024x256 píxeles (ratio 4:1).",
|
|
||||||
"chooseImage": "Elegir una imagen",
|
|
||||||
"clickToEdit": "Haga clic para editar",
|
|
||||||
"uploading": "Subiendo"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, ref} from 'vue'
|
import { computed, ref } from 'vue';
|
||||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
import { useUserProfileStore } from '@/stores/userProfileStore.js';
|
||||||
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
|
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
||||||
import {useClient} from "@/plugins/api.js";
|
import { useClient } from '@/plugins/api.js';
|
||||||
import {useRouter, useRoute} from "vue-router";
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import NameEditor from "@/views/creators/NameEditor.vue";
|
import NameEditor from '@/views/creators/NameEditor.vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const creatorName = ref('');
|
const creatorName = ref('');
|
||||||
const creatorNameReservationId = ref(undefined);
|
const creatorNameReservationId = ref(undefined);
|
||||||
const canSave = computed(() => creatorNameReservationId.value !== undefined)
|
const canSave = computed(() => creatorNameReservationId.value !== undefined);
|
||||||
|
|
||||||
const isOperationPending = ref(false);
|
const isOperationPending = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
@@ -21,19 +21,19 @@ const userProfileStore = useUserProfileStore();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
function handleCreatorNameReservationIdChanged($event) {
|
function handleCreatorNameReservationIdChanged($event) {
|
||||||
creatorNameReservationId.value = $event
|
creatorNameReservationId.value = $event;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
// if a returnUrl query‑string was supplied, prefer it
|
// if a returnUrl query‑string was supplied, prefer it
|
||||||
const returnUrl = route.query.returnUrl
|
const returnUrl = route.query.returnUrl;
|
||||||
if (typeof returnUrl === 'string' && returnUrl.length) {
|
if (typeof returnUrl === 'string' && returnUrl.length) {
|
||||||
router.push(returnUrl)
|
router.push(returnUrl);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise just go back one step in history
|
// otherwise just go back one step in history
|
||||||
router.back()
|
router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: The `fetchCreatorProfile` function should be private (push-up to the store)!
|
// TODO: The `fetchCreatorProfile` function should be private (push-up to the store)!
|
||||||
@@ -58,13 +58,11 @@ async function createAccount() {
|
|||||||
isOperationPending.value = false;
|
isOperationPending.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
{{ t('title') }}
|
{{ t('title') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -80,17 +78,18 @@ async function createAccount() {
|
|||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button
|
<button
|
||||||
class="secondary"
|
class="secondary"
|
||||||
@click="cancel">
|
@click="cancel"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="primary"
|
|
||||||
:disabled="!canSave || isOperationPending"
|
:disabled="!canSave || isOperationPending"
|
||||||
@click="createAccount">
|
class="primary"
|
||||||
|
@click="createAccount"
|
||||||
|
>
|
||||||
{{ t('create') }}
|
{{ t('create') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,17 +100,13 @@ async function createAccount() {
|
|||||||
>
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@apply min-h-screen w-full;
|
@apply min-h-screen w-full;
|
||||||
@apply flex items-center justify-center;
|
@apply flex items-center justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
@@ -131,14 +126,6 @@ async function createAccount() {
|
|||||||
"errors": {
|
"errors": {
|
||||||
"unexpected": "Une erreur inattendue s'est produite"
|
"unexpected": "Une erreur inattendue s'est produite"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Crea tu Hutopy",
|
|
||||||
"cancel": "Cancelar",
|
|
||||||
"create": "Crear mi página",
|
|
||||||
"errors": {
|
|
||||||
"unexpected": "Se produjo un error inesperado"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="creator-home">
|
<div class="creator-home">
|
||||||
|
|
||||||
<!-- Content sections container -->
|
<!-- Content sections container -->
|
||||||
<div class="content-sections">
|
<div class="content-sections">
|
||||||
|
|
||||||
<!-- Donation Section -->
|
<!-- Donation Section -->
|
||||||
<div v-if="brandingStore.value?.acceptDonation" class="section sm:hidden">
|
<div
|
||||||
<DonationButton :creator-id="brandingStore.value?.id" :creator-name="brandingStore.value?.name"
|
v-if="brandingStore.value?.acceptDonation"
|
||||||
|
class="section sm:hidden"
|
||||||
|
>
|
||||||
|
<DonationButton
|
||||||
|
:creator-id="brandingStore.value?.id"
|
||||||
|
:creator-name="brandingStore.value?.name"
|
||||||
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
|
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
|
||||||
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id" />
|
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- About Creator Section -->
|
<!-- About Creator Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<AboutCreator />
|
<AboutCreator />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import AboutCreator from './AboutCreator.vue';
|
import AboutCreator from './AboutCreator.vue';
|
||||||
import DonationButton from "@/views/creators/DonationButton.vue";
|
import DonationButton from '@/views/creators/DonationButton.vue';
|
||||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||||
|
|
||||||
const brandingStore = useBrandingStore();
|
const brandingStore = useBrandingStore();
|
||||||
const baseURL = window.location.origin;
|
const baseURL = window.location.origin;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -52,13 +54,16 @@ const baseURL = window.location.origin;
|
|||||||
@apply absolute inset-0;
|
@apply absolute inset-0;
|
||||||
@apply rounded-2xl;
|
@apply rounded-2xl;
|
||||||
@apply p-[1px];
|
@apply p-[1px];
|
||||||
background: linear-gradient(135deg, rgba(64, 64, 64, 1) 0%, rgba(64, 64, 64, 0) 20%, rgba(64, 64, 64, 0.5) 100%);
|
background: linear-gradient(
|
||||||
mask: linear-gradient(#fff 0 0) content-box,
|
135deg,
|
||||||
|
rgba(64, 64, 64, 1) 0%,
|
||||||
|
rgba(64, 64, 64, 0) 20%,
|
||||||
|
rgba(64, 64, 64, 0.5) 100%
|
||||||
|
);
|
||||||
|
mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
linear-gradient(#fff 0 0);
|
linear-gradient(#fff 0 0);
|
||||||
mask-composite: exclude;
|
mask-composite: exclude;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
|
||||||
</i18n>
|
|
||||||
|
|||||||
@@ -141,26 +141,6 @@
|
|||||||
"goHome": "Aller à l'accueil"
|
"goHome": "Aller à l'accueil"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"creator": {
|
|
||||||
"layout": {
|
|
||||||
"deletion": {
|
|
||||||
"pending": "Esta página de creador está pendiente de eliminación"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notFound": {
|
|
||||||
"title": "Creador No Encontrado",
|
|
||||||
"message": "El creador '{creator}' no existe o puede haber sido eliminado.",
|
|
||||||
"goHome": "Ir al Inicio",
|
|
||||||
"goBack": "Volver"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"title": "Algo Salió Mal",
|
|
||||||
"message": "Tenemos problemas para cargar esta página de creador. Por favor, inténtalo de nuevo más tarde.",
|
|
||||||
"goHome": "Ir al Inicio"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,38 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative" @mouseenter="showTint = isCurrentCreator" @mouseleave="showTint = false"
|
<div
|
||||||
@click="isCurrentCreator && openBannerEditor()">
|
class="relative"
|
||||||
|
@click="isCurrentCreator && openBannerEditor()"
|
||||||
|
@mouseenter="showTint = isCurrentCreator"
|
||||||
|
@mouseleave="showTint = false"
|
||||||
|
>
|
||||||
<div class="size-[110px] rounded-full border-4 border-hPrimary">
|
<div class="size-[110px] rounded-full border-4 border-hPrimary">
|
||||||
<img :src="brandingStore.value?.portraitUrl ?? '/images/placeholders/profile.png'" :alt="t('logoAlt')"
|
<img
|
||||||
width="110px" height="110px" class="rounded-full" />
|
:alt="t('logoAlt')"
|
||||||
|
:src="brandingStore.value?.portraitUrl ?? '/images/placeholders/profile.png'"
|
||||||
|
class="rounded-full"
|
||||||
|
height="110px"
|
||||||
|
width="110px"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tint Effect -->
|
<!-- Tint Effect -->
|
||||||
<div v-if="showTint" class="absolute inset-0 cursor-pointer rounded-full bg-black/25" :title="t('editLogo')">
|
<div
|
||||||
|
v-if="showTint"
|
||||||
|
:title="t('editLogo')"
|
||||||
|
class="absolute inset-0 cursor-pointer rounded-full bg-black/25"
|
||||||
|
>
|
||||||
<!-- Top-right Icon -->
|
<!-- Top-right Icon -->
|
||||||
<div
|
<div
|
||||||
class="absolute right-0 top-0 flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg">
|
class="absolute right-0 top-0 flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg"
|
||||||
<v-icon large :icon="mdiPencil" />
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="mdiPencil"
|
||||||
|
large
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<v-dialog
|
||||||
|
v-model="isDialogOpen"
|
||||||
<v-dialog v-model="isDialogOpen" max-width="800px">
|
max-width="800px"
|
||||||
|
>
|
||||||
<template #default="{ close }">
|
<template #default="{ close }">
|
||||||
<creator-logo-editor :creator="brandingStore?.value"
|
<creator-logo-editor
|
||||||
@closeRequested="() => isDialogOpen = false"></creator-logo-editor>
|
:creator="brandingStore?.value"
|
||||||
|
@closeRequested="() => (isDialogOpen = false)"
|
||||||
|
></creator-logo-editor>
|
||||||
</template>
|
</template>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAuthStore } from "@/stores/authStore.js";
|
import { useAuthStore } from '@/stores/authStore.js';
|
||||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||||
import CreatorLogoEditor from "@/views/creators/CreatorLogoEditor.vue";
|
import CreatorLogoEditor from '@/views/creators/CreatorLogoEditor.vue';
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n';
|
||||||
import { mdiPencil } from '@mdi/js';
|
import { mdiPencil } from '@mdi/js';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -51,7 +70,6 @@ const openBannerEditor = () => {
|
|||||||
const isCurrentCreator = computed(() => {
|
const isCurrentCreator = computed(() => {
|
||||||
return authStore.userId === brandingStore.value.id;
|
return authStore.userId === brandingStore.value.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -69,10 +87,6 @@ const isCurrentCreator = computed(() => {
|
|||||||
"fr": {
|
"fr": {
|
||||||
"logoAlt": "Logo du créateur",
|
"logoAlt": "Logo du créateur",
|
||||||
"editLogo": "Modifier le logo"
|
"editLogo": "Modifier le logo"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"logoAlt": "Logo del creador",
|
|
||||||
"editLogo": "Editar logo"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
<div class="file-input-container">
|
<div class="file-input-container">
|
||||||
<input
|
<input
|
||||||
type="file"
|
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
|
type="file"
|
||||||
@change="onFileSelected"
|
@change="onFileSelected"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -25,31 +25,40 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errorMessage" class="error-message">
|
<div
|
||||||
|
v-if="errorMessage"
|
||||||
|
class="error-message"
|
||||||
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showCropper" class="cropper-wrapper">
|
<div
|
||||||
|
v-if="showCropper"
|
||||||
|
class="cropper-wrapper"
|
||||||
|
>
|
||||||
<Cropper
|
<Cropper
|
||||||
ref="cropper"
|
ref="cropper"
|
||||||
:src="fileUrl"
|
|
||||||
:aspect-ratio="1"
|
:aspect-ratio="1"
|
||||||
|
:src="fileUrl"
|
||||||
:stencil-component="CircleStencil"
|
:stencil-component="CircleStencil"
|
||||||
:stencil-props="{
|
:stencil-props="{
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
class: 'circle-stencil'
|
class: 'circle-stencil',
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="image-preview-container"
|
<div
|
||||||
|
v-else
|
||||||
|
class="image-preview-container"
|
||||||
@click="startEditing"
|
@click="startEditing"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.prevent="handleDrop">
|
@drop.prevent="handleDrop"
|
||||||
|
>
|
||||||
<div class="circular-preview">
|
<div class="circular-preview">
|
||||||
<img
|
<img
|
||||||
:src="fileUrl || fallbackUrl"
|
|
||||||
:alt="t('preview')"
|
:alt="t('preview')"
|
||||||
|
:src="fileUrl || fallbackUrl"
|
||||||
class="preview-image"
|
class="preview-image"
|
||||||
/>
|
/>
|
||||||
<div class="edit-overlay">
|
<div class="edit-overlay">
|
||||||
@@ -60,14 +69,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
|
:disabled="isUploading"
|
||||||
|
class="secondary"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
:disabled="isUploading">
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
|
:disabled="!selectedFile || isUploading"
|
||||||
|
class="primary"
|
||||||
@click="showCropper ? applyCrop() : publish()"
|
@click="showCropper ? applyCrop() : publish()"
|
||||||
:disabled="!selectedFile || isUploading">
|
>
|
||||||
<template v-if="isUploading">
|
<template v-if="isUploading">
|
||||||
<span class="loading-spinner"></span>
|
<span class="loading-spinner"></span>
|
||||||
{{ t('uploading') }} ({{ uploadProgress }}%)
|
{{ t('uploading') }} ({{ uploadProgress }}%)
|
||||||
@@ -81,158 +94,154 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue'
|
import { ref } from 'vue';
|
||||||
import {useClient} from '@/plugins/api.js'
|
import { useClient } from '@/plugins/api.js';
|
||||||
import { Cropper, CircleStencil } from 'vue-advanced-cropper'
|
import { CircleStencil, Cropper } from 'vue-advanced-cropper';
|
||||||
import 'vue-advanced-cropper/dist/style.css'
|
import 'vue-advanced-cropper/dist/style.css';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['closeRequested'])
|
const emits = defineEmits(['closeRequested']);
|
||||||
|
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null);
|
||||||
const selectedFile = ref(null)
|
const selectedFile = ref(null);
|
||||||
const fileUrl = ref(props.creator.portraitUrl)
|
const fileUrl = ref(props.creator.portraitUrl);
|
||||||
const fallbackUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png'
|
const fallbackUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png';
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('');
|
||||||
const showCropper = ref(false)
|
const showCropper = ref(false);
|
||||||
const cropper = ref(null)
|
const cropper = ref(null);
|
||||||
const isUploading = ref(false)
|
const isUploading = ref(false);
|
||||||
const uploadProgress = ref(0)
|
const uploadProgress = ref(0);
|
||||||
|
|
||||||
const TARGET_WIDTH = 200
|
const TARGET_WIDTH = 200;
|
||||||
const TARGET_HEIGHT = 200
|
const TARGET_HEIGHT = 200;
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
if (fileInput.value) {
|
if (fileInput.value) {
|
||||||
fileInput.value.value = '' // Reset the input value to ensure the change event fires
|
fileInput.value.value = ''; // Reset the input value to ensure the change event fires
|
||||||
fileInput.value.click()
|
fileInput.value.click();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onFileSelected = (event) => {
|
const onFileSelected = event => {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
selectedFile.value = file
|
selectedFile.value = file;
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = e => {
|
||||||
fileUrl.value = e.target.result
|
fileUrl.value = e.target.result;
|
||||||
showCropper.value = true
|
showCropper.value = true;
|
||||||
}
|
};
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
} else {
|
} else {
|
||||||
selectedFile.value = null
|
selectedFile.value = null;
|
||||||
fileUrl.value = null
|
fileUrl.value = null;
|
||||||
showCropper.value = false
|
showCropper.value = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
if (fileUrl.value && fileUrl.value.startsWith('data:')) {
|
if (fileUrl.value && fileUrl.value.startsWith('data:')) {
|
||||||
// Only try to load the image if it's a data URL (newly selected image)
|
// Only try to load the image if it's a data URL (newly selected image)
|
||||||
const blob = dataURLtoBlob(fileUrl.value)
|
const blob = dataURLtoBlob(fileUrl.value);
|
||||||
selectedFile.value = new File([blob], 'current-image.jpg', { type: 'image/jpeg' })
|
selectedFile.value = new File([blob], 'current-image.jpg', { type: 'image/jpeg' });
|
||||||
showCropper.value = true
|
showCropper.value = true;
|
||||||
} else {
|
} else {
|
||||||
// If no image is selected, using fallback, or have an existing uploaded image, trigger the file input
|
// If no image is selected, using fallback, or have an existing uploaded image, trigger the file input
|
||||||
triggerFileInput()
|
triggerFileInput();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function to convert data URL to blob
|
// Helper function to convert data URL to blob
|
||||||
const dataURLtoBlob = (dataURL) => {
|
const dataURLtoBlob = dataURL => {
|
||||||
const arr = dataURL.split(',')
|
const arr = dataURL.split(',');
|
||||||
const mime = arr[0].match(/:(.*?);/)[1]
|
const mime = arr[0].match(/:(.*?);/)[1];
|
||||||
const bstr = atob(arr[1])
|
const bstr = atob(arr[1]);
|
||||||
let n = bstr.length
|
let n = bstr.length;
|
||||||
const u8arr = new Uint8Array(n)
|
const u8arr = new Uint8Array(n);
|
||||||
while (n--) {
|
while (n--) {
|
||||||
u8arr[n] = bstr.charCodeAt(n)
|
u8arr[n] = bstr.charCodeAt(n);
|
||||||
}
|
|
||||||
return new Blob([u8arr], { type: mime })
|
|
||||||
}
|
}
|
||||||
|
return new Blob([u8arr], { type: mime });
|
||||||
|
};
|
||||||
|
|
||||||
const applyCrop = () => {
|
const applyCrop = () => {
|
||||||
if (!cropper.value) return
|
if (!cropper.value) return;
|
||||||
|
|
||||||
const canvas = cropper.value.getResult().canvas
|
const canvas = cropper.value.getResult().canvas;
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob(blob => {
|
||||||
const croppedFile = new File([blob], selectedFile.value.name, {
|
const croppedFile = new File([blob], selectedFile.value.name, {
|
||||||
type: selectedFile.value.type
|
type: selectedFile.value.type,
|
||||||
})
|
});
|
||||||
selectedFile.value = croppedFile
|
selectedFile.value = croppedFile;
|
||||||
fileUrl.value = canvas.toDataURL()
|
fileUrl.value = canvas.toDataURL();
|
||||||
showCropper.value = false
|
showCropper.value = false;
|
||||||
}, selectedFile.value.type)
|
}, selectedFile.value.type);
|
||||||
}
|
};
|
||||||
|
|
||||||
const client = useClient()
|
const client = useClient();
|
||||||
const publish = async () => {
|
const publish = async () => {
|
||||||
if (!selectedFile.value || isUploading.value) return
|
if (!selectedFile.value || isUploading.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isUploading.value = true
|
isUploading.value = true;
|
||||||
uploadProgress.value = 0
|
uploadProgress.value = 0;
|
||||||
const formData = new FormData()
|
const formData = new FormData();
|
||||||
formData.append('file', selectedFile.value)
|
formData.append('file', selectedFile.value);
|
||||||
|
|
||||||
const response = await client.post(
|
const response = await client.post(`/api/creators/${props.creator.id}/logo`, formData, {
|
||||||
`/api/creators/${props.creator.id}/logo`,
|
onUploadProgress: progressEvent => {
|
||||||
formData,
|
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
{
|
},
|
||||||
onUploadProgress: (progressEvent) => {
|
});
|
||||||
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
props.creator.portraitUrl = `${response.data.blobUrl}?t=${Date.now()}`
|
props.creator.portraitUrl = `${response.data.blobUrl}?t=${Date.now()}`;
|
||||||
if (props.creator.portraitUrl) {
|
if (props.creator.portraitUrl) {
|
||||||
fileUrl.value = props.creator.portraitUrl
|
fileUrl.value = props.creator.portraitUrl;
|
||||||
}
|
}
|
||||||
emits('closeRequested')
|
emits('closeRequested');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
errorMessage.value = t('errors.imageUpload')
|
errorMessage.value = t('errors.imageUpload');
|
||||||
} finally {
|
} finally {
|
||||||
isUploading.value = false
|
isUploading.value = false;
|
||||||
uploadProgress.value = 0
|
uploadProgress.value = 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
showCropper.value = false
|
showCropper.value = false;
|
||||||
// Reset to original state if we were editing
|
// Reset to original state if we were editing
|
||||||
if (props.creator.portraitUrl) {
|
if (props.creator.portraitUrl) {
|
||||||
fileUrl.value = props.creator.portraitUrl
|
fileUrl.value = props.creator.portraitUrl;
|
||||||
selectedFile.value = null
|
selectedFile.value = null;
|
||||||
} else {
|
} else {
|
||||||
fileUrl.value = fallbackUrl
|
fileUrl.value = fallbackUrl;
|
||||||
selectedFile.value = null
|
selectedFile.value = null;
|
||||||
}
|
|
||||||
emits('closeRequested')
|
|
||||||
}
|
}
|
||||||
|
emits('closeRequested');
|
||||||
|
};
|
||||||
|
|
||||||
// Add drop handler
|
// Add drop handler
|
||||||
const handleDrop = (event) => {
|
const handleDrop = event => {
|
||||||
const file = event.dataTransfer.files[0]
|
const file = event.dataTransfer.files[0];
|
||||||
if (file && file.type.startsWith('image/')) {
|
if (file && file.type.startsWith('image/')) {
|
||||||
selectedFile.value = file
|
selectedFile.value = file;
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = e => {
|
||||||
fileUrl.value = e.target.result
|
fileUrl.value = e.target.result;
|
||||||
showCropper.value = true
|
showCropper.value = true;
|
||||||
}
|
};
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -384,14 +393,6 @@ const handleDrop = (event) => {
|
|||||||
"chooseImage": "Choisir une image",
|
"chooseImage": "Choisir une image",
|
||||||
"clickToEdit": "Cliquez pour modifier",
|
"clickToEdit": "Cliquez pour modifier",
|
||||||
"uploading": "Téléchargement"
|
"uploading": "Téléchargement"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"logoTitle": "Editar logo",
|
|
||||||
"logoDescription": "Elige una imagen de logo para tu página de creador. La imagen se recortará en círculo.",
|
|
||||||
"chooseImage": "Elegir imagen",
|
|
||||||
"clickToEdit": "Haz clic para editar",
|
|
||||||
"uploading": "Subiendo"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="secondary donation-action"
|
class="secondary donation-action"
|
||||||
@click="openDonationDialog()"
|
@click="openDonationDialog()"
|
||||||
@@ -11,12 +10,11 @@
|
|||||||
ref="donationDialogRef"
|
ref="donationDialogRef"
|
||||||
:creator-id="creatorId"
|
:creator-id="creatorId"
|
||||||
:creator-name="creatorName"
|
:creator-name="creatorName"
|
||||||
:on-success-url="onSuccessUrl"
|
|
||||||
:on-cancelled-url="onCancelledUrl"
|
|
||||||
:icon-color-class="iconColorClass"
|
:icon-color-class="iconColorClass"
|
||||||
|
:on-cancelled-url="onCancelledUrl"
|
||||||
|
:on-success-url="onSuccessUrl"
|
||||||
@close="handleDialogClose"
|
@close="handleDialogClose"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -71,13 +69,6 @@ function handleDialogClose() {
|
|||||||
"isupport": "Je Soutiens"
|
"isupport": "Je Soutiens"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"creator": {
|
|
||||||
"donation": {
|
|
||||||
"isupport": "Apoyo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -183,24 +183,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"common": {
|
|
||||||
"cancel": "Cancelar"
|
|
||||||
},
|
|
||||||
"creator": {
|
|
||||||
"donation": {
|
|
||||||
"isupport": "Apoyo",
|
|
||||||
"amount": "Cantidad ($)",
|
|
||||||
"message": "Mensaje (opcional)",
|
|
||||||
"send": "Enviar",
|
|
||||||
"processing": "Procesando...",
|
|
||||||
"errors": {
|
|
||||||
"payment": "Ocurrió un error durante el procesamiento del pago",
|
|
||||||
"invalidAmount": "Por favor ingrese un monto válido"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -1,28 +1,25 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
import { computed, onMounted, onUnmounted, ref } 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';
|
import config from '@/config';
|
||||||
import { mdiCheckCircle, mdiCloseCircle } from '@mdi/js';
|
import { mdiCheckCircle, mdiCloseCircle } from '@mdi/js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: {
|
name: {
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
creatorNameReservationId: {
|
creatorNameReservationId: {
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
originalSlug: {
|
originalSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits([
|
const emits = defineEmits(['update:name', 'update:creatorNameReservationId']);
|
||||||
'update:name',
|
|
||||||
'update:creatorNameReservationId'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const name = ref(props.name);
|
const name = ref(props.name);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -43,7 +40,7 @@ const isCurrentSlug = computed(() => {
|
|||||||
const baseUrl = computed(() => `${config.baseUrl}/@`);
|
const baseUrl = computed(() => `${config.baseUrl}/@`);
|
||||||
|
|
||||||
// Validation function for the slug
|
// Validation function for the slug
|
||||||
const validateSlug = (slug) => {
|
const validateSlug = slug => {
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
validationError.value = t('creator.name.errors.required');
|
validationError.value = t('creator.name.errors.required');
|
||||||
return false;
|
return false;
|
||||||
@@ -68,7 +65,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
// If the name is the same as the original slug, set the reservation state to "reserved"
|
// If the name is the same as the original slug, set the reservation state to "reserved"
|
||||||
if (isCurrentSlug.value) {
|
if (isCurrentSlug.value) {
|
||||||
reservationState.value = "reserved";
|
reservationState.value = 'reserved';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,13 +91,13 @@ const handleInput = () => {
|
|||||||
|
|
||||||
// Validate the slug
|
// Validate the slug
|
||||||
if (!validateSlug(currentName)) {
|
if (!validateSlug(currentName)) {
|
||||||
reservationState.value = "unavailable";
|
reservationState.value = 'unavailable';
|
||||||
return;
|
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';
|
||||||
lastProcessedName = currentName;
|
lastProcessedName = currentName;
|
||||||
emits('update:name', currentName);
|
emits('update:name', currentName);
|
||||||
return;
|
return;
|
||||||
@@ -111,8 +108,8 @@ const handleInput = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const checkNameAvailability = async (nameToCheck) => {
|
const checkNameAvailability = async nameToCheck => {
|
||||||
if (!nameToCheck || nameToCheck.trim() === "") {
|
if (!nameToCheck || nameToCheck.trim() === '') {
|
||||||
reservationState.value = null;
|
reservationState.value = null;
|
||||||
lastProcessedName = nameToCheck;
|
lastProcessedName = nameToCheck;
|
||||||
return;
|
return;
|
||||||
@@ -123,7 +120,7 @@ const checkNameAvailability = async (nameToCheck) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isOperationPending.value = true;
|
isOperationPending.value = true;
|
||||||
reservationState.value = "loading";
|
reservationState.value = 'loading';
|
||||||
|
|
||||||
// Create a new request with cancellation token
|
// Create a new request with cancellation token
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -137,14 +134,14 @@ const checkNameAvailability = async (nameToCheck) => {
|
|||||||
|
|
||||||
// Only process the response if this is still the current request
|
// Only process the response if this is still the current request
|
||||||
if (currentController === controller) {
|
if (currentController === controller) {
|
||||||
reservationState.value = "reserved";
|
reservationState.value = 'reserved';
|
||||||
lastProcessedName = nameToCheck;
|
lastProcessedName = nameToCheck;
|
||||||
emits('update:name', nameToCheck);
|
emits('update:name', nameToCheck);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Only process the error if this is still the current request and it's not an abort error
|
// Only process the error if this is still the current request and it's not an abort error
|
||||||
if (currentController && error.name !== 'AbortError') {
|
if (currentController && error.name !== 'AbortError') {
|
||||||
reservationState.value = "unavailable";
|
reservationState.value = 'unavailable';
|
||||||
lastProcessedName = nameToCheck;
|
lastProcessedName = nameToCheck;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -159,22 +156,39 @@ onUnmounted(() => {
|
|||||||
cancelCurrentRequest();
|
cancelCurrentRequest();
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-text-field variant="outlined" :label="t('creator.name.label')" v-model="name" @input="handleInput"
|
<v-text-field
|
||||||
:error-messages="validationError">
|
v-model="name"
|
||||||
|
:error-messages="validationError"
|
||||||
|
:label="t('creator.name.label')"
|
||||||
|
variant="outlined"
|
||||||
|
@input="handleInput"
|
||||||
|
>
|
||||||
<template #prepend-inner>
|
<template #prepend-inner>
|
||||||
<span class="text-nowrap font-sans text-gray-400">{{ baseUrl }}</span>
|
<span class="text-nowrap font-sans text-gray-400">{{ baseUrl }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #append-inner>
|
<template #append-inner>
|
||||||
<v-progress-circular v-if="reservationState === 'loading'" indeterminate size="24" width="3"
|
<v-progress-circular
|
||||||
color="grey"></v-progress-circular>
|
v-if="reservationState === 'loading'"
|
||||||
|
color="grey"
|
||||||
|
indeterminate
|
||||||
|
size="24"
|
||||||
|
width="3"
|
||||||
|
></v-progress-circular>
|
||||||
|
|
||||||
<v-icon v-else-if="reservationState === 'reserved'" color="green" :icon="mdiCheckCircle" />
|
<v-icon
|
||||||
<v-icon v-else-if="reservationState === 'unavailable'" color="red" :icon="mdiCloseCircle" />
|
v-else-if="reservationState === 'reserved'"
|
||||||
|
:icon="mdiCheckCircle"
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
<v-icon
|
||||||
|
v-else-if="reservationState === 'unavailable'"
|
||||||
|
:icon="mdiCloseCircle"
|
||||||
|
color="red"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</template>
|
</template>
|
||||||
@@ -204,17 +218,6 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"creator": {
|
|
||||||
"name": {
|
|
||||||
"label": "Tu identificador de creador",
|
|
||||||
"errors": {
|
|
||||||
"required": "El identificador es obligatorio",
|
|
||||||
"invalid": "Solo se permiten letras, números y guiones"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
<span class="capitalize text-3xl">
|
<span class="capitalize text-3xl">
|
||||||
{{ brandingStore.value.name }}
|
{{ brandingStore.value.name }}
|
||||||
</span>
|
</span>
|
||||||
<div v-show="brandingStore.value.verified"
|
<div
|
||||||
|
v-show="brandingStore.value.verified"
|
||||||
|
:title="t('verified')"
|
||||||
class="text-blue mt-1"
|
class="text-blue mt-1"
|
||||||
:title="t('verified')">
|
>
|
||||||
<icon-account-verified></icon-account-verified>
|
<icon-account-verified></icon-account-verified>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,8 +19,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import IconAccountVerified from "@/components/icons/IconAccountVerified.vue";
|
import IconAccountVerified from '@/components/icons/IconAccountVerified.vue';
|
||||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const brandingStore = useBrandingStore();
|
const brandingStore = useBrandingStore();
|
||||||
@@ -32,9 +34,6 @@ const { t } = useI18n();
|
|||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"verified": "Compte vérifié"
|
"verified": "Compte vérifié"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"verified": "Cuenta verificada"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -75,13 +75,6 @@
|
|||||||
"usernameDefault": "Le créateur",
|
"usernameDefault": "Le créateur",
|
||||||
"receipt": "Un reçu a été envoyé à votre email.",
|
"receipt": "Un reçu a été envoyé à votre email.",
|
||||||
"returnToCreator": "Retourner à la page du créateur"
|
"returnToCreator": "Retourner à la page du créateur"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "¡{creatorName} te agradece!",
|
|
||||||
"message": "Su pago ha sido procesado con éxito.",
|
|
||||||
"usernameDefault": "El creador",
|
|
||||||
"receipt": "Se ha enviado un recibo a su correo electrónico.",
|
|
||||||
"returnToCreator": "Volver a la página del creador"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -46,12 +46,6 @@
|
|||||||
"message": "Nous n'avons pas pu traiter votre paiement.",
|
"message": "Nous n'avons pas pu traiter votre paiement.",
|
||||||
"retry": "Réessayer",
|
"retry": "Réessayer",
|
||||||
"returnToCreator": "Retourner à la page du créateur"
|
"returnToCreator": "Retourner à la page du créateur"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Pago fallido",
|
|
||||||
"message": "No pudimos procesar su pago.",
|
|
||||||
"retry": "Intentar de nuevo",
|
|
||||||
"returnToCreator": "Volver a la página del creador"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,51 +1,76 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Instagram from "@/views/svg/Instagram.vue";
|
import Instagram from '@/views/svg/Instagram.vue';
|
||||||
import Facebook from "@/views/svg/Facebook.vue";
|
import Facebook from '@/views/svg/Facebook.vue';
|
||||||
import X from "@/views/svg/X.vue";
|
import X from '@/views/svg/X.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<footer class="flex flex-col gap-10 pt-7 pb-10">
|
<footer class="flex flex-col gap-10 pt-7 pb-10">
|
||||||
|
|
||||||
<div class="footer-socials">
|
<div class="footer-socials">
|
||||||
<a href="https://www.facebook.com/profile.php?id=61556819217561" target="_blank">
|
<a
|
||||||
|
href="https://www.facebook.com/profile.php?id=61556819217561"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<facebook class="social-icon"></facebook>
|
<facebook class="social-icon"></facebook>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.instagram.com/hutopy.inc/" target="_blank">
|
<a
|
||||||
|
href="https://www.instagram.com/hutopy.inc/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<instagram class="social-icon"></instagram>
|
<instagram class="social-icon"></instagram>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/Hutopyinc/" target="_blank">
|
<a
|
||||||
|
href="https://x.com/Hutopyinc/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<x class="social-icon"></x>
|
<x class="social-icon"></x>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<router-link to="/documents/helpandcontact"
|
<router-link
|
||||||
class="link">
|
class="link"
|
||||||
|
to="/documents/helpandcontact"
|
||||||
|
>
|
||||||
{{ t('footer.helpandcontact') }}
|
{{ t('footer.helpandcontact') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/documents/faq"
|
<router-link
|
||||||
class="link">
|
class="link"
|
||||||
|
to="/documents/faq"
|
||||||
|
>
|
||||||
{{ t('footer.faq') }}
|
{{ t('footer.faq') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/documents/termsandconditions"
|
<router-link
|
||||||
class="link">
|
class="link"
|
||||||
|
to="/documents/guideforcreators"
|
||||||
|
>
|
||||||
|
{{ t('footer.creatorguide') }}
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="link"
|
||||||
|
to="/documents/termsandconditions"
|
||||||
|
>
|
||||||
{{ t('footer.termsandconditions') }}
|
{{ t('footer.termsandconditions') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/documents/contentpolicy"
|
<router-link
|
||||||
class="link">
|
class="link"
|
||||||
|
to="/documents/contentpolicy"
|
||||||
|
>
|
||||||
{{ t('footer.contentpolicy') }}
|
{{ t('footer.contentpolicy') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/documents/about"
|
<router-link
|
||||||
class="link">
|
class="link"
|
||||||
|
to="/documents/about"
|
||||||
|
>
|
||||||
{{ t('footer.about') }}
|
{{ t('footer.about') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/documents/pricing"
|
<router-link
|
||||||
class="link">
|
class="link"
|
||||||
|
to="/documents/pricing"
|
||||||
|
>
|
||||||
{{ t('footer.pricing') }}
|
{{ t('footer.pricing') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,13 +78,10 @@ const { t } = useI18n();
|
|||||||
<div class="footer-copyright">
|
<div class="footer-copyright">
|
||||||
Hutopy ©{{ new Date().getFullYear() }} - {{ t('footer.allRightsReserved') }}
|
Hutopy ©{{ new Date().getFullYear() }} - {{ t('footer.allRightsReserved') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.footer-socials {
|
.footer-socials {
|
||||||
@apply flex flex-row justify-center;
|
@apply flex flex-row justify-center;
|
||||||
@apply gap-10;
|
@apply gap-10;
|
||||||
@@ -85,7 +107,6 @@ const { t } = useI18n();
|
|||||||
@apply tracking-widest font-sans text-sm;
|
@apply tracking-widest font-sans text-sm;
|
||||||
@apply hover:text-gray-400;
|
@apply hover:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
@@ -113,18 +134,6 @@ const { t } = useI18n();
|
|||||||
"pricing": "Tarifs",
|
"pricing": "Tarifs",
|
||||||
"allRightsReserved": "Tous Droits Réservés"
|
"allRightsReserved": "Tous Droits Réservés"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"footer": {
|
|
||||||
"helpandcontact": "Ayuda y Contacto",
|
|
||||||
"faq": "Preguntas Frecuentes",
|
|
||||||
"creatorguide": "Guía del Creador",
|
|
||||||
"termsandconditions": "Términos y Condiciones",
|
|
||||||
"contentpolicy": "Política de Contenido",
|
|
||||||
"about": "Acerca de",
|
|
||||||
"pricing": "Precios",
|
|
||||||
"allRightsReserved": "Todos los Derechos Reservados"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Footer from "@/views/main/Footer.vue";
|
import Footer from '@/views/main/Footer.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -7,50 +7,63 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="pa-4 flex flex-col justify-center md:flex-row">
|
<div class="pa-4 flex flex-col justify-center md:flex-row">
|
||||||
<div class="py-6">
|
<div class="py-6">
|
||||||
<div>
|
<div>
|
||||||
<img alt="Hutopy Logo" class="md:h-44 logo-image sm:h-28 sm:mx-auto"
|
<img
|
||||||
src="/images/hutopymedia/banners/hutopy.png">
|
alt="Hutopy Logo"
|
||||||
|
class="md:h-44 logo-image sm:h-28 sm:mx-auto"
|
||||||
|
src="/images/hutopymedia/banners/hutopy.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-3 header-btn">
|
<div class="flex flex-col space-y-3 header-btn">
|
||||||
<v-btn
|
<v-btn
|
||||||
class="text-white w-full sm:w-auto inscription-btn-header"
|
class="text-white w-full sm:w-auto inscription-btn-header"
|
||||||
to="/login">
|
to="/login"
|
||||||
|
>
|
||||||
{{ t('inscription') }}
|
{{ t('inscription') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="w-full sm:w-auto inscription-btn-header-outlined"
|
class="w-full sm:w-auto inscription-btn-header-outlined"
|
||||||
to="/create-creator"
|
to="/create-creator"
|
||||||
variant="outlined">
|
variant="outlined"
|
||||||
|
>
|
||||||
{{ t('createPage') }}
|
{{ t('createPage') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="support-container flex flex-col items-center space-y-4 md:flex-row md:space-y-0 md:space-x-6">
|
<div class="support-container flex flex-col items-center space-y-4 md:flex-row md:space-y-0 md:space-x-6">
|
||||||
<div class="support-text text-justify md:text-left">
|
<div class="support-text text-justify md:text-left">
|
||||||
<span class="text-white"> {{ t('slogan') }} </span><br>
|
<span class="text-white">{{ t('support') }}</span>
|
||||||
|
<br />
|
||||||
|
<span class="text-white">{{ t('creators') }}</span>
|
||||||
|
<br />
|
||||||
|
<span class="text-white">{{ t('projects') }}</span>
|
||||||
|
<br />
|
||||||
|
<span class="text-white">{{ t('love') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<img alt="YourHutopy" class="w-48 h-48 md:w-48 md:h-48 object-contain"
|
<img
|
||||||
src="/images/hutopymedia/banners/heart.png">
|
alt="YourHutopy"
|
||||||
|
class="w-48 h-48 md:w-48 md:h-48 object-contain"
|
||||||
|
src="/images/hutopymedia/banners/heart.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative mt-10">
|
<div class="relative mt-10">
|
||||||
|
<div
|
||||||
<div class="flex flex-col lg:flex-row justify-center items-center lg:space-x-14 space-y-6 lg:space-y-0 pa-1">
|
class="flex flex-col lg:flex-row justify-center items-center lg:space-x-14 space-y-6 lg:space-y-0 pa-1"
|
||||||
|
>
|
||||||
<div class="bg-hSurface p-4 max-w-md text-center rounded-3xl space-y-8 shadow-xl h-[520px]">
|
<div class="bg-hSurface p-4 max-w-md text-center rounded-3xl space-y-8 shadow-xl h-[520px]">
|
||||||
<div class="text-xl mb-2 box-text">{{ t('supportText') }}</div>
|
<div class="text-xl mb-2 box-text">{{ t('supportText') }}</div>
|
||||||
<img
|
<img
|
||||||
alt="YourHutopy"
|
alt="YourHutopy"
|
||||||
class="max-h-56 mx-auto"
|
class="max-h-56 mx-auto"
|
||||||
src="/images/hutopymedia/homepage/hands.png"
|
src="/images/hutopymedia/homepage/hands.png"
|
||||||
>
|
/>
|
||||||
<div class="text-md text-justify px-6">
|
<div class="text-md text-justify px-6">
|
||||||
{{ t('supportDescription') }}
|
{{ t('supportDescription') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -62,7 +75,7 @@ const { t } = useI18n();
|
|||||||
alt="YourHutopy"
|
alt="YourHutopy"
|
||||||
class="max-h-56 mx-auto"
|
class="max-h-56 mx-auto"
|
||||||
src="/images/hutopymedia/homepage/brain.png"
|
src="/images/hutopymedia/homepage/brain.png"
|
||||||
>
|
/>
|
||||||
<div class="text-md text-justify px-6">
|
<div class="text-md text-justify px-6">
|
||||||
{{ t('creatorDescription') }}
|
{{ t('creatorDescription') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -74,16 +87,21 @@ const { t } = useI18n();
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-5xl mx-auto px-6 py-8">
|
<div class="max-w-5xl mx-auto px-6 py-8">
|
||||||
<div class="gap-8 items-start flex flex-col md:flex-row">
|
<div class="gap-8 items-start flex flex-col md:flex-row">
|
||||||
<!-- Section de texte -->
|
<!-- Section de texte -->
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<img alt="YourHutopy" class="w-full mb-6" src="/images/hutopymedia/homepage/votrehutopy.png">
|
<img
|
||||||
|
alt="YourHutopy"
|
||||||
|
class="w-full mb-6"
|
||||||
|
src="/images/hutopymedia/homepage/votrehutopy.png"
|
||||||
|
/>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-lg leading-relaxed text-justify sm:mx-5 md:mx-1 homepagetext">{{ t('whatIsHutopy') }}</p>
|
<p class="text-lg leading-relaxed text-justify sm:mx-5 md:mx-1 homepagetext">
|
||||||
|
{{ t('whatIsHutopy') }}
|
||||||
|
</p>
|
||||||
<p class="text-lg leading-relaxed text-justify sm:mx-5 md:mx-1 homepagetext">
|
<p class="text-lg leading-relaxed text-justify sm:mx-5 md:mx-1 homepagetext">
|
||||||
{{ t('hutopyDescription') }}
|
{{ t('hutopyDescription') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -103,54 +121,71 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
<!-- Section droite : 4 images -->
|
<!-- Section droite : 4 images -->
|
||||||
<div class="mt-8 md:mt-0 grid grid-cols-2 gap-4 lg:ml-15">
|
<div class="mt-8 md:mt-0 grid grid-cols-2 gap-4 lg:ml-15">
|
||||||
<div><img alt="Grinding" class="w-full h-auto object-cover rounded-2xl"
|
<div>
|
||||||
src="/images/hutopymedia/homepage/grinding.png"></div>
|
<img
|
||||||
<div><img alt="Microphone" class="w-full h-auto object-cover rounded-2xl"
|
alt="Grinding"
|
||||||
src="/images/hutopymedia/homepage/sign.png"></div>
|
class="w-full h-auto object-cover rounded-2xl"
|
||||||
<div><img alt="Girl VR" class="w-full h-auto object-cover rounded-2xl"
|
src="/images/hutopymedia/homepage/grinding.png"
|
||||||
src="/images/hutopymedia/homepage/girlvr.png"></div>
|
/>
|
||||||
<div><img alt="Girl Army" class="w-full h-auto object-cover rounded-2xl"
|
</div>
|
||||||
src="/images/hutopymedia/homepage/girlarmy.png"></div>
|
<div>
|
||||||
|
<img
|
||||||
|
alt="Microphone"
|
||||||
|
class="w-full h-auto object-cover rounded-2xl"
|
||||||
|
src="/images/hutopymedia/homepage/sign.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
alt="Girl VR"
|
||||||
|
class="w-full h-auto object-cover rounded-2xl"
|
||||||
|
src="/images/hutopymedia/homepage/girlvr.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
alt="Girl Army"
|
||||||
|
class="w-full h-auto object-cover rounded-2xl"
|
||||||
|
src="/images/hutopymedia/homepage/girlarmy.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer class="mt-10"></Footer>
|
<Footer class="mt-10"></Footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.box-text {
|
.box-text {
|
||||||
color: #6A0164;
|
color: #6a0164;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inscription-btn-header {
|
.inscription-btn-header {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #6A0164;
|
background-color: #6a0164;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inscription-btn-header-outlined {
|
.inscription-btn-header-outlined {
|
||||||
color: #6A0164;
|
color: #6a0164;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inscription-btn {
|
.inscription-btn {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #6A0164;
|
background-color: #6a0164;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -160,13 +195,13 @@ const { t } = useI18n();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-btn {
|
.create-btn {
|
||||||
background-color: #6A0164;
|
background-color: #6a0164;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-radius: 10px
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay p {
|
.overlay p {
|
||||||
@@ -176,14 +211,13 @@ const { t } = useI18n();
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #F4F4F4;
|
background-color: #f4f4f4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-container {
|
.support-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center; /* Centre le bloc horizontalement */
|
justify-content: center; /* Centre le bloc horizontalement */
|
||||||
align-items: center; /* Centre le bloc verticalement (optionnel) */
|
align-items: center; /* Centre le bloc verticalement (optionnel) */
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-text {
|
.support-text {
|
||||||
@@ -194,13 +228,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-text .highlight {
|
.support-text .highlight {
|
||||||
color: #6A0164; /* Remplacez par la couleur souhaitée */
|
color: #6a0164; /* Remplacez par la couleur souhaitée */
|
||||||
font-weight: bold; /* Mettre en gras */
|
font-weight: bold; /* Mettre en gras */
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight2 {
|
.highlight2 {
|
||||||
color: #B81286; /* Remplacez par la couleur souhaitée */
|
color: #b81286; /* Remplacez par la couleur souhaitée */
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-image {
|
.logo-image {
|
||||||
@@ -214,7 +247,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-text {
|
.support-text {
|
||||||
font-size: 3.0rem; /* Ajustez la taille du texte */
|
font-size: 3rem; /* Ajustez la taille du texte */
|
||||||
line-height: 1.1; /* Ajustez l'espacement entre les lignes */
|
line-height: 1.1; /* Ajustez l'espacement entre les lignes */
|
||||||
text-align: left; /* Alignement du texte à gauche */
|
text-align: left; /* Alignement du texte à gauche */
|
||||||
font-weight: bold; /* Rend le texte gras */
|
font-weight: bold; /* Rend le texte gras */
|
||||||
@@ -222,7 +255,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|
||||||
.header-btn {
|
.header-btn {
|
||||||
margin-top: 60px;
|
margin-top: 60px;
|
||||||
}
|
}
|
||||||
@@ -235,9 +267,8 @@ body {
|
|||||||
|
|
||||||
.homepagetext {
|
.homepagetext {
|
||||||
color: white;
|
color: white;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: 'Roboto', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
@@ -261,32 +292,18 @@ body {
|
|||||||
"fr": {
|
"fr": {
|
||||||
"inscription": "S'inscrire",
|
"inscription": "S'inscrire",
|
||||||
"createPage": "Créer une Page",
|
"createPage": "Créer une Page",
|
||||||
"slogan": "Soutenez les projets qui vous tiennent à cœur",
|
"support": "Soutenir",
|
||||||
|
"creators": "Créateurs",
|
||||||
|
"projects": "Projets",
|
||||||
|
"love": "Passion",
|
||||||
"supportText": "Soutenir",
|
"supportText": "Soutenir",
|
||||||
"supportDescription": "Soutenez vos créateurs préférés et aidez-les à grandir. Vos contributions font une réelle différence dans leur parcours créatif.",
|
"supportDescription": "Soutenez vos créateurs préférés et aidez-les à grandir. Vos contributions font une réelle différence dans leur parcours créatif.",
|
||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
"creatorDescription": "Créez votre propre page et construisez votre Hutopy.",
|
"creatorDescription": "Créez votre propre page et commencez votre parcours créatif. Partagez votre passion avec le monde et construisez votre communauté.",
|
||||||
|
|
||||||
"signup": "S'inscrire",
|
"signup": "S'inscrire",
|
||||||
"whatIsHutopy": "Qu'est-ce que Hutopy ?",
|
"whatIsHutopy": "Qu'est-ce que Hutopy ?",
|
||||||
"hutopyDescription": "Hutopy est une plateforme qui connecte les créateurs avec leur audience. Nous fournissons des outils et des fonctionnalités pour aider les créateurs à monétiser leur contenu et à construire leur communauté.",
|
"hutopyDescription": "Hutopy est une plateforme qui connecte les créateurs avec leur audience. Nous fournissons des outils et des fonctionnalités pour aider les créateurs à monétiser leur contenu et à construire leur communauté.",
|
||||||
"hutopyValues": "Nos valeurs sont centrées sur la créativité, la communauté et le soutient."
|
"hutopyValues": "Nos valeurs sont centrées sur la créativité, la communauté et le soutien. Nous croyons en l'autonomisation des créateurs pour poursuivre leurs passions et construire des carrières durables."
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"inscription": "Registrarse",
|
|
||||||
"createPage": "Crear Página",
|
|
||||||
"support": "Apoyar",
|
|
||||||
"creators": "Creadores",
|
|
||||||
"projects": "Proyectos",
|
|
||||||
"love": "Pasión",
|
|
||||||
"supportText": "Apoyar",
|
|
||||||
"supportDescription": "Apoya a tus creadores favoritos y ayúdales a crecer. Tus contribuciones hacen una diferencia real en su viaje creativo.",
|
|
||||||
"create": "Crear",
|
|
||||||
"creatorDescription": "Crea tu propia página y comienza tu viaje creativo. Comparte tu pasión con el mundo y construye tu comunidad.",
|
|
||||||
"signup": "Registrarse",
|
|
||||||
"whatIsHutopy": "¿Qué es Hutopy?",
|
|
||||||
"hutopyDescription": "Hutopy es una plataforma que conecta a los creadores con su audiencia. Proporcionamos herramientas y funciones para ayudar a los creadores a monetizar su contenido y construir su comunidad.",
|
|
||||||
"hutopyValues": "Nuestros valores se centran en la creatividad, la comunidad y el apoyo. Creemos en empoderar a los creadores para perseguir sus pasiones y construir carreras sostenibles."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -4,19 +4,17 @@
|
|||||||
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
||||||
import { useUserProfileStore } from '@/stores/userProfileStore.js';
|
import { useUserProfileStore } from '@/stores/userProfileStore.js';
|
||||||
import { useLanguageStore } from '@/stores/languageStore.js';
|
import { useLanguageStore } from '@/stores/languageStore.js';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { mdiAccount, mdiFileAccountOutline, mdiLogin, mdiLogout, mdiTranslateVariant } from '@mdi/js';
|
import { mdiAccount, mdiFileAccountOutline, mdiLogin, mdiLogout, mdiTranslateVariant } from '@mdi/js';
|
||||||
|
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
const languageStore = useLanguageStore();
|
const languageStore = useLanguageStore();
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const userProfileStore = useUserProfileStore();
|
const userProfileStore = useUserProfileStore();
|
||||||
const creatorProfileStore = useCreatorProfileStore();
|
const creatorProfileStore = useCreatorProfileStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
function toggleLanguage() {
|
function toggleLanguage() {
|
||||||
const languages = ['fr', 'en', 'es'];
|
const languages = ['fr', 'en'];
|
||||||
const currentIndex = languages.indexOf(locale.value);
|
const currentIndex = languages.indexOf(locale.value);
|
||||||
const nextIndex = (currentIndex + 1) % languages.length;
|
const nextIndex = (currentIndex + 1) % languages.length;
|
||||||
languageStore.setLocale(languages[nextIndex]);
|
languageStore.setLocale(languages[nextIndex]);
|
||||||
@@ -117,23 +115,20 @@
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.side-container {
|
.side-container {
|
||||||
@apply bg-hSurface text-hOnSurface;
|
@apply bg-hSurface text-hOnSurface;
|
||||||
@apply lg:fixed lg:max-h-screen;
|
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply lg:flex-col lg:w-64 lg:max-w-64;
|
@apply max-h-screen;
|
||||||
@apply h-16 lg:h-screen;
|
@apply h-16;
|
||||||
@apply lg:border-r-2 lg:border-[#2d282d];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-logo {
|
.side-logo {
|
||||||
@apply flex flex-grow;
|
@apply flex flex-grow;
|
||||||
@apply items-center justify-start p-4;
|
@apply items-center justify-start p-4;
|
||||||
@apply lg:items-start lg:justify-center lg:pt-4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu {
|
.side-menu {
|
||||||
@apply flex gap-4 p-6;
|
@apply flex gap-4 p-6;
|
||||||
@apply items-center lg:items-stretch;
|
@apply items-center;
|
||||||
@apply flex-row-reverse lg:flex-col;
|
@apply flex-row-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-portrait {
|
.side-menu-portrait {
|
||||||
@@ -145,21 +140,20 @@
|
|||||||
.side-menu-items {
|
.side-menu-items {
|
||||||
@apply flex gap-2;
|
@apply flex gap-2;
|
||||||
@apply flex-row;
|
@apply flex-row;
|
||||||
@apply lg:w-full lg:flex-col;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-label {
|
.profile-label {
|
||||||
@apply ml-5;
|
@apply ml-5;
|
||||||
@apply text-lg font-sans capitalize;
|
@apply text-lg font-sans capitalize;
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply hidden lg:inline;
|
@apply hidden;
|
||||||
@apply min-w-40 truncate;
|
@apply min-w-40 truncate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@apply text-nowrap;
|
@apply text-nowrap;
|
||||||
@apply ml-4;
|
@apply ml-4;
|
||||||
@apply hidden lg:inline;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item-action {
|
.menu-item-action {
|
||||||
@@ -193,14 +187,6 @@
|
|||||||
"signIn": "Se Connecter",
|
"signIn": "Se Connecter",
|
||||||
"signOut": "Se Déconnecter"
|
"signOut": "Se Déconnecter"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"sidebar": {
|
|
||||||
"myPage": "Mi Página",
|
|
||||||
"myProfile": "Mi Perfil",
|
|
||||||
"signIn": "Iniciar Sesión",
|
|
||||||
"signOut": "Cerrar Sesión"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -948,39 +948,6 @@
|
|||||||
"phoneNumber": "Numéro de téléphone",
|
"phoneNumber": "Numéro de téléphone",
|
||||||
"title": "Titre",
|
"title": "Titre",
|
||||||
"removeStripe": "Retirer Stripe"
|
"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>
|
</i18n>
|
||||||
|
|||||||
@@ -1,35 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="card dialog">
|
<div class="card dialog">
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
{{ t('title') }}
|
{{ t('title') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
variant="outlined"
|
|
||||||
v-model="alias"
|
v-model="alias"
|
||||||
:label="t('label')"
|
:label="t('label')"
|
||||||
|
variant="outlined"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
<button
|
||||||
<button class="secondary"
|
class="secondary"
|
||||||
@click="requestClose">
|
@click="requestClose"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="primary"
|
<button
|
||||||
@click="requestSave">
|
class="primary"
|
||||||
|
@click="requestSave"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -37,13 +35,13 @@ import {ref} from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const props = defineProps(['alias'])
|
const props = defineProps(['alias']);
|
||||||
const emit = defineEmits(['close', 'save'])
|
const emit = defineEmits(['close', 'save']);
|
||||||
|
|
||||||
const alias = ref(props.alias)
|
const alias = ref(props.alias);
|
||||||
|
|
||||||
const requestClose = () => emit('close')
|
const requestClose = () => emit('close');
|
||||||
const requestSave = () => emit('save', alias.value)
|
const requestSave = () => emit('save', alias.value);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
@@ -55,12 +53,6 @@ const requestSave = () => emit('save', alias.value)
|
|||||||
"fr": {
|
"fr": {
|
||||||
"title": "Alias",
|
"title": "Alias",
|
||||||
"label": "Votre alias"
|
"label": "Votre alias"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Alias",
|
|
||||||
"label": "Tu alias"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card dialog">
|
<div class="card dialog">
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
{{ t('changePassword') }}
|
{{ t('changePassword') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -8,36 +7,64 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p class="description mb-4">{{ t('passwordDescription') }}</p>
|
<p class="description mb-4">{{ t('passwordDescription') }}</p>
|
||||||
|
|
||||||
<v-text-field v-model="newPassword" :label="t('newPassword')" :type="showNewPassword ? 'text' : 'password'"
|
<v-text-field
|
||||||
variant="outlined" required :hint="t('passwordRequirements')">
|
v-model="newPassword"
|
||||||
|
:hint="t('passwordRequirements')"
|
||||||
|
:label="t('newPassword')"
|
||||||
|
:type="showNewPassword ? 'text' : 'password'"
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
<template v-slot:append-inner>
|
<template v-slot:append-inner>
|
||||||
<v-icon @click="showNewPassword = !showNewPassword" class="visibility-toggle" size="small"
|
<v-icon
|
||||||
:icon="showNewPassword ? mdiEyeOff : mdiEye" />
|
:icon="showNewPassword ? mdiEyeOff : mdiEye"
|
||||||
|
class="visibility-toggle"
|
||||||
|
size="small"
|
||||||
|
@click="showNewPassword = !showNewPassword"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
|
|
||||||
<v-text-field v-model="confirmPassword" :label="t('confirmPassword')"
|
<v-text-field
|
||||||
:type="showConfirmPassword ? 'text' : 'password'" variant="outlined" required>
|
v-model="confirmPassword"
|
||||||
|
:label="t('confirmPassword')"
|
||||||
|
:type="showConfirmPassword ? 'text' : 'password'"
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
<template v-slot:append-inner>
|
<template v-slot:append-inner>
|
||||||
<v-icon @click="showConfirmPassword = !showConfirmPassword" class="visibility-toggle" size="small"
|
<v-icon
|
||||||
:icon="showNewPassword ? mdiEyeOff : mdiEye" />
|
:icon="showNewPassword ? mdiEyeOff : mdiEye"
|
||||||
|
class="visibility-toggle"
|
||||||
|
size="small"
|
||||||
|
@click="showConfirmPassword = !showConfirmPassword"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
|
|
||||||
<div v-if="errorMessage" class="error-message mb-4">
|
<div
|
||||||
|
v-if="errorMessage"
|
||||||
|
class="error-message mb-4"
|
||||||
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary" @click="$emit('closeRequested')">
|
<button
|
||||||
|
class="secondary"
|
||||||
|
@click="$emit('closeRequested')"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary" @click="handleChangePassword" :disabled="isLoading">
|
<button
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="primary"
|
||||||
|
@click="handleChangePassword"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -144,18 +171,6 @@ async function handleChangePassword() {
|
|||||||
"passwordsDoNotMatch": "Les nouveaux mots de passe ne correspondent pas",
|
"passwordsDoNotMatch": "Les nouveaux mots de passe ne correspondent pas",
|
||||||
"passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères",
|
"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."
|
"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."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
@click="cancel">
|
class="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
@click="save">
|
class="primary"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,15 +31,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import {useClient} from "@/plugins/api.js";
|
import { useClient } from '@/plugins/api.js';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
email: {
|
email: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: String,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['closeRequested']);
|
const emits = defineEmits(['closeRequested']);
|
||||||
@@ -45,10 +49,8 @@ const email = ref(props.email);
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
await client.post(
|
await client.post(`/api/users/email`, {
|
||||||
`/api/users/email`,
|
email: email.value,
|
||||||
{
|
|
||||||
email: email.value
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emits('closeRequested');
|
emits('closeRequested');
|
||||||
@@ -71,12 +73,6 @@ const cancel = () => {
|
|||||||
"fr": {
|
"fr": {
|
||||||
"title": "Changez votre Courriel",
|
"title": "Changez votre Courriel",
|
||||||
"label": "Votre email"
|
"label": "Votre email"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Cambia tu correo electrónico",
|
|
||||||
"label": "Tu correo electrónico"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import {ref} from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const props = defineProps(['firstname', 'lastname'])
|
const props = defineProps(['firstname', 'lastname']);
|
||||||
const emit = defineEmits(['close', 'save'])
|
const emit = defineEmits(['close', 'save']);
|
||||||
|
|
||||||
const firstname = ref(props.firstname)
|
const firstname = ref(props.firstname);
|
||||||
const lastname = ref(props.lastname)
|
const lastname = ref(props.lastname);
|
||||||
|
|
||||||
const requestClose = () => emit('close')
|
const requestClose = () => emit('close');
|
||||||
const requestSave = () => emit('save', firstname.value, lastname.value)
|
const requestSave = () => emit('save', firstname.value, lastname.value);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -21,28 +21,32 @@ const requestSave = () => emit('save', firstname.value, lastname.value)
|
|||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
variant="outlined"
|
|
||||||
v-model="firstname"
|
v-model="firstname"
|
||||||
:label="t('firstname')"
|
:label="t('firstname')"
|
||||||
|
variant="outlined"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
variant="outlined"
|
|
||||||
v-model="lastname"
|
v-model="lastname"
|
||||||
:label="t('lastname')"
|
:label="t('lastname')"
|
||||||
|
variant="outlined"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
@click="requestClose">
|
class="secondary"
|
||||||
|
@click="requestClose"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="primary"
|
<button
|
||||||
@click="requestSave">
|
class="primary"
|
||||||
|
@click="requestSave"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,11 +64,6 @@ const requestSave = () => emit('save', firstname.value, lastname.value)
|
|||||||
"title": "Nom complet",
|
"title": "Nom complet",
|
||||||
"firstname": "Prénom",
|
"firstname": "Prénom",
|
||||||
"lastname": "Nom"
|
"lastname": "Nom"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Nombre completo",
|
|
||||||
"firstname": "Nombre",
|
|
||||||
"lastname": "Apellido"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -4,28 +4,36 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="email"
|
v-model="email"
|
||||||
class="w-full p-2"
|
|
||||||
:label="t('email')"
|
|
||||||
type="email"
|
|
||||||
variant="outlined"
|
|
||||||
:error-messages="emailErrors"
|
:error-messages="emailErrors"
|
||||||
|
:label="t('email')"
|
||||||
:rules="emailRules"
|
:rules="emailRules"
|
||||||
|
class="w-full p-2"
|
||||||
|
type="email"
|
||||||
validate-on="blur"
|
validate-on="blur"
|
||||||
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-alert
|
<v-alert
|
||||||
v-if="!!errorMessage"
|
v-if="!!errorMessage"
|
||||||
|
class="mt-4"
|
||||||
outlined
|
outlined
|
||||||
type="error"
|
type="error"
|
||||||
class="mt-4">
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary" @click="$emit('closeRequested')">
|
<button
|
||||||
|
class="secondary"
|
||||||
|
@click="$emit('closeRequested')"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary" @click="saveEmail" :disabled="!canSave || isLoading">
|
<button
|
||||||
|
:disabled="!canSave || isLoading"
|
||||||
|
class="primary"
|
||||||
|
@click="saveEmail"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useClient } from '@/plugins/api.js';
|
import { useClient } from '@/plugins/api.js';
|
||||||
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
||||||
@@ -46,8 +54,8 @@ const creatorProfileStore = useCreatorProfileStore();
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const email = ref(props.creator.presentation?.email || '');
|
const email = ref(props.creator.presentation?.email || '');
|
||||||
@@ -55,7 +63,7 @@ const isLoading = ref(false);
|
|||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
const isValidEmail = (email) => {
|
const isValidEmail = email => {
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
return emailRegex.test(email);
|
return emailRegex.test(email);
|
||||||
};
|
};
|
||||||
@@ -76,14 +84,12 @@ const emailErrors = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const canSave = computed(() => {
|
const canSave = computed(() => {
|
||||||
return email.value &&
|
return email.value && isValidEmail(email.value) && email.value !== (props.creator.presentation?.email || '');
|
||||||
isValidEmail(email.value) &&
|
|
||||||
email.value !== (props.creator.presentation?.email || '');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function saveEmail() {
|
async function saveEmail() {
|
||||||
if (!props.creator.id) {
|
if (!props.creator.id) {
|
||||||
console.error("Creator ID is missing!");
|
console.error('Creator ID is missing!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +102,9 @@ async function saveEmail() {
|
|||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
|
|
||||||
// Save email
|
// Save email
|
||||||
await client.post(
|
await client.post(`/api/creators/${props.creator.id}/email`, {
|
||||||
`/api/creators/${props.creator.id}/email`,
|
email: email.value.trim(),
|
||||||
{
|
});
|
||||||
email: email.value.trim()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh creator profile
|
// Refresh creator profile
|
||||||
await creatorProfileStore.fetchCreatorProfile();
|
await creatorProfileStore.fetchCreatorProfile();
|
||||||
@@ -109,7 +112,7 @@ async function saveEmail() {
|
|||||||
// Close dialog
|
// Close dialog
|
||||||
emit('closeRequested');
|
emit('closeRequested');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving email:", error);
|
console.error('Error saving email:', error);
|
||||||
if (error?.response?.data?.errors) {
|
if (error?.response?.data?.errors) {
|
||||||
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
|
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
|
||||||
} else {
|
} else {
|
||||||
@@ -156,19 +159,6 @@ const emit = defineEmits(['closeRequested']);
|
|||||||
"errors": {
|
"errors": {
|
||||||
"unexpected": "Une erreur inattendue s'est produite"
|
"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>
|
</i18n>
|
||||||
@@ -6,8 +6,8 @@ import { useI18n } from 'vue-i18n';
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['closeRequested']);
|
const emits = defineEmits(['closeRequested']);
|
||||||
@@ -18,12 +18,9 @@ const client = useClient();
|
|||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
await client.post(
|
await client.post(`/api/creators/${props.creator.id}/name`, {
|
||||||
`/api/creators/${props.creator.id}/name`,
|
name: name.value,
|
||||||
{
|
});
|
||||||
name: name.value
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
props.creator.name = name.value;
|
props.creator.name = name.value;
|
||||||
emits('closeRequested');
|
emits('closeRequested');
|
||||||
@@ -52,12 +49,16 @@ const cancel = () => {
|
|||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
@click="cancel">
|
class="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
@click="save">
|
class="primary"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,9 +66,7 @@ const cancel = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
{
|
{
|
||||||
@@ -78,10 +77,6 @@ const cancel = () => {
|
|||||||
"fr": {
|
"fr": {
|
||||||
"title": "Modifier le nom",
|
"title": "Modifier le nom",
|
||||||
"label": "Votre nom"
|
"label": "Votre nom"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Cambiar nombre",
|
|
||||||
"label": "Tu nombre"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -4,32 +4,40 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="displayPhoneNumber"
|
v-model="displayPhoneNumber"
|
||||||
class="w-full p-2"
|
|
||||||
:label="t('phoneNumber')"
|
|
||||||
type="tel"
|
|
||||||
variant="outlined"
|
|
||||||
:error-messages="phoneErrors"
|
:error-messages="phoneErrors"
|
||||||
:rules="phoneRules"
|
:label="t('phoneNumber')"
|
||||||
validate-on="blur"
|
|
||||||
:placeholder="t('phonePlaceholder')"
|
:placeholder="t('phonePlaceholder')"
|
||||||
|
:rules="phoneRules"
|
||||||
|
class="w-full p-2"
|
||||||
|
maxlength="14"
|
||||||
|
type="tel"
|
||||||
|
validate-on="blur"
|
||||||
|
variant="outlined"
|
||||||
@input="handlePhoneInput"
|
@input="handlePhoneInput"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
maxlength="14"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-alert
|
<v-alert
|
||||||
v-if="!!errorMessage"
|
v-if="!!errorMessage"
|
||||||
|
class="mt-4"
|
||||||
outlined
|
outlined
|
||||||
type="error"
|
type="error"
|
||||||
class="mt-4">
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary" @click="$emit('closeRequested')">
|
<button
|
||||||
|
class="secondary"
|
||||||
|
@click="$emit('closeRequested')"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary" @click="savePhoneNumber" :disabled="!canSave || isLoading">
|
<button
|
||||||
|
:disabled="!canSave || isLoading"
|
||||||
|
class="primary"
|
||||||
|
@click="savePhoneNumber"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +46,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useClient } from '@/plugins/api.js';
|
import { useClient } from '@/plugins/api.js';
|
||||||
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
||||||
@@ -50,12 +58,12 @@ const creatorProfileStore = useCreatorProfileStore();
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Format existing phone number to display format
|
// Format existing phone number to display format
|
||||||
const formatPhoneForDisplay = (phone) => {
|
const formatPhoneForDisplay = phone => {
|
||||||
if (!phone) return '';
|
if (!phone) return '';
|
||||||
const digits = phone.replace(/\D/g, '');
|
const digits = phone.replace(/\D/g, '');
|
||||||
if (digits.length === 10) {
|
if (digits.length === 10) {
|
||||||
@@ -65,7 +73,7 @@ const formatPhoneForDisplay = (phone) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Extract just the digits from formatted phone
|
// Extract just the digits from formatted phone
|
||||||
const extractDigits = (formattedPhone) => {
|
const extractDigits = formattedPhone => {
|
||||||
return formattedPhone.replace(/\D/g, '');
|
return formattedPhone.replace(/\D/g, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +83,7 @@ const isLoading = ref(false);
|
|||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
|
|
||||||
// Phone number formatting and validation
|
// Phone number formatting and validation
|
||||||
const formatPhoneNumber = (digits) => {
|
const formatPhoneNumber = digits => {
|
||||||
// Remove all non-digits
|
// Remove all non-digits
|
||||||
const cleaned = digits.replace(/\D/g, '');
|
const cleaned = digits.replace(/\D/g, '');
|
||||||
|
|
||||||
@@ -86,7 +94,7 @@ const formatPhoneNumber = (digits) => {
|
|||||||
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
|
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePhoneInput = (event) => {
|
const handlePhoneInput = event => {
|
||||||
const input = event.target.value;
|
const input = event.target.value;
|
||||||
const digits = extractDigits(input);
|
const digits = extractDigits(input);
|
||||||
|
|
||||||
@@ -97,7 +105,7 @@ const handlePhoneInput = (event) => {
|
|||||||
displayPhoneNumber.value = formatPhoneNumber(digits);
|
displayPhoneNumber.value = formatPhoneNumber(digits);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeydown = (event) => {
|
const handleKeydown = event => {
|
||||||
// Allow backspace, delete, tab, escape, enter
|
// Allow backspace, delete, tab, escape, enter
|
||||||
if ([8, 9, 27, 13, 46].includes(event.keyCode)) return;
|
if ([8, 9, 27, 13, 46].includes(event.keyCode)) return;
|
||||||
|
|
||||||
@@ -114,11 +122,11 @@ const handleKeydown = (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Watch for changes to phoneDigits to update display
|
// Watch for changes to phoneDigits to update display
|
||||||
watch(phoneDigits, (newDigits) => {
|
watch(phoneDigits, newDigits => {
|
||||||
displayPhoneNumber.value = formatPhoneNumber(newDigits);
|
displayPhoneNumber.value = formatPhoneNumber(newDigits);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isValidPhoneNumber = (digits) => {
|
const isValidPhoneNumber = digits => {
|
||||||
return digits.length === 10;
|
return digits.length === 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,13 +152,15 @@ const phoneErrors = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const canSave = computed(() => {
|
const canSave = computed(() => {
|
||||||
return phoneDigits.value.length === 10 &&
|
return (
|
||||||
phoneDigits.value !== extractDigits(props.creator.presentation?.phoneNumber || '');
|
phoneDigits.value.length === 10 &&
|
||||||
|
phoneDigits.value !== extractDigits(props.creator.presentation?.phoneNumber || '')
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function savePhoneNumber() {
|
async function savePhoneNumber() {
|
||||||
if (!props.creator.id) {
|
if (!props.creator.id) {
|
||||||
console.error("Creator ID is missing!");
|
console.error('Creator ID is missing!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,12 +175,9 @@ async function savePhoneNumber() {
|
|||||||
// Save the formatted phone number
|
// Save the formatted phone number
|
||||||
const formattedPhone = formatPhoneNumber(phoneDigits.value);
|
const formattedPhone = formatPhoneNumber(phoneDigits.value);
|
||||||
|
|
||||||
await client.post(
|
await client.post(`/api/creators/${props.creator.id}/phone`, {
|
||||||
`/api/creators/${props.creator.id}/phone`,
|
phoneNumber: formattedPhone,
|
||||||
{
|
});
|
||||||
phoneNumber: formattedPhone
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh creator profile
|
// Refresh creator profile
|
||||||
await creatorProfileStore.fetchCreatorProfile();
|
await creatorProfileStore.fetchCreatorProfile();
|
||||||
@@ -178,7 +185,7 @@ async function savePhoneNumber() {
|
|||||||
// Close dialog
|
// Close dialog
|
||||||
emit('closeRequested');
|
emit('closeRequested');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving phone number:", error);
|
console.error('Error saving phone number:', error);
|
||||||
if (error?.response?.data?.errors) {
|
if (error?.response?.data?.errors) {
|
||||||
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
|
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
|
||||||
} else {
|
} else {
|
||||||
@@ -227,20 +234,6 @@ const emit = defineEmits(['closeRequested']);
|
|||||||
"errors": {
|
"errors": {
|
||||||
"unexpected": "Une erreur inattendue s'est produite"
|
"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>
|
</i18n>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
||||||
import {useClient} from "@/plugins/api.js";
|
import { useClient } from '@/plugins/api.js';
|
||||||
import NameEditor from "@/views/creators/NameEditor.vue";
|
import NameEditor from '@/views/creators/NameEditor.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['closeRequested']);
|
const emit = defineEmits(['closeRequested']);
|
||||||
@@ -24,7 +24,7 @@ const errorMessage = ref('');
|
|||||||
const isCurrentHandle = ref(false);
|
const isCurrentHandle = ref(false);
|
||||||
|
|
||||||
// Watch for changes to the new slug to check if it's the same as the current one
|
// Watch for changes to the new slug to check if it's the same as the current one
|
||||||
watch(newSlug, (newValue) => {
|
watch(newSlug, newValue => {
|
||||||
isCurrentHandle.value = newValue === props.creator.slug;
|
isCurrentHandle.value = newValue === props.creator.slug;
|
||||||
if (isCurrentHandle.value) {
|
if (isCurrentHandle.value) {
|
||||||
slugReservationId.value = undefined;
|
slugReservationId.value = undefined;
|
||||||
@@ -43,7 +43,7 @@ async function save() {
|
|||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
|
|
||||||
await client.put(`/api/creators/${props.creator.id}/slug`, {
|
await client.put(`/api/creators/${props.creator.id}/slug`, {
|
||||||
slugReservationId: slugReservationId.value
|
slugReservationId: slugReservationId.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
await creatorProfileStore.fetchCreatorProfile();
|
await creatorProfileStore.fetchCreatorProfile();
|
||||||
@@ -74,26 +74,31 @@ const cancel = () => {
|
|||||||
<name-editor
|
<name-editor
|
||||||
v-model:name="newSlug"
|
v-model:name="newSlug"
|
||||||
:creator-name-reservation-id="slugReservationId"
|
:creator-name-reservation-id="slugReservationId"
|
||||||
@update:creator-name-reservation-id="handleSlugReservationIdChanged"
|
|
||||||
:original-slug="creator.slug"
|
:original-slug="creator.slug"
|
||||||
|
@update:creator-name-reservation-id="handleSlugReservationIdChanged"
|
||||||
></name-editor>
|
></name-editor>
|
||||||
|
|
||||||
<v-alert
|
<v-alert
|
||||||
v-if="!!errorMessage"
|
v-if="!!errorMessage"
|
||||||
|
class="mt-4"
|
||||||
outlined
|
outlined
|
||||||
type="error"
|
type="error"
|
||||||
class="mt-4">
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
@click="cancel">
|
class="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
|
:disabled="!canSave || isOperationPending"
|
||||||
|
class="primary"
|
||||||
@click="save"
|
@click="save"
|
||||||
:disabled="!canSave || isOperationPending">
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,8 +106,7 @@ const cancel = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
</style>
|
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
{
|
{
|
||||||
@@ -111,9 +115,6 @@ const cancel = () => {
|
|||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"title": "Modifier l'identifiant du créateur"
|
"title": "Modifier l'identifiant du créateur"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Cambiar identificador del creador"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
@@ -51,12 +51,16 @@ const cancel = () => {
|
|||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
@click="cancel">
|
class="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
@click="save">
|
class="primary"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,9 +68,7 @@ const cancel = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
{
|
{
|
||||||
@@ -77,10 +79,6 @@ const cancel = () => {
|
|||||||
"fr": {
|
"fr": {
|
||||||
"title": "Modifier l'ID Stripe",
|
"title": "Modifier l'ID Stripe",
|
||||||
"label": "Votre ID Stripe"
|
"label": "Votre ID Stripe"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Cambiar ID de Stripe",
|
|
||||||
"label": "Tu ID de Stripe"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useI18n } from 'vue-i18n';
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['closeRequested']);
|
const emits = defineEmits(['closeRequested']);
|
||||||
@@ -18,12 +18,9 @@ const client = useClient();
|
|||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
await client.post(
|
await client.post(`/api/creators/${props.creator.id}/title`, {
|
||||||
`/api/creators/${props.creator.id}/title`,
|
title: title.value,
|
||||||
{
|
});
|
||||||
title: title.value
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
props.creator.title = title.value;
|
props.creator.title = title.value;
|
||||||
emits('closeRequested');
|
emits('closeRequested');
|
||||||
@@ -39,7 +36,6 @@ const cancel = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card dialog">
|
<div class="card dialog">
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
{{ t('title') }}
|
{{ t('title') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -53,12 +49,16 @@ const cancel = () => {
|
|||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
@click="cancel">
|
class="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
@click="save">
|
class="primary"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,9 +66,7 @@ const cancel = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
{
|
{
|
||||||
@@ -79,10 +77,6 @@ const cancel = () => {
|
|||||||
"fr": {
|
"fr": {
|
||||||
"title": "Modifier le titre",
|
"title": "Modifier le titre",
|
||||||
"label": "Votre titre"
|
"label": "Votre titre"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Cambiar título",
|
|
||||||
"label": "Tu título"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@@ -1,87 +1,81 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue'
|
import { ref } from 'vue';
|
||||||
import {useClient} from "@/plugins/api.js";
|
import { useClient } from '@/plugins/api.js';
|
||||||
import X from "@/views/svg/X.vue";
|
import X from '@/views/svg/X.vue';
|
||||||
import Tiktok from "@/views/svg/Tiktok.vue";
|
import Tiktok from '@/views/svg/Tiktok.vue';
|
||||||
import Reddit from "@/views/svg/Reddit.vue";
|
import Reddit from '@/views/svg/Reddit.vue';
|
||||||
import Web from "@/views/svg/Web.vue";
|
import Web from '@/views/svg/Web.vue';
|
||||||
import Youtube from "@/views/svg/Youtube.vue";
|
import Youtube from '@/views/svg/Youtube.vue';
|
||||||
import Linkedin from "@/views/svg/Linkedin.vue";
|
import Linkedin from '@/views/svg/Linkedin.vue';
|
||||||
import Instagram from "@/views/svg/Instagram.vue";
|
import Instagram from '@/views/svg/Instagram.vue';
|
||||||
import Facebook from "@/views/svg/Facebook.vue";
|
import Facebook from '@/views/svg/Facebook.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['closeRequested'])
|
const emits = defineEmits(['closeRequested']);
|
||||||
|
|
||||||
const facebookUrl = ref(props.creator.socials.facebookUrl)
|
const facebookUrl = ref(props.creator.socials.facebookUrl);
|
||||||
const instagramUrl = ref(props.creator.socials.instagramUrl)
|
const instagramUrl = ref(props.creator.socials.instagramUrl);
|
||||||
const linkedInUrl = ref(props.creator.socials.linkedInUrl)
|
const linkedInUrl = ref(props.creator.socials.linkedInUrl);
|
||||||
const redditUrl = ref(props.creator.socials.redditUrl)
|
const redditUrl = ref(props.creator.socials.redditUrl);
|
||||||
const tikTokUrl = ref(props.creator.socials.tikTokUrl)
|
const tikTokUrl = ref(props.creator.socials.tikTokUrl);
|
||||||
const websiteUrl = ref(props.creator.socials.websiteUrl)
|
const websiteUrl = ref(props.creator.socials.websiteUrl);
|
||||||
const xUrl = ref(props.creator.socials.xUrl)
|
const xUrl = ref(props.creator.socials.xUrl);
|
||||||
const youtubeUrl = ref(props.creator.socials.youtubeUrl)
|
const youtubeUrl = ref(props.creator.socials.youtubeUrl);
|
||||||
|
|
||||||
const client = useClient()
|
const client = useClient();
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
await client.post(
|
await client.post(`/api/creators/${props.creator.id}/socials`, {
|
||||||
`/api/creators/${props.creator.id}/socials`,
|
facebookUrl: facebookUrl.value || null,
|
||||||
{
|
instagramUrl: instagramUrl.value || null,
|
||||||
"facebookUrl": facebookUrl.value || null,
|
linkedInUrl: linkedInUrl.value || null,
|
||||||
"instagramUrl": instagramUrl.value || null,
|
redditUrl: redditUrl.value || null,
|
||||||
"linkedInUrl": linkedInUrl.value || null,
|
tikTokUrl: tikTokUrl.value || null,
|
||||||
"redditUrl": redditUrl.value || null,
|
websiteUrl: websiteUrl.value || null,
|
||||||
"tikTokUrl": tikTokUrl.value || null,
|
xUrl: xUrl.value || null,
|
||||||
"websiteUrl": websiteUrl.value || null,
|
youtubeUrl: youtubeUrl.value || null,
|
||||||
"xUrl": xUrl.value || null,
|
});
|
||||||
"youtubeUrl": youtubeUrl.value || null,
|
|
||||||
})
|
|
||||||
|
|
||||||
props.creator.socials.facebookUrl = facebookUrl
|
props.creator.socials.facebookUrl = facebookUrl;
|
||||||
props.creator.socials.instagramUrl = instagramUrl
|
props.creator.socials.instagramUrl = instagramUrl;
|
||||||
props.creator.socials.linkedInUrl = linkedInUrl
|
props.creator.socials.linkedInUrl = linkedInUrl;
|
||||||
props.creator.socials.redditUrl = redditUrl
|
props.creator.socials.redditUrl = redditUrl;
|
||||||
props.creator.socials.tikTokUrl = tikTokUrl
|
props.creator.socials.tikTokUrl = tikTokUrl;
|
||||||
props.creator.socials.websiteUrl = websiteUrl
|
props.creator.socials.websiteUrl = websiteUrl;
|
||||||
props.creator.socials.xUrl = xUrl
|
props.creator.socials.xUrl = xUrl;
|
||||||
props.creator.socials.youtubeUrl = youtubeUrl
|
props.creator.socials.youtubeUrl = youtubeUrl;
|
||||||
|
|
||||||
emits('closeRequested')
|
emits('closeRequested');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emits('closeRequested')
|
emits('closeRequested');
|
||||||
}
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="card dialog">
|
<div class="card dialog">
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
{{ t('title') }}
|
{{ t('title') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|
||||||
<div class="editor-line">
|
<div class="editor-line">
|
||||||
<facebook class="social-icon"></facebook>
|
<facebook class="social-icon"></facebook>
|
||||||
<input
|
<input
|
||||||
v-model="facebookUrl"
|
v-model="facebookUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('facebook')"
|
:placeholder="t('facebook')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,8 +84,8 @@ const cancel = () => {
|
|||||||
<instagram class="social-icon"></instagram>
|
<instagram class="social-icon"></instagram>
|
||||||
<input
|
<input
|
||||||
v-model="instagramUrl"
|
v-model="instagramUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('instagram')"
|
:placeholder="t('instagram')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,8 +94,8 @@ const cancel = () => {
|
|||||||
<linkedin class="social-icon"></linkedin>
|
<linkedin class="social-icon"></linkedin>
|
||||||
<input
|
<input
|
||||||
v-model="linkedInUrl"
|
v-model="linkedInUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('linkedin')"
|
:placeholder="t('linkedin')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,8 +104,8 @@ const cancel = () => {
|
|||||||
<reddit class="social-icon"></reddit>
|
<reddit class="social-icon"></reddit>
|
||||||
<input
|
<input
|
||||||
v-model="redditUrl"
|
v-model="redditUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('reddit')"
|
:placeholder="t('reddit')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,8 +114,8 @@ const cancel = () => {
|
|||||||
<tiktok class="social-icon"></tiktok>
|
<tiktok class="social-icon"></tiktok>
|
||||||
<input
|
<input
|
||||||
v-model="tikTokUrl"
|
v-model="tikTokUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('tiktok')"
|
:placeholder="t('tiktok')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,8 +124,8 @@ const cancel = () => {
|
|||||||
<web class="social-icon"></web>
|
<web class="social-icon"></web>
|
||||||
<input
|
<input
|
||||||
v-model="websiteUrl"
|
v-model="websiteUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('website')"
|
:placeholder="t('website')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,8 +134,8 @@ const cancel = () => {
|
|||||||
<x class="social-icon"></x>
|
<x class="social-icon"></x>
|
||||||
<input
|
<input
|
||||||
v-model="xUrl"
|
v-model="xUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('x')"
|
:placeholder="t('x')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,31 +144,31 @@ const cancel = () => {
|
|||||||
<youtube class="social-icon"></youtube>
|
<youtube class="social-icon"></youtube>
|
||||||
<input
|
<input
|
||||||
v-model="youtubeUrl"
|
v-model="youtubeUrl"
|
||||||
class="input-field"
|
|
||||||
:placeholder="t('youtube')"
|
:placeholder="t('youtube')"
|
||||||
|
class="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button
|
||||||
@click="cancel">
|
class="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button
|
||||||
@click="save">
|
class="primary"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.editor-line {
|
.editor-line {
|
||||||
@apply flex flex-row gap-4;
|
@apply flex flex-row gap-4;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@@ -190,9 +184,8 @@ const cancel = () => {
|
|||||||
@apply transition duration-200;
|
@apply transition duration-200;
|
||||||
@apply ring-1 ring-[#6D6C70] focus:outline-none focus:ring-hutopySecondary;
|
@apply ring-1 ring-[#6D6C70] focus:outline-none focus:ring-hutopySecondary;
|
||||||
@apply hover:ring-hutopyPrimary;
|
@apply hover:ring-hutopyPrimary;
|
||||||
@apply placeholder:text-[#6D6C70]
|
@apply placeholder:text-[#6D6C70];
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
@@ -202,9 +195,6 @@ const cancel = () => {
|
|||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"title": "Liens des réseaux sociaux"
|
"title": "Liens des réseaux sociaux"
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"title": "Enlaces de redes sociales"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
Reference in New Issue
Block a user