Auth Layout

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

Alt text

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>