chore(pack): optimize dependencies, reduce bundle size

This commit is contained in:
2025-06-05 13:52:05 -04:00
parent 74323247c9
commit 31ba18fa8d
145 changed files with 992 additions and 15921 deletions

View File

@@ -1,52 +1,36 @@
<template>
<div class="flex min-h-full justify-center items-center w-full p-4">
<div class="flex min-h-full w-full items-center justify-center p-4">
<div class="flex flex-col gap-10 w-full max-w-[512px]">
<h1 class="text-2xl font-bold login-text text-center">
<div class="flex w-full max-w-[512px] flex-col gap-10">
<h1 class="login-text text-center text-2xl font-bold">
{{ t('title') }}
</h1>
<div class="flex flex-col gap-4">
<google-login :callback="googleCallback"
popup-type="TOKEN">
<google-login :callback="googleCallback" popup-type="TOKEN">
<button class="secondary">
<v-icon class="mr-2">mdi-google</v-icon>
<v-icon class="mr-2" :icon="mdiGoogle" />
{{ t('continueWithGoogle') }}
</button>
</google-login>
</div>
<div class="flex items-center my-4">
<div class="flex-grow h-[1px] bg-gray-200"></div>
<span class="px-3 text-gray-300 uppercase text-sm font-semibold">{{ t('orContinueWith') }}</span>
<div class="flex-grow h-[1px] bg-gray-200"></div>
<div class="my-4 flex items-center">
<div class="h-px grow bg-gray-200"></div>
<span class="px-3 text-sm font-semibold uppercase text-gray-300">{{ t('orContinueWith') }}</span>
<div class="h-px grow bg-gray-200"></div>
</div>
<!-- Add email/password form -->
<v-form @submit.prevent="handleLocalLogin">
<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')" type="email" required></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>
<v-icon
@click="showPassword = !showPassword"
class="visibility-toggle"
size="small"
>
{{ showPassword ? 'mdi-eye-off' : 'mdi-eye' }}
</v-icon>
<v-icon @click="showPassword = !showPassword" class="visibility-toggle" size="small"
:icon="showPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
@@ -55,12 +39,12 @@
</v-btn>
<div class="text-center">
<a @click="forgotPassword" class="text-sm text-blue-500 cursor-pointer">
<a @click="forgotPassword" class="cursor-pointer text-sm text-blue-500">
{{ t('forgotPassword') }}
</a>
</div>
<div class="text-center mt-4">
<div class="mt-4 text-center">
{{ t('noAccount') }}
<router-link to="/register" class="text-blue-500">
{{ t('register') }}
@@ -79,13 +63,14 @@
</template>
<script setup>
import {ref} from 'vue';
import {GoogleLogin} from "vue3-google-login";
import {useAuthStore} from '@/stores/authStore.js';
import {useI18n} from 'vue-i18n';
import {useRouter} from 'vue-router';
import { ref } from 'vue';
import { GoogleLogin } from "vue3-google-login";
import { useAuthStore } from '@/stores/authStore.js';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { mdiGoogle, mdiEye, mdiEyeOff } from '@mdi/js';
const {t} = useI18n();
const { t } = useI18n();
const router = useRouter();
const authStore = useAuthStore();
@@ -146,7 +131,8 @@ function forgotPassword() {
/* Dark mode support if needed */
@media (prefers-color-scheme: dark) {
.custom-divider {
background-color: rgb(75, 85, 99); /* Equivalent to gray-600 */
background-color: rgb(75, 85, 99);
/* Equivalent to gray-600 */
}
}
</style>
@@ -193,4 +179,4 @@ function forgotPassword() {
"continueWithGoogle": "Continuar con Google"
}
}
</i18n>
</i18n>

View File

@@ -4,7 +4,7 @@
<!-- Navigation Link at the top -->
<div class="navigation-link">
<button class="link-button" @click="goBack()">
<v-icon>mdi-arrow-left</v-icon>
<v-icon :icon="mdiArrowLeft" />
{{ t('returnToCreator') }}
</button>
</div>
@@ -14,9 +14,7 @@
</h1>
<p>
<v-icon size="120" color="success">
mdi-check-circle
</v-icon>
<v-icon size="120" color="success" :icon="mdiCheckCircle" />
</p>
<p>
@@ -40,9 +38,10 @@
</template>
<script setup>
import {useRouter, useRoute} from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useBrandingStore } from '@/stores/brandingStore.js';
import { mdiArrowLeft, mdiCheckCircle } from '@mdi/js';
const router = useRouter();
const route = useRoute();
@@ -114,7 +113,7 @@ function goBack() {
content: '';
background: linear-gradient(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;
pointer-events: none;
}
@@ -125,7 +124,7 @@ function goBack() {
}
.link-button {
@apply flex items-center gap-2;
@apply flex items-center gap-2;
@apply text-hutopyPrimary hover:text-hutopySecondary;
@apply transition-colors;
@apply duration-300;

View File

@@ -4,11 +4,11 @@
<!-- Navigation Link at the top -->
<div class="navigation-link">
<button class="link-button" @click="goBack()">
<v-icon>mdi-arrow-left</v-icon>
<v-icon :icon="mdiArrowLeft" />
{{ t('returnToCreator') }}
</button>
</div>
<h1>{{ t('title') }}</h1>
<p>{{ t('message') }}</p>
</div>
@@ -18,12 +18,11 @@
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useBrandingStore } from '@/stores/brandingStore.js';
import { mdiArrowLeft } from '@mdi/js';
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const brandingStore = useBrandingStore();
function goBack() {
const creatorName = route.params.creator?.split('/')[0] || '';
@@ -82,7 +81,7 @@ function goBack() {
@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%);
mask: linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
}

View File

@@ -1,60 +1,31 @@
<template>
<div class="flex min-h-full justify-center items-center p-20 w-full">
<div class="flex min-h-full w-full items-center justify-center p-20">
<div class="card justify-items-center">
<img :alt="t('alt')"
src="/images/hutopymedia/loginpage/hutopylogin.svg"/>
<img :alt="t('alt')" src="/images/hutopymedia/loginpage/hutopylogin.svg" />
<div class="flex flex-col gap-10">
<h1 class="text-2xl font-bold login-text text-center ">
<h1 class="login-text text-center text-2xl font-bold ">
{{ t('title') }}
</h1>
<v-form @submit.prevent="handleRegister">
<div class="flex flex-col gap-4">
<v-text-field
v-model="name"
:label="t('name')"
required
></v-text-field>
<v-text-field v-model="name" :label="t('name')" required></v-text-field>
<v-text-field
v-model="email"
:label="t('email')"
type="email"
required
></v-text-field>
<v-text-field v-model="email" :label="t('email')" type="email" required></v-text-field>
<v-text-field
v-model="password"
:label="t('password')"
:type="showPassword ? 'text' : 'password'"
required
:hint="t('passwordRequirements')"
>
<v-text-field v-model="password" :label="t('password')" :type="showPassword ? 'text' : 'password'" required
:hint="t('passwordRequirements')">
<template v-slot:append-inner>
<v-icon
@click="showPassword = !showPassword"
class="visibility-toggle"
size="small"
>
{{ showPassword ? 'mdi-eye-off' : 'mdi-eye' }}
</v-icon>
<v-icon @click="showPassword = !showPassword" class="visibility-toggle" size="small"
:icon="showPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
<v-text-field
v-model="confirmPassword"
:label="t('confirmPassword')"
:type="showConfirmPassword ? 'text' : 'password'"
required
>
<v-text-field v-model="confirmPassword" :label="t('confirmPassword')"
:type="showConfirmPassword ? 'text' : 'password'" required>
<template v-slot:append-inner>
<v-icon
@click="showConfirmPassword = !showConfirmPassword"
class="visibility-toggle"
size="small"
>
{{ showConfirmPassword ? 'mdi-eye-off' : 'mdi-eye' }}
</v-icon>
<v-icon @click="showConfirmPassword = !showConfirmPassword" class="visibility-toggle" size="small"
:icon="showConfirmPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
@@ -62,7 +33,7 @@
{{ t('register') }}
</v-btn>
<div class="text-center mt-4">
<div class="mt-4 text-center">
{{ t('alreadyHaveAccount') }}
<router-link to="/login" class="text-blue-500">
{{ t('signIn') }}
@@ -85,6 +56,7 @@ import { useClient } from '@/plugins/api.js';
import { useAuthStore } from '@/stores/authStore.js';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { mdiEye, mdiEyeOff } from '@mdi/js';
const { t } = useI18n();
const router = useRouter();
@@ -192,4 +164,4 @@ async function handleRegister() {
"registrationFailed": "El registro falló. Por favor, inténtelo de nuevo."
}
}
</i18n>
</i18n>

View File

@@ -1,50 +1,32 @@
<template>
<div class="flex min-h-full justify-center items-center w-full p-4">
<div class="flex flex-col gap-10 w-full max-w-[512px]">
<h1 class="text-2xl font-bold text-center">
<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">
<h1 class="text-center text-2xl font-bold">
{{ t('title') }}
</h1>
<form @submit.prevent="handleResetPassword" class="card">
<div class="card-content">
<div class="flex flex-col gap-4">
<div class="form-field">
<label for="password" class="form-label">{{ t('newPassword') }}</label>
<div class="relative">
<input
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
class="form-input"
required
/>
<button
type="button"
@click="showPassword = !showPassword"
class="password-toggle"
>
<v-icon size="small">{{ showPassword ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
<input id="password" v-model="password" :type="showPassword ? 'text' : 'password'" class="form-input"
required />
<button type="button" @click="showPassword = !showPassword" class="password-toggle">
<v-icon size="small" :icon="showPassword ? mdiEyeOff : mdiEye" />
</button>
</div>
<p class="text-sm text-gray-500 mt-1">{{ t('passwordRequirements') }}</p>
<p class="mt-1 text-sm text-gray-500">{{ t('passwordRequirements') }}</p>
</div>
<div class="form-field">
<label for="confirmPassword" class="form-label">{{ t('confirmPassword') }}</label>
<div class="relative">
<input
id="confirmPassword"
v-model="confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
class="form-input"
required
/>
<button
type="button"
@click="showConfirmPassword = !showConfirmPassword"
class="password-toggle"
>
<v-icon size="small">{{ showConfirmPassword ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
<input id="confirmPassword" v-model="confirmPassword" :type="showConfirmPassword ? 'text' : 'password'"
class="form-input" required />
<button type="button" @click="showConfirmPassword = !showConfirmPassword" class="password-toggle">
<v-icon size="small" :icon="showConfirmPassword ? mdiEyeOff : mdiEye" />
</button>
</div>
</div>
@@ -53,18 +35,14 @@
{{ errorMessage }}
</div>
<button
type="submit"
class="primary w-full"
:disabled="isLoading"
>
<button type="submit" class="primary w-full" :disabled="isLoading">
<span v-if="isLoading" class="loading-spinner mr-2"></span>
{{ t('resetPassword') }}
</button>
</div>
</div>
</form>
<!-- Success message -->
<div v-if="success" class="success-message">
{{ t('passwordResetSuccess') }}
@@ -83,7 +61,7 @@ import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import { useClient } from '@/plugins/api.js';
import { VIcon } from 'vuetify/components';
import { mdiEye, mdiEyeOff } from '@mdi/js';
const { t } = useI18n();
const router = useRouter();
@@ -104,7 +82,7 @@ onMounted(() => {
// Get email and token from URL query parameters
email.value = route.query.email || '';
token.value = route.query.token || '';
// Validate that we have both email and token
if (!email.value || !token.value) {
errorMessage.value = t('invalidResetLink');
@@ -114,27 +92,27 @@ onMounted(() => {
async function handleResetPassword() {
// Reset error message
errorMessage.value = '';
// Validate passwords match
if (password.value !== confirmPassword.value) {
errorMessage.value = t('passwordsDoNotMatch');
return;
}
// Validate password length
if (password.value.length < 8) {
errorMessage.value = t('passwordTooShort');
return;
}
// Validate that we have email and token
if (!email.value || !token.value) {
errorMessage.value = t('invalidResetLink');
return;
}
isLoading.value = true;
try {
// Call password reset API
await clientApi.post('api/users/reset-password', {
@@ -142,14 +120,14 @@ async function handleResetPassword() {
token: token.value,
newPassword: password.value
});
// Show success message
success.value = true;
// Clear form fields
password.value = '';
confirmPassword.value = '';
// Redirect to login after a delay
setTimeout(() => {
router.push('/login');
@@ -181,14 +159,11 @@ async function handleResetPassword() {
}
.form-input {
@apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg
focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5
dark:bg-gray-700 dark:border-gray-600 dark:text-white;
@apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white;
}
.primary {
@apply bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg text-sm px-5 py-2.5
focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:opacity-50 disabled:cursor-not-allowed;
@apply bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg text-sm px-5 py-2.5 focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:opacity-50 disabled:cursor-not-allowed;
}
.error-message {

View File

@@ -10,7 +10,7 @@
<button v-if="!isEditMode" :title="t('edit')"
class="flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg"
@click="toggleEditMode()">
<v-icon large>mdi-pencil</v-icon>
<v-icon :icon="mdiPencil" large />
</button>
<!-- Save button -->
@@ -20,14 +20,14 @@
<v-progress-circular indeterminate size="20" width="2" color="white" />
</template>
<template v-else>
<v-icon>mdi-check</v-icon>
<v-icon :icon="mdiCheck" />
</template>
</button>
<!-- Cancel button -->
<button v-if="isEditMode" :title="t('cancel')"
class="flex size-12 items-center justify-center rounded-full bg-red-500 shadow-lg" @click="cancelEdit">
<v-icon large>mdi-close</v-icon>
<v-icon :icon="mdiClose" large />
</button>
</div>
@@ -112,6 +112,7 @@ import { buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId } from '@/utils/yo
import AlbumEditor from "@/views/creators/AlbumEditor.vue";
import AlbumView from "@/views/creators/AlbumView.vue";
import AlbumViewer from './AlbumViewer.vue';
import { mdiPencil, mdiCheck, mdiClose } from '@mdi/js';
const { t } = useI18n();
const creatorProfileStore = useCreatorProfileStore();

View File

@@ -1,50 +1,37 @@
<template>
<div class="relative">
<!-- Banner Container with mouse events -->
<div
class="relative rounded-b-2xl overflow-y-auto"
@mouseenter="showTint = isCurrentCreator"
@mouseleave="showTint = false"
@click="isCurrentCreator && openBannerEditor()"
>
<img
class="w-full aspect-[4/1] banner object-cover"
:src="brandingStore.value?.bannerUrl ?? '/images/placeholders/banner.png'"
:alt="t('alt')"
>
<div class="relative overflow-y-auto rounded-b-2xl" @mouseenter="showTint = isCurrentCreator"
@mouseleave="showTint = false" @click="isCurrentCreator && openBannerEditor()">
<img class="banner aspect-[4/1] w-full object-cover"
:src="brandingStore.value?.bannerUrl ?? '/images/placeholders/banner.png'" :alt="t('alt')">
<!-- Tint Effect -->
<div
v-if="showTint"
class="absolute inset-0 bg-black/25 cursor-pointer"
>
<div v-if="showTint" class="absolute inset-0 cursor-pointer bg-black/25">
<!-- Top-right Icon -->
<div
class="absolute top-4 right-4 w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
>
<v-icon large>mdi-pencil</v-icon>
class="absolute right-4 top-4 flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg">
<v-icon large :icon="mdiPencil" />
</div>
</div>
</div>
</div>
<v-dialog v-model="isDialogOpen" max-width="800px">
<BannerEditor
:creator="brandingStore.value"
@closeRequested="() => isDialogOpen = false"
/>
<BannerEditor :creator="brandingStore.value" @closeRequested="() => isDialogOpen = false" />
</v-dialog>
</template>
<script setup>
import BannerEditor from "@/views/creators/BannerEditor.vue";
import {computed, ref} from "vue";
import {useBrandingStore} from "@/stores/brandingStore.js";
import {useAuthStore} from "@/stores/authStore.js";
import {useI18n} from 'vue-i18n';
import { computed, ref } from "vue";
import { useBrandingStore } from "@/stores/brandingStore.js";
import { useAuthStore } from "@/stores/authStore.js";
import { useI18n } from 'vue-i18n';
import { mdiPencil } from '@mdi/js';
const authStore = useAuthStore();
const brandingStore = useBrandingStore();
const {t} = useI18n();
const { t } = useI18n();
// State
const showTint = ref(false);
@@ -60,8 +47,7 @@ const isCurrentCreator = computed(() => {
});
</script>
<style scoped>
</style>
<style scoped></style>
<i18n>
{
@@ -75,4 +61,4 @@ const isCurrentCreator = computed(() => {
"alt": "Banner del creador"
}
}
</i18n>
</i18n>

View File

@@ -10,7 +10,7 @@
<!-- Upload prompt -->
<div class="drop-zone-content">
<v-icon size="large">mdi-plus</v-icon>
<v-icon size="large" :icon="mdiPlus" />
<span class="mt-2 text-sm">{{ t('dropzoneText') }}</span>
</div>
@@ -37,17 +37,17 @@
<!-- Left arrow -->
<button @click.stop="moveImage(index, 'up')" @touchstart.stop="moveImage(index, 'up')"
class="action-btn left-btn" :disabled="index === 0" :title="t('moveLeft')">
<v-icon>mdi-arrow-left</v-icon>
<v-icon :icon="mdiArrowLeft" />
</button>
<!-- Right arrow -->
<button @click.stop="moveImage(index, 'down')" @touchstart.stop="moveImage(index, 'down')"
class="action-btn right-btn" :disabled="index === localImages.length - 1" :title="t('moveRight')">
<v-icon>mdi-arrow-right</v-icon>
<v-icon :icon="mdiArrowRight" />
</button>
<!-- Delete button -->
<button @click.stop="deleteImage(index)" touchstart.stop="deleteImage(index)" class="action-btn delete-btn"
:title="t('delete')">
<v-icon>mdi-delete</v-icon>
<v-icon :icon="mdiDelete" />
</button>
</div>
</template>
@@ -62,6 +62,7 @@ import { ref, onMounted } from "vue";
import { useI18n } from 'vue-i18n';
import { v7 } from 'uuid';
import draggable from 'vuedraggable';
import { mdiArrowLeft, mdiArrowRight, mdiDelete, mdiPlus } from '@mdi/js';
const props = defineProps({
images: {

View File

@@ -1,46 +1,25 @@
<template>
<v-dialog
v-model="dialog"
fullscreen
:scrim="true"
transition="dialog-bottom-transition"
@click:outside="closeViewer"
>
<v-dialog v-model="dialog" fullscreen :scrim="true" transition="dialog-bottom-transition"
@click:outside="closeViewer">
<div class="album-viewer" @click.self="closeViewer">
<!-- Main image container -->
<div class="image-container">
<img
:src="currentImage"
:alt="t('viewer.imageAlt', { index: currentIndex + 1 })"
class="main-image"
/>
<img :src="currentImage" :alt="t('viewer.imageAlt', { index: currentIndex + 1 })" class="main-image" />
<!-- Navigation buttons -->
<button
class="nav-btn left-btn"
@click.stop="previousImage"
:disabled="currentIndex === 0"
:title="t('viewer.previous')"
>
<v-icon size="large" color="white">mdi-chevron-left</v-icon>
<button class="nav-btn left-btn" @click.stop="previousImage" :disabled="currentIndex === 0"
:title="t('viewer.previous')">
<v-icon size="large" color="white" :icon="mdiChevronLeft" />
</button>
<button
class="nav-btn right-btn"
@click.stop="nextImage"
:disabled="currentIndex === images.length - 1"
:title="t('viewer.next')"
>
<v-icon size="large" color="white">mdi-chevron-right</v-icon>
<button class="nav-btn right-btn" @click.stop="nextImage" :disabled="currentIndex === images.length - 1"
:title="t('viewer.next')">
<v-icon size="large" color="white" :icon="mdiChevronRight" />
</button>
<!-- Close button -->
<button
class="close-btn"
@click.stop="closeViewer"
:title="t('viewer.close')"
>
<v-icon size="large" color="white">mdi-close</v-icon>
<button class="close-btn" @click.stop="closeViewer" :title="t('viewer.close')">
<v-icon size="large" color="white" :icon="mdiClose" />
</button>
<!-- Image counter -->
@@ -55,6 +34,7 @@
<script setup>
import { ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { mdiChevronLeft, mdiChevronRight, mdiClose } from '@mdi/js';
const { t } = useI18n();

View File

@@ -1,31 +1,18 @@
<template>
<div class="relative"
@mouseenter="showTint = isCurrentCreator"
@mouseleave="showTint = false"
@click="isCurrentCreator && openBannerEditor()"
>
<div class="relative" @mouseenter="showTint = isCurrentCreator" @mouseleave="showTint = false"
@click="isCurrentCreator && openBannerEditor()">
<div class="rounded-full border-4 border-hPrimary w-[110px] h-[110px]">
<img
:src="brandingStore.value?.portraitUrl ?? '/images/placeholders/profile.png'"
:alt="t('logoAlt')"
width="110px"
height="110px"
class="rounded-full"
/>
<div class="size-[110px] rounded-full border-4 border-hPrimary">
<img :src="brandingStore.value?.portraitUrl ?? '/images/placeholders/profile.png'" :alt="t('logoAlt')"
width="110px" height="110px" class="rounded-full" />
</div>
<!-- Tint Effect -->
<div
v-if="showTint"
class="absolute rounded-full inset-0 bg-black/25 cursor-pointer"
:title="t('editLogo')"
>
<div v-if="showTint" class="absolute inset-0 cursor-pointer rounded-full bg-black/25" :title="t('editLogo')">
<!-- Top-right Icon -->
<div
class="absolute top-0 right-0 w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
>
<v-icon large>mdi-pencil</v-icon>
class="absolute right-0 top-0 flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg">
<v-icon large :icon="mdiPencil" />
</div>
</div>
@@ -33,21 +20,20 @@
<v-dialog v-model="isDialogOpen" max-width="800px">
<template #default="{ close }">
<creator-logo-editor
:creator="brandingStore?.value"
@closeRequested="() => isDialogOpen = false"
></creator-logo-editor>
<creator-logo-editor :creator="brandingStore?.value"
@closeRequested="() => isDialogOpen = false"></creator-logo-editor>
</template>
</v-dialog>
</template>
<script setup>
import {useAuthStore} from "@/stores/authStore.js";
import {useBrandingStore} from "@/stores/brandingStore.js";
import { useAuthStore } from "@/stores/authStore.js";
import { useBrandingStore } from "@/stores/brandingStore.js";
import CreatorLogoEditor from "@/views/creators/CreatorLogoEditor.vue";
import {computed, ref} from "vue";
import { computed, ref } from "vue";
import { useI18n } from 'vue-i18n'
import { mdiPencil } from '@mdi/js';
const authStore = useAuthStore();
const brandingStore = useBrandingStore();
@@ -72,7 +58,6 @@ const isCurrentCreator = computed(() => {
.logo-image {
@apply border-4 border-solid border-hTertiary;
}
</style>
<i18n>
@@ -90,4 +75,4 @@ const isCurrentCreator = computed(() => {
"editLogo": "Editar logo"
}
}
</i18n>
</i18n>

View File

@@ -1,9 +1,10 @@
<script setup>
import {ref, onMounted, onUnmounted, computed} from "vue";
import {v7} from "uuid";
import {useClient} from "@/plugins/api.js";
import {useI18n} from 'vue-i18n';
import { ref, onMounted, onUnmounted, computed } from "vue";
import { v7 } from "uuid";
import { useClient } from "@/plugins/api.js";
import { useI18n } from 'vue-i18n';
import config from '@/config';
import { mdiCheckCircle, mdiCloseCircle } from '@mdi/js';
const props = defineProps({
name: {
@@ -24,7 +25,7 @@ const emits = defineEmits([
]);
const name = ref(props.name);
const {t} = useI18n();
const { t } = useI18n();
const isOperationPending = ref(false);
const reservationState = ref(null);
@@ -129,9 +130,9 @@ const checkNameAvailability = async (nameToCheck) => {
currentController = controller;
await client.post(
`/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`,
{reservationId: reservationId.value},
{signal: controller.signal}
`/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`,
{ reservationId: reservationId.value },
{ signal: controller.signal }
);
// Only process the response if this is still the current request
@@ -162,35 +163,23 @@ onUnmounted(() => {
</script>
<template>
<v-text-field
variant="outlined"
:label="t('creator.name.label')"
v-model="name"
@input="handleInput"
:error-messages="validationError"
>
<v-text-field variant="outlined" :label="t('creator.name.label')" v-model="name" @input="handleInput"
:error-messages="validationError">
<template #prepend-inner>
<span class="text-gray-400 font-sans">{{ baseUrl }}</span>
<span class="font-sans text-gray-400">{{ baseUrl }}</span>
</template>
<template #append-inner>
<v-progress-circular
v-if="reservationState === 'loading'"
indeterminate
size="24"
width="3"
color="grey"
></v-progress-circular>
<v-progress-circular v-if="reservationState === 'loading'" indeterminate size="24" width="3"
color="grey"></v-progress-circular>
<v-icon v-else-if="reservationState === 'reserved'" color="green">mdi-check-circle</v-icon>
<v-icon v-else-if="reservationState === 'unavailable'" color="red">mdi-close-circle</v-icon>
<v-icon v-else-if="reservationState === 'reserved'" color="green" :icon="mdiCheckCircle" />
<v-icon v-else-if="reservationState === 'unavailable'" color="red" :icon="mdiCloseCircle" />
</template>
</v-text-field>
</template>
<style scoped>
</style>
<style scoped></style>
<i18n>
{
@@ -228,4 +217,4 @@ onUnmounted(() => {
}
}
}
</i18n>
</i18n>

View File

@@ -1,14 +1,14 @@
<script setup>
import {useI18n} from "vue-i18n";
import {useAuthStore} from "@/stores/authStore.js";
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
import {useUserProfileStore} from "@/stores/userProfileStore.js";
import {useLanguageStore} from "@/stores/languageStore.js";
import {useRouter, useRoute} from 'vue-router';
import { useI18n } from "vue-i18n";
import { useAuthStore } from "@/stores/authStore.js";
import { useCreatorProfileStore } from "@/stores/creatorProfileStore.js";
import { useUserProfileStore } from "@/stores/userProfileStore.js";
import { useLanguageStore } from "@/stores/languageStore.js";
import { useRoute } from 'vue-router';
import { mdiFileAccountOutline, mdiAccount, mdiLogin, mdiTranslateVariant, mdiLogout } from '@mdi/js';
const {locale, t} = useI18n();
const { locale, t } = useI18n();
const languageStore = useLanguageStore();
const router = useRouter();
const route = useRoute();
const userProfileStore = useUserProfileStore();
@@ -36,20 +36,14 @@ function handleLogout() {
<div class="side-logo">
<router-link to="/@hutopy">
<img src="/images/hutopy-logo.png"
alt="hutopy logo"
height="50">
<img src="/images/hutopy-logo.png" alt="hutopy logo" height="50">
</router-link>
</div>
<div class="side-menu">
<div v-if="authStore.isAuthenticated"
class="side-menu-portrait">
<img :src="userProfileStore.portraitUrl"
alt="Profile Image"
referrerpolicy="no-referrer"
class="rounded-full">
<div v-if="authStore.isAuthenticated" class="side-menu-portrait">
<img :src="userProfileStore.portraitUrl" alt="Profile Image" referrerpolicy="no-referrer" class="rounded-full">
<span class="profile-label">{{ userProfileStore.alias }}</span>
</div>
@@ -58,13 +52,13 @@ function handleLogout() {
<template v-if="authStore.isAuthenticated">
<router-link v-if="creatorProfileStore.hasCreator" :to="`/@${creatorProfileStore.creator.slug}`">
<button class="menu-item-action">
<i class="mdi mdi-file-account-outline"></i>
<v-icon :icon="mdiFileAccountOutline" />
<span class="label">{{ t('sidebar.myPage') }}</span>
</button>
</router-link>
<router-link v-else to="/create-creator">
<button class="menu-item-action">
<i class="mdi mdi-file-account-outline"></i>
<v-icon :icon="mdiFileAccountOutline" />
<span class="label">{{ t('sidebar.myPage') }}</span>
</button>
</router-link>
@@ -73,31 +67,29 @@ function handleLogout() {
<template v-if="authStore.isAuthenticated">
<router-link to="/profile">
<button class="menu-item-action">
<i class="mdi mdi-account"></i>
<v-icon :icon="mdiAccount" />
<span class="label">{{ t('sidebar.myProfile') }}</span>
</button>
</router-link>
</template>
<button class="menu-item-action"
@click="toggleLanguage">
<i class="mdi mdi-translate-variant"></i>
<button class="menu-item-action" @click="toggleLanguage">
<v-icon :icon="mdiTranslateVariant" />
<span class="label">{{ locale }}</span>
</button>
<template v-if="!authStore.isAuthenticated">
<router-link to="/login">
<button class="menu-item-action">
<i class="mdi mdi-login"></i>
<v-icon :icon="mdiLogin" />
<span class="label">{{ t('sidebar.signIn') }}</span>
</button>
</router-link>
</template>
<div v-else>
<button class="menu-item-action"
@click="handleLogout">
<i class="mdi mdi-logout"></i>
<button class="menu-item-action" @click="handleLogout">
<v-icon :icon="mdiLogout" />
<span class="label">{{ t('sidebar.signOut') }}</span>
</button>
</div>
@@ -108,7 +100,6 @@ function handleLogout() {
</template>
<style scoped>
.side-container {
@apply bg-hSurface text-hOnSurface;
@apply lg:fixed lg:max-h-screen;
@@ -168,7 +159,6 @@ function handleLogout() {
@apply text-xl;
}
}
</style>
<i18n>
@@ -198,4 +188,4 @@ function handleLogout() {
}
}
}
</i18n>
</i18n>

View File

@@ -1,9 +1,8 @@
<script setup>
import {ref, markRaw} from 'vue';
import {useCreatorProfileStore} from '@/stores/creatorProfileStore.js';
import {useUserProfileStore} from "@/stores/userProfileStore.js";
import {useClient} from '@/plugins/api.js';
import {useRouter} from 'vue-router';
import { ref, markRaw } from 'vue';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
import { useUserProfileStore } from "@/stores/userProfileStore.js";
import { useClient } from '@/plugins/api.js';
import SocialsDialog from './creators/SocialsDialog.vue';
import AliasDialog from "@/views/profile/account/AliasDialog.vue";
import FullnameDialog from "@/views/profile/account/FullnameDialog.vue";
@@ -23,11 +22,11 @@ import Linkedin from "@/views/svg/Linkedin.vue";
import Tiktok from "@/views/svg/Tiktok.vue";
import Instagram from "@/views/svg/Instagram.vue";
import Facebook from "@/views/svg/Facebook.vue";
import {useI18n} from 'vue-i18n';
import { useI18n } from 'vue-i18n';
import QRCodeVue from 'qrcode.vue';
import { mdiChevronRight, mdiCreditCard, mdiCreditCardOff, mdiDownload, mdiCheck, mdiContentCopy } from '@mdi/js';
const {t} = useI18n();
const router = useRouter();
const { t } = useI18n();
const userProfileStore = useUserProfileStore()
const creatorProfileStore = useCreatorProfileStore();
const baseURL = window.location.origin;
@@ -143,10 +142,10 @@ function handleDelete() {
function downloadQRCode() {
try {
// Get the SVG element
const svgElement = document.querySelector('.qr-code svg') ||
document.querySelector('.qr-container svg') ||
document.querySelector('svg[class*="qr"]');
const svgElement = document.querySelector('.qr-code svg') ||
document.querySelector('.qr-container svg') ||
document.querySelector('svg[class*="qr"]');
if (!svgElement) {
console.error('QR code SVG element not found');
return;
@@ -162,43 +161,43 @@ function downloadQRCode() {
img.onload = () => {
// Set padding
const padding = 20; // 20 pixels padding on each side
// Create canvas with padding
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas size to include padding
canvas.width = img.width + (padding * 2);
canvas.height = img.height + (padding * 2);
// Fill white background for entire canvas (including padding)
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw the image with padding offset
ctx.drawImage(img, padding, padding);
// Convert to PNG
const pngUrl = canvas.toDataURL('image/png');
// Create download link
const downloadLink = document.createElement('a');
downloadLink.href = pngUrl;
downloadLink.download = `hutopy-qr-${creatorProfileStore.creator.slug}.png`;
// Trigger download
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
// Cleanup
URL.revokeObjectURL(svgUrl);
};
img.onerror = (error) => {
console.error('Error loading SVG:', error);
};
// Load the SVG into the image
img.src = svgUrl;
} catch (error) {
@@ -221,7 +220,7 @@ async function deconfigureStripe() {
<template>
<div class="min-h-screen w-full">
<div class="flex flex-col items-center gap-4 m-4">
<div class="m-4 flex flex-col items-center gap-4">
<div class="card">
@@ -230,20 +229,20 @@ async function deconfigureStripe() {
</div>
<div class="content">
<button
class="action"
@click="openEditFullname">
<button class="action" @click="openEditFullname">
<span class="label">{{ t('fullName') }}</span>
<span class="value">{{ userProfileStore.fullname }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openEditAlias">
<button class="action" @click="openEditAlias">
<span class="label">{{ t('alias') }}</span>
<span class="value">{{ userProfileStore.user.alias }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
@@ -251,7 +250,9 @@ async function deconfigureStripe() {
<button class="action" @click="openDialog('EmailDialog')">
<span class="label">{{ t('email') }}</span>
<span class="value">{{ userProfileStore.user.email }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
@@ -259,10 +260,12 @@ async function deconfigureStripe() {
<button class="action" @click="openDialog('ChangePasswordDialog')">
<span class="label">{{ t('changePassword') }}</span>
<span class="value"></span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
</div>
<template v-if="creatorProfileStore.hasCreator">
@@ -275,29 +278,31 @@ async function deconfigureStripe() {
<div class="action" @click="openDialog('ChangeSlugDialog')">
<span class="label">{{ t('handle') }}</span>
<span class="value">{{ baseURL }}/@{{ creatorProfileStore.creator.slug }}</span>
<button
ref="copyButtonRef"
class="copy-button"
@click.stop="copyCreatorUrl"
:class="{ 'success': copySuccess }"
>
<v-icon>{{ copySuccess ? 'mdi-check' : 'mdi-content-copy' }}</v-icon>
<button ref="copyButtonRef" class="copy-button" @click.stop="copyCreatorUrl"
:class="{ 'success': copySuccess }">
<v-icon :icon="copySuccess ? mdiCheck : mdiContentCopy" />
</button>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</div>
<!-- NAME -->
<button class="action" @click="openDialog('ChangeNameDialog')">
<span class="label">{{ t('name') }}</span>
<span class="value">{{ creatorProfileStore.creator.name }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<!-- TITLE -->
<button class="action" @click="openDialog('ChangeTitleDialog')">
<span class="label">{{ t('title') }}</span>
<span class="value">{{ creatorProfileStore.creator.title }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<!-- PHONE NUMBER -->
@@ -306,7 +311,9 @@ async function deconfigureStripe() {
<span class="value" :class="{ 'not-set': !creatorProfileStore.creator.presentation?.phoneNumber }">
{{ creatorProfileStore.creator.presentation?.phoneNumber || t('notSet') }}
</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<!-- EMAIL -->
@@ -315,9 +322,10 @@ async function deconfigureStripe() {
<span class="value" :class="{ 'not-set': !creatorProfileStore.creator.presentation?.email }">
{{ creatorProfileStore.creator.presentation?.email || t('notSet') }}
</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
</div>
@@ -335,13 +343,12 @@ async function deconfigureStripe() {
</span>
<div class="stripe-actions">
<button class="configure-stripe-button" @click="openDialog('ChangeStripeIdDialog')">
<v-icon>mdi-credit-card</v-icon>
<v-icon :icon="mdiCreditCard" />
{{ t('configureStripe') }}
</button>
<button v-if="creatorProfileStore.creator.acceptDonation"
class="deconfigure-stripe-button"
@click="deconfigureStripe">
<v-icon>mdi-credit-card-off</v-icon>
<button v-if="creatorProfileStore.creator.acceptDonation" class="deconfigure-stripe-button"
@click="deconfigureStripe">
<v-icon :icon="mdiCreditCardOff"/>
{{ t('deconfigureStripe') }}
</button>
</div>
@@ -361,7 +368,9 @@ async function deconfigureStripe() {
<facebook class="social-icon"></facebook>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.facebookUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button class="action" @click="openDialog('SocialsDialog')">
@@ -369,7 +378,9 @@ async function deconfigureStripe() {
<instagram class="social-icon"></instagram>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.instagramUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button class="action" @click="openDialog('SocialsDialog')">
@@ -377,7 +388,9 @@ async function deconfigureStripe() {
<x class="social-icon"></x>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.xUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button class="action" @click="openDialog('SocialsDialog')">
@@ -385,7 +398,9 @@ async function deconfigureStripe() {
<linkedin class="social-icon"></linkedin>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.linkedInUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button class="action" @click="openDialog('SocialsDialog')">
@@ -393,7 +408,9 @@ async function deconfigureStripe() {
<tiktok class="social-icon"></tiktok>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.tikTokUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button class="action" @click="openDialog('SocialsDialog')">
@@ -401,7 +418,9 @@ async function deconfigureStripe() {
<youtube class="social-icon"></youtube>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.youtubeUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button class="action" @click="openDialog('SocialsDialog')">
@@ -409,7 +428,9 @@ async function deconfigureStripe() {
<reddit class="social-icon"></reddit>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.redditUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button class="action" @click="openDialog('SocialsDialog')">
@@ -417,7 +438,9 @@ async function deconfigureStripe() {
<web class="social-icon"></web>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.websiteUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
@@ -430,20 +453,10 @@ async function deconfigureStripe() {
<div class="content">
<div class="qr-container">
<p class="qr-text">{{ t('qrCodeDescription') }}</p>
<QRCodeVue
v-if="creatorProfileStore.creator"
:value="baseURL + '/@' + creatorProfileStore.creator.slug"
:size="200"
level="H"
render-as="svg"
class="qr-code"
/>
<button
v-if="creatorProfileStore.creator"
class="download-button"
@click="downloadQRCode"
>
<v-icon>mdi-download</v-icon>
<QRCodeVue v-if="creatorProfileStore.creator" :value="baseURL + '/@' + creatorProfileStore.creator.slug"
:size="200" level="H" render-as="svg" class="qr-code" />
<button v-if="creatorProfileStore.creator" class="download-button" @click="downloadQRCode">
<v-icon :icon="mdiDownload" />
{{ t('downloadQRCode') }}
</button>
</div>
@@ -459,14 +472,11 @@ async function deconfigureStripe() {
{{ t('dangerZoneWarning') }}
</span>
<div class="p-2">
<button v-if="!creatorProfileStore.creator.isDeleted"
class="primary danger-action"
@click="deleteDialogShown = true">
<button v-if="!creatorProfileStore.creator.isDeleted" class="primary danger-action"
@click="deleteDialogShown = true">
{{ t('deleteCreatorPage') }}
</button>
<button v-else
class="primary safe-action"
@click="restoreDialogShown = true">
<button v-else class="primary safe-action" @click="restoreDialogShown = true">
{{ t('restoreCreatorPage') }}
</button>
</div>
@@ -478,27 +488,16 @@ async function deconfigureStripe() {
</div>
<v-dialog v-model="dialogEditFullnameShown">
<FullnameDialog
:firstname="userProfileStore.user.firstname"
:lastname="userProfileStore.user.lastname"
@close="handleCloseEditFullname"
@save="handleSaveEditFullname"
/>
<FullnameDialog :firstname="userProfileStore.user.firstname" :lastname="userProfileStore.user.lastname"
@close="handleCloseEditFullname" @save="handleSaveEditFullname" />
</v-dialog>
<v-dialog v-model="dialogEditAliasShown">
<alias-dialog
:alias="userProfileStore.user.alias"
@close="handleCloseEditAlias"
@save="handleSaveEditAlias"
></alias-dialog>
<alias-dialog :alias="userProfileStore.user.alias" @close="handleCloseEditAlias"
@save="handleSaveEditAlias"></alias-dialog>
</v-dialog>
<v-dialog v-model="dialogShown">
<component
:is="currentComponent"
:creator="creatorProfileStore.creator"
:email="userProfileStore.user.email"
@closeRequested="closeDialog"
></component>
<component :is="currentComponent" :creator="creatorProfileStore.creator" :email="userProfileStore.user.email"
@closeRequested="closeDialog"></component>
</v-dialog>
<v-dialog v-model="restoreDialogShown">
<div class="card dialog">
@@ -760,4 +759,4 @@ async function deconfigureStripe() {
"deconfigureStripe": "Eliminar Stripe"
}
}
</i18n>
</i18n>

View File

@@ -1,47 +1,26 @@
<template>
<div class="card dialog">
<div class="card-title">
{{ t('changePassword') }}
</div>
<div class="card-content">
<p class="description mb-4">{{ t('passwordDescription') }}</p>
<v-text-field
v-model="newPassword"
:label="t('newPassword')"
:type="showNewPassword ? 'text' : 'password'"
variant="outlined"
required
:hint="t('passwordRequirements')"
>
<v-text-field v-model="newPassword" :label="t('newPassword')" :type="showNewPassword ? 'text' : 'password'"
variant="outlined" required :hint="t('passwordRequirements')">
<template v-slot:append-inner>
<v-icon
@click="showNewPassword = !showNewPassword"
class="visibility-toggle"
size="small"
>
{{ showNewPassword ? 'mdi-eye-off' : 'mdi-eye' }}
</v-icon>
<v-icon @click="showNewPassword = !showNewPassword" class="visibility-toggle" size="small"
:icon="showNewPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
<v-text-field
v-model="confirmPassword"
:label="t('confirmPassword')"
:type="showConfirmPassword ? 'text' : 'password'"
variant="outlined"
required
>
<v-text-field v-model="confirmPassword" :label="t('confirmPassword')"
:type="showConfirmPassword ? 'text' : 'password'" variant="outlined" required>
<template v-slot:append-inner>
<v-icon
@click="showConfirmPassword = !showConfirmPassword"
class="visibility-toggle"
size="small"
>
{{ showConfirmPassword ? 'mdi-eye-off' : 'mdi-eye' }}
</v-icon>
<v-icon @click="showConfirmPassword = !showConfirmPassword" class="visibility-toggle" size="small"
:icon="showNewPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
@@ -58,7 +37,7 @@
</button>
</div>
</div>
</div>
</template>
@@ -81,7 +60,7 @@ const showConfirmPassword = ref(false);
async function handleChangePassword() {
// Clear previous error
errorMessage.value = '';
// Validate passwords match
if (newPassword.value !== confirmPassword.value) {
errorMessage.value = t('passwordsDoNotMatch');
@@ -100,10 +79,10 @@ async function handleChangePassword() {
// Pass empty string for current password since we're already authenticated
// This will use the set-password endpoint for OAuth users
await authStore.changePassword(newPassword.value);
// Success - close dialog
emit('closeRequested');
// You could also emit a success event if needed
// emit('success');
} catch (error) {
@@ -117,7 +96,6 @@ async function handleChangePassword() {
</script>
<style scoped>
.dialog {
@apply max-w-md mx-auto;
}
@@ -138,7 +116,6 @@ async function handleChangePassword() {
:deep(.v-field__append-inner) {
padding-inline-start: 0;
}
</style>
<i18n>
@@ -181,4 +158,4 @@ async function handleChangePassword() {
}
}
</i18n>
</i18n>