Auth Layout
This layout is designed to provide a clean, responsive, and visually appealing structure for authentication-related pages such as login and signup.

Features
Split screen layout with an image and message panel on large screens
Fully responsive: stacks vertically on small devices
Uses custom utility classes for consistent design
Slot-based content injection for reuse across auth pages
<template>
<div class="w-full bg-neutral-30 min-h-screen text-neutral-500 sm:py-[50px] sm:px-[82px] p-3">
<div class="flex rounded-xl">
<div
class="hidden lg:block w-6/12 py-[72px] px-[45px] h-[85vh] bg-primary-300 relative overflow-hidden rounded-l-xl"
>
<h1 class="text-neutral-10 text-center mb-3">Powerful Admin Control at Your Fingertips</h1>
<p class="body-regular text-center text-neutral-10 max-w-[381px] mx-auto">
Manage vendors, track revenue, and optimise operations and more with ease.
</p>
<div class="absolute left-0 -bottom-20">
<img src="@/assets/images/authBG.png" alt="background image" class="w-screen" />
</div>
</div>
<div
class="sm:py-[80px] xl:px-[145px] lg:px-[60px] sm:px-[96px] px-4 py-8 bg-neutral-10 lg:w-6/12 w-full lg:rounded-r-xl lg:rounded-none rounded-xl lg:overflow-hidden lg:h-[85vh]"
>
<BaseCustomIcon name="logo" class="mb-6" />
<slot />
</div>
</div>
</div>
</template>
Admin Login Form Component
This form allows admin users to securely log in to the application. It includes input validation, error messaging, form state management, and integrates with your store and routing logic.
Features
Form Validation using Joi
Error Handling for each field
Loading and Disable States during submission
Session Expiry Toast Notification
Responsive layout and reusable UI components
Template Structure
pages/login.vue
<template>
<div>
<div class="mb-8">
<h3>Admin Login</h3>
<p class="body-small text-neutral-200">
Securely log in to manage users, services, and more.
</p>
</div>
<form @submit.prevent="onSubmit" class=" ">
<div class="flex flex-col">
<div>
<FormText
label="Email Address"
required
v-model="userEmail"
placeholder="Enter your email address"
@input="validateField('userEmail')"
></FormText>
<span v-if="errors.userEmail" class="text-danger-300 mt-8 text-xs">
{{ errors.userEmail }}
</span>
</div>
<div class="mt-5 mb-2">
<FormText
label="Password"
v-model="userPassword"
type="password"
placeholder="Enter your password"
@input="validateField('userPassword')"
/>
<div v-if="errors.userPassword" class="text-danger-300 text-xs">
{{ errors.userPassword }}
</div>
</div>
</div>
<div class="flex items-center justify-between mb-6">
<div class="flex items-center"></div>
<NuxtLink
to="/forgot-password"
class="no-underline ml-2 label text-primary-200 text-right cursor-pointer"
>Forgot password?</NuxtLink
>
</div>
<NuxtLink to="/">
<CommonButton
title="Log In"
bgColor="bg-secondary-300"
textColor="text-primary-300 font-semibold text-sm"
type="submit"
:loading="isLoading"
:disabled="isLoading || !isFormValid"
/>
</NuxtLink>
</form>
</div>
</template>
Script Setup
<script setup>
import Joi from 'joi'
import { useToast } from 'primevue/usetoast'
import FormText from '~/components/common/input/FormText.vue'
import { useRoute, useRouter } from 'vue-router'
definePageMeta({
layout: 'auth' // Specify the layout to use
})
useHead({
title: 'Login | Attafly Admin'
})
const toast = useToast()
// Get the current route
const route = useRoute()
const router = useRouter()
// Access the query parameters
const query = route.query
const isLoading = ref(false)
const errors = ref({})
const userEmail = ref()
const userPassword = ref()
const isFormValid = computed(() => {
return (
userEmail.value && userPassword.value && !errors.value.userEmail && !errors.value.userPassword
)
})
const validationSchema = {
userEmail: Joi.alternatives()
.try(
Joi.string()
.email({ tlds: { allow: false } })
.messages({
'string.email': 'Enter a valid email address'
})
)
.required()
.messages({
'alternatives.match': 'Enter a valid email address',
'any.required': 'Email is required',
'string.empty': 'Email cannot be empty'
}),
userPassword: Joi.string().min(8).required().messages({
'any.required': 'Password is required',
'string.empty': 'Password is required',
'string.min': 'Password must be at least 8 characters'
})
}
// Field validation function
const validateField = (field) => {
const value = field === 'userEmail' ? userEmail.value : userPassword.value
const { error } = validationSchema[field].validate(value)
if (error) {
errors.value[field] = error.details[0].message
} else {
delete errors.value[field]
}
}
// Form submission handler
const onSubmit = async () => {
Object.keys(validationSchema).forEach((field) => validateField(field))
if (isFormValid.value) {
// await signInUser();
}
}
onMounted(() => {
if (query.toast === 'Session Expired') {
toast.add({
severity: 'info',
summary: '',
detail: 'Session Expired',
life: 5000,
closable: false
})
removeTypeQuery()
}
})
const removeTypeQuery = () => {
// Navigate to the same route with the updated query
router.push({ path: route.path })
}
</script>