refactor(auth): cleanup auth module and streamline the registration flow

This commit is contained in:
2025-06-18 16:50:11 -04:00
parent 25b94d3e02
commit cdcfe8d7e2
24 changed files with 2140 additions and 1387 deletions

View File

@@ -1,167 +0,0 @@
<template>
<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" />
<div class="flex flex-col gap-10">
<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="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')">
<template v-slot:append-inner>
<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>
<template v-slot:append-inner>
<v-icon @click="showConfirmPassword = !showConfirmPassword" class="visibility-toggle" size="small"
:icon="showConfirmPassword ? mdiEyeOff : mdiEye" />
</template>
</v-text-field>
<v-btn type="submit" color="primary" block :loading="isLoading">
{{ t('register') }}
</v-btn>
<div class="mt-4 text-center">
{{ t('alreadyHaveAccount') }}
<router-link to="/login" class="text-blue-500">
{{ t('signIn') }}
</router-link>
</div>
</div>
</v-form>
</div>
</div>
<v-snackbar v-model="errorSnackBar" color="error">
{{ errorMessage }}
</v-snackbar>
</div>
</template>
<script setup>
import { ref } from 'vue';
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();
const authStore = useAuthStore();
const clientApi = useClient();
const name = ref('');
const email = ref('');
const password = ref('');
const confirmPassword = ref('');
const isLoading = ref(false);
const errorSnackBar = ref(false);
const errorMessage = ref('');
const showPassword = ref(false);
const showConfirmPassword = ref(false);
async function handleRegister() {
if (password.value !== confirmPassword.value) {
errorMessage.value = t('passwordsDoNotMatch');
errorSnackBar.value = true;
return;
}
isLoading.value = true;
try {
// Register the user
const response = await clientApi.post('api/users/register', {
name: name.value,
email: email.value.trim(),
password: password.value
});
// If registration is successful, log them in
await authStore.login(email.value, password.value);
// Redirect to home or welcome page
await router.push('/landing');
} catch (error) {
console.error('Registration failed:', error);
errorMessage.value = error.response?.data?.message || t('registrationFailed');
errorSnackBar.value = true;
} finally {
isLoading.value = false;
}
}
</script>
<style scoped>
.visibility-toggle {
@apply cursor-pointer;
@apply transition-opacity duration-300;
@apply opacity-60 hover:opacity-100;
@apply z-10;
}
/* Override Vuetify's default padding to accommodate our icon */
:deep(.v-field__append-inner) {
padding-inline-start: 0;
}
</style>
<i18n>
{
"en": {
"title": "Create your account",
"alt": "Hutopy Registration",
"name": "Full Name",
"email": "Email",
"password": "Password",
"confirmPassword": "Confirm Password",
"passwordRequirements": "Password must be at least 8 characters",
"register": "Register",
"alreadyHaveAccount": "Already have an account?",
"signIn": "Sign in",
"passwordsDoNotMatch": "Passwords do not match",
"registrationFailed": "Registration failed. Please try again."
},
"fr": {
"title": "Créer votre compte",
"alt": "Inscription Hutopy",
"name": "Nom complet",
"email": "Email",
"password": "Mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères",
"register": "S'inscrire",
"alreadyHaveAccount": "Vous avez déjà un compte?",
"signIn": "Se connecter",
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
"registrationFailed": "L'inscription a échoué. Veuillez réessayer."
},
"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."
}
}
</i18n>

View File

@@ -44,6 +44,12 @@
</a>
</div>
<div class="mt-2 text-center">
<a @click="resendVerification" class="cursor-pointer text-sm text-blue-500">
{{ t('resendVerification') }}
</a>
</div>
<div class="mt-4 text-center">
{{ t('noAccount') }}
<router-link to="/register" class="text-blue-500">
@@ -113,6 +119,10 @@ async function googleCallback(token) {
function forgotPassword() {
router.push('/forgot-password');
}
function resendVerification() {
router.push('/verify-email');
}
</script>
<style scoped>
@@ -146,6 +156,7 @@ function forgotPassword() {
"password": "Password",
"signIn": "Connect",
"forgotPassword": "Forgot password?",
"resendVerification": "Resend verification email",
"orContinueWith": "Or",
"noAccount": "Don't have an account?",
"register": "Register",
@@ -159,6 +170,7 @@ function forgotPassword() {
"password": "Mot de passe",
"signIn": "Connexion",
"forgotPassword": "Mot de passe oublié?",
"resendVerification": "Renvoyer l'email de vérification",
"orContinueWith": "Ou",
"noAccount": "Vous n'avez pas de compte?",
"register": "S'inscrire",
@@ -172,6 +184,7 @@ function forgotPassword() {
"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",

View File

@@ -0,0 +1,257 @@
<template>
<div class="flex min-h-full w-full items-center justify-center p-20">
<!-- Show verification message on success -->
<div
v-if="registrationSuccess"
class="card justify-items-center"
>
<img
:alt="t('alt')"
src="/images/hutopymedia/loginpage/hutopylogin.svg"
/>
<div class="flex flex-col gap-10 text-center">
<h1 class="login-text text-2xl font-bold text-green-600">
{{ t('success.title') }}
</h1>
<div class="text-hOnSurface">
<p>{{ t('success.message') }}</p>
<p class="mt-2 font-medium">{{ userEmail }}</p>
</div>
<div class="mt-4 flex flex-col gap-2">
<router-link
class="text-blue-500 hover:underline"
to="/login"
>
{{ t('success.backToLogin') }}
</router-link>
<router-link
class="text-blue-500 hover:underline"
:to="{ path: '/verify-email', query: { email: userEmail } }"
>
{{ t('success.resendVerification') }}
</router-link>
</div>
</div>
</div>
<!-- Show registration form -->
<div
v-else
class="card justify-items-center"
>
<img
:alt="t('alt')"
src="/images/hutopymedia/loginpage/hutopylogin.svg"
/>
<div class="flex flex-col gap-10">
<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="email"
:label="t('email')"
required
type="email"
></v-text-field>
<v-text-field
v-model="password"
:hint="t('passwordRequirements')"
:label="t('password')"
:type="showPassword ? 'text' : 'password'"
required
>
<template v-slot:append-inner>
<v-icon
:icon="showPassword ? mdiEyeOff : mdiEye"
class="visibility-toggle"
size="small"
@click="showPassword = !showPassword"
/>
</template>
</v-text-field>
<v-text-field
v-model="confirmPassword"
:label="t('confirmPassword')"
:type="showConfirmPassword ? 'text' : 'password'"
required
>
<template v-slot:append-inner>
<v-icon
:icon="showConfirmPassword ? mdiEyeOff : mdiEye"
class="visibility-toggle"
size="small"
@click="showConfirmPassword = !showConfirmPassword"
/>
</template>
</v-text-field>
<v-btn
:loading="isLoading"
block
color="primary"
type="submit"
>
{{ t('register') }}
</v-btn>
<!-- Error message displayed as block text below submit button -->
<div
v-if="errorMessage"
class="mt-2 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm"
>
{{ errorMessage }}
</div>
<div class="mt-4 text-center">
{{ t('alreadyHaveAccount') }}
<router-link
class="text-blue-500"
to="/login"
>
{{ t('signIn') }}
</router-link>
</div>
</div>
</v-form>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useClient } from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
import { mdiEye, mdiEyeOff } from '@mdi/js';
const { t } = useI18n();
const clientApi = useClient();
const name = ref('');
const email = ref('');
const password = ref('');
const confirmPassword = ref('');
const isLoading = ref(false);
const errorMessage = ref('');
const showPassword = ref(false);
const showConfirmPassword = ref(false);
const registrationSuccess = ref(false);
const userEmail = ref('');
async function handleRegister() {
if (password.value !== confirmPassword.value) {
errorMessage.value = t('passwordsDoNotMatch');
return;
}
isLoading.value = true;
errorMessage.value = '';
try {
await clientApi.post('api/users/register', {
name: name.value,
email: email.value.trim(),
password: password.value,
});
// On success, show verification message
userEmail.value = email.value.trim();
registrationSuccess.value = true;
} catch (error) {
console.error('Registration failed:', error);
errorMessage.value = error.response?.data?.message || t('registrationFailed');
} finally {
isLoading.value = false;
}
}
</script>
<style scoped>
.visibility-toggle {
@apply cursor-pointer;
@apply transition-opacity duration-300;
@apply opacity-60 hover:opacity-100;
@apply z-10;
}
/* Override Vuetify's default padding to accommodate our icon */
:deep(.v-field__append-inner) {
padding-inline-start: 0;
}
</style>
<i18n>
{
"en": {
"title": "Create your account",
"alt": "Hutopy Registration",
"name": "Full Name",
"email": "Email",
"password": "Password",
"confirmPassword": "Confirm Password",
"passwordRequirements": "Password must be at least 8 characters",
"register": "Register",
"alreadyHaveAccount": "Already have an account?",
"signIn": "Sign in",
"passwordsDoNotMatch": "Passwords do not match",
"registrationFailed": "Registration failed. Please try again.",
"success": {
"title": "Registration Successful!",
"message": "Please check your email to verify your account. We've sent a verification link to:",
"backToLogin": "Back to Login",
"resendVerification": "Didn't receive the email? Resend verification"
}
},
"fr": {
"title": "Créer votre compte",
"alt": "Inscription Hutopy",
"name": "Nom complet",
"email": "Email",
"password": "Mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères",
"register": "S'inscrire",
"alreadyHaveAccount": "Vous avez déjà un compte?",
"signIn": "Se connecter",
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
"registrationFailed": "L'inscription a échoué. Veuillez réessayer.",
"success": {
"title": "Inscription réussie!",
"message": "Veuillez vérifier votre email pour activer votre compte. Nous avons envoyé un lien de vérification à:",
"backToLogin": "Retour à la connexion",
"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>

View File

@@ -0,0 +1,219 @@
<template>
<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">
<!-- Loading state while verification is in progress -->
<div v-if="isLoading" class="flex flex-col items-center gap-4">
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
<h2 class="text-xl font-medium">{{ t('verifying') }}</h2>
</div>
<!-- Success state -->
<div v-else-if="verificationSuccess" class="flex flex-col items-center gap-6">
<v-icon icon="mdi-check-circle" color="green" size="64"></v-icon>
<h1 class="text-2xl font-bold text-green-600">{{ t('success.title') }}</h1>
<p>{{ t('success.message') }}</p>
<v-btn color="primary" @click="goToLogin">{{ t('success.goToLogin') }}</v-btn>
</div>
<!-- Error state -->
<div v-else class="flex flex-col items-center gap-6">
<v-icon icon="mdi-alert-circle" color="error" size="64"></v-icon>
<h1 class="text-2xl font-bold text-red-600">{{ t('error.title') }}</h1>
<p>{{ errorMessage || t('error.defaultMessage') }}</p>
<div class="mt-4 flex flex-col gap-4 w-full">
<v-btn color="primary" @click="goToLogin">{{ t('error.goToLogin') }}</v-btn>
<v-divider class="my-4"></v-divider>
<!-- Resend verification email section -->
<h2 class="text-xl font-medium">{{ t('resend.title') }}</h2>
<v-form @submit.prevent="handleResendVerification" class="w-full">
<div class="flex flex-col gap-4">
<v-text-field
v-model="resendEmail"
:label="t('resend.emailLabel')"
type="email"
required
:error-messages="resendEmailError"
></v-text-field>
<v-btn
type="submit"
color="secondary"
block
:loading="resendLoading"
>
{{ t('resend.button') }}
</v-btn>
<!-- 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">
{{ t('resend.success') }}
</div>
<!-- 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">
{{ resendError }}
</div>
</div>
</v-form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useClient } from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const clientApi = useClient();
// Verification state
const isLoading = ref(true);
const verificationSuccess = ref(false);
const errorMessage = ref('');
// Resend verification state
const resendEmail = ref('');
const resendEmailError = ref('');
const resendLoading = ref(false);
const resendSuccess = ref(false);
const resendError = ref('');
onMounted(async () => {
const userId = route.query.userId;
const token = route.query.token;
// Populate resend email field if it was in the URL
if (route.query.email) {
resendEmail.value = route.query.email;
}
// Check if we have the required parameters
if (!userId || !token) {
isLoading.value = false;
errorMessage.value = t('error.missingParams');
return;
}
try {
// Call the verification endpoint
await clientApi.get(`/api/users/verify-email?userId=${userId}&token=${token}`);
verificationSuccess.value = true;
} catch (error) {
console.error('Email verification failed:', error);
errorMessage.value = error.response?.data?.message || t('error.defaultMessage');
} finally {
isLoading.value = false;
}
});
async function handleResendVerification() {
// Reset states
resendEmailError.value = '';
resendSuccess.value = false;
resendError.value = '';
// Simple email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(resendEmail.value)) {
resendEmailError.value = t('resend.invalidEmail');
return;
}
resendLoading.value = true;
try {
await clientApi.post('/api/users/resend-verification', {
email: resendEmail.value.trim()
});
resendSuccess.value = true;
} catch (error) {
console.error('Resend verification failed:', error);
resendError.value = error.response?.data?.message || t('resend.error');
} finally {
resendLoading.value = false;
}
}
function goToLogin() {
router.push('/login');
}
</script>
<i18n>
{
"en": {
"verifying": "Verifying your email...",
"success": {
"title": "Email Verified Successfully!",
"message": "Your email has been verified. You can now log in to your account.",
"goToLogin": "Go to Login"
},
"error": {
"title": "Verification Failed",
"defaultMessage": "We couldn't verify your email. The link may be invalid or expired.",
"missingParams": "Missing required verification parameters.",
"goToLogin": "Go to Login"
},
"resend": {
"title": "Resend Verification Email",
"emailLabel": "Email",
"button": "Resend Verification Email",
"success": "Verification email sent successfully. Please check your inbox.",
"error": "Failed to send verification email. Please try again.",
"invalidEmail": "Please enter a valid email address."
}
},
"fr": {
"verifying": "Vérification de votre email...",
"success": {
"title": "Email vérifié avec succès !",
"message": "Votre email a été vérifié. Vous pouvez maintenant vous connecter à votre compte.",
"goToLogin": "Aller à la connexion"
},
"error": {
"title": "Échec de la vérification",
"defaultMessage": "Nous n'avons pas pu vérifier votre email. Le lien peut être invalide ou expiré.",
"missingParams": "Paramètres de vérification requis manquants.",
"goToLogin": "Aller à la connexion"
},
"resend": {
"title": "Renvoyer l'email de vérification",
"emailLabel": "Email",
"button": "Renvoyer l'email de vérification",
"success": "Email de vérification envoyé avec succès. Veuillez vérifier votre boîte de réception.",
"error": "Échec de l'envoi de l'email de vérification. Veuillez réessayer.",
"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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,191 +1,206 @@
<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 { useRoute } from 'vue-router';
import { mdiFileAccountOutline, mdiAccount, mdiLogin, mdiTranslateVariant, mdiLogout } from '@mdi/js';
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 { mdiAccount, mdiFileAccountOutline, mdiLogin, mdiLogout, mdiTranslateVariant } from '@mdi/js';
const { locale, t } = useI18n();
const languageStore = useLanguageStore();
const route = useRoute();
const { locale, t } = useI18n();
const languageStore = useLanguageStore();
const route = useRoute();
const userProfileStore = useUserProfileStore();
const creatorProfileStore = useCreatorProfileStore();
const authStore = useAuthStore();
const userProfileStore = useUserProfileStore();
const creatorProfileStore = useCreatorProfileStore();
const authStore = useAuthStore();
function toggleLanguage() {
const languages = ['fr', 'en', 'es'];
const currentIndex = languages.indexOf(locale.value);
const nextIndex = (currentIndex + 1) % languages.length;
languageStore.setLocale(languages[nextIndex]);
}
function toggleLanguage() {
const languages = ['fr', 'en', 'es'];
const currentIndex = languages.indexOf(locale.value);
const nextIndex = (currentIndex + 1) % languages.length;
languageStore.setLocale(languages[nextIndex]);
}
function handleLogout() {
// Check if current route requires authentication
const requiresAuth = route.matched.some(record => record.meta.requiresAuth);
// If on a protected page, redirect to landing, otherwise stay on current page
const redirectTo = requiresAuth ? '/landing' : route.fullPath;
authStore.logout(redirectTo);
}
function handleLogout() {
authStore.logout();
}
</script>
<template>
<nav class="side-container">
<div class="side-logo">
<router-link to="/@hutopy">
<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">
<span class="profile-label">{{ userProfileStore.alias }}</span>
</div>
<div class="side-menu-items">
<template v-if="authStore.isAuthenticated">
<router-link v-if="creatorProfileStore.hasCreator" :to="`/@${creatorProfileStore.creator.slug}`">
<button class="menu-item-action">
<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">
<v-icon :icon="mdiFileAccountOutline" />
<span class="label">{{ t('sidebar.myPage') }}</span>
</button>
</router-link>
</template>
<template v-if="authStore.isAuthenticated">
<router-link to="/profile">
<button class="menu-item-action">
<v-icon :icon="mdiAccount" />
<span class="label">{{ t('sidebar.myProfile') }}</span>
</button>
</router-link>
</template>
<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">
<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">
<v-icon :icon="mdiLogout" />
<span class="label">{{ t('sidebar.signOut') }}</span>
</button>
<nav class="side-container">
<div class="side-logo">
<router-link to="/@hutopy">
<img
alt="hutopy logo"
height="50"
src="/images/hutopy-logo.png"
/>
</router-link>
</div>
</div>
</div>
</nav>
<div class="side-menu">
<div
v-if="authStore.isAuthenticated"
class="side-menu-portrait"
>
<img
:src="userProfileStore.portraitUrl"
alt="Profile Image"
class="rounded-full"
referrerpolicy="no-referrer"
/>
<span class="profile-label">{{ userProfileStore.alias }}</span>
</div>
<div class="side-menu-items">
<template v-if="authStore.isAuthenticated">
<router-link
v-if="creatorProfileStore.hasCreator"
:to="`/@${creatorProfileStore.creator.slug}`"
>
<button class="menu-item-action">
<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">
<v-icon :icon="mdiFileAccountOutline" />
<span class="label">{{ t('sidebar.myPage') }}</span>
</button>
</router-link>
</template>
<template v-if="authStore.isAuthenticated">
<router-link to="/profile">
<button class="menu-item-action">
<v-icon :icon="mdiAccount" />
<span class="label">{{ t('sidebar.myProfile') }}</span>
</button>
</router-link>
</template>
<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">
<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"
>
<v-icon :icon="mdiLogout" />
<span class="label">{{ t('sidebar.signOut') }}</span>
</button>
</div>
</div>
</div>
</nav>
</template>
<style scoped>
.side-container {
@apply bg-hSurface text-hOnSurface;
@apply lg:fixed lg:max-h-screen;
@apply flex;
@apply lg:flex-col lg:w-64 lg:max-w-64;
@apply h-16 lg:h-screen;
@apply lg:border-r-2 lg:border-[#2d282d];
}
.side-container {
@apply bg-hSurface text-hOnSurface;
@apply lg:fixed lg:max-h-screen;
@apply flex;
@apply lg:flex-col lg:w-64 lg:max-w-64;
@apply h-16 lg:h-screen;
@apply lg:border-r-2 lg:border-[#2d282d];
}
.side-logo {
@apply flex flex-grow;
@apply items-center justify-start p-4;
@apply lg:items-start lg:justify-center lg:pt-4;
}
.side-logo {
@apply flex flex-grow;
@apply items-center justify-start p-4;
@apply lg:items-start lg:justify-center lg:pt-4;
}
.side-menu {
@apply flex gap-4 p-6;
@apply items-center lg:items-stretch;
@apply flex-row-reverse lg:flex-col;
}
.side-menu {
@apply flex gap-4 p-6;
@apply items-center lg:items-stretch;
@apply flex-row-reverse lg:flex-col;
}
.side-menu-portrait {
@apply w-10 h-10;
@apply -ml-1;
@apply flex items-center justify-start;
}
.side-menu-portrait {
@apply w-10 h-10;
@apply -ml-1;
@apply flex items-center justify-start;
}
.side-menu-items {
@apply flex gap-2;
@apply flex-row;
@apply lg:w-full lg:flex-col;
}
.side-menu-items {
@apply flex gap-2;
@apply flex-row;
@apply lg:w-full lg:flex-col;
}
.profile-label {
@apply ml-5;
@apply text-lg font-sans capitalize;
@apply font-semibold;
@apply hidden lg:inline;
@apply min-w-40 truncate;
}
.profile-label {
@apply ml-5;
@apply text-lg font-sans capitalize;
@apply font-semibold;
@apply hidden lg:inline;
@apply min-w-40 truncate;
}
.label {
@apply text-nowrap;
@apply ml-4;
@apply hidden lg:inline;
}
.label {
@apply text-nowrap;
@apply ml-4;
@apply hidden lg:inline;
}
.menu-item-action {
@apply bg-hSurface text-hOnSurface hover:mix-blend-screen;
@apply capitalize;
@apply flex items-center gap-3 p-2 rounded-full md:rounded-full;
@apply mx-0;
@apply lg:pl-2;
@apply w-10 h-10 justify-center lg:w-full lg:h-auto lg:justify-normal;
.menu-item-action {
@apply bg-hSurface text-hOnSurface hover:mix-blend-screen;
@apply capitalize;
@apply flex items-center gap-3 p-2 rounded-full md:rounded-full;
@apply mx-0;
@apply lg:pl-2;
@apply w-10 h-10 justify-center lg:w-full lg:h-auto lg:justify-normal;
i {
@apply text-xl;
}
}
i {
@apply text-xl;
}
}
</style>
<i18n>
{
"en": {
"sidebar": {
"myPage": "My Page",
"myProfile": "My Profile",
"signIn": "Sign In",
"signOut": "Sign Out"
"en": {
"sidebar": {
"myPage": "My Page",
"myProfile": "My Profile",
"signIn": "Sign In",
"signOut": "Sign Out"
}
},
"fr": {
"sidebar": {
"myPage": "Ma Page",
"myProfile": "Mon Profil",
"signIn": "Se Connecter",
"signOut": "Se Déconnecter"
}
},
"es": {
"sidebar": {
"myPage": "Mi Página",
"myProfile": "Mi Perfil",
"signIn": "Iniciar Sesión",
"signOut": "Cerrar Sesión"
}
}
},
"fr": {
"sidebar": {
"myPage": "Ma Page",
"myProfile": "Mon Profil",
"signIn": "Se Connecter",
"signOut": "Se Déconnecter"
}
},
"es": {
"sidebar": {
"myPage": "Mi Página",
"myProfile": "Mi Perfil",
"signIn": "Iniciar Sesión",
"signOut": "Cerrar Sesión"
}
}
}
</i18n>