All files / src/views LoginPage.vue

91.66% Statements 33/36
85.29% Branches 29/34
70% Functions 7/10
91.66% Lines 33/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188  13x       43x   13x         1x 1x     7x             1x 1x     7x                                   13x                     1x   1x                                                           1x                       1x                       13x 13x 13x 13x   13x 13x 13x 13x 13x 13x   13x   13x               8x 8x   8x 8x 4x   4x   8x                                                                                        
<template>
  <AuthLayout
    :presentation-title="t('auth.presentationTitleLogin')"
    :presentation-subtitle="t('auth.presentationSubtitleLogin')"
  >
    <span>{{ t('auth.newToFlows') }}</span>
    <router-link to="/signup" class="btn btn-success shadow p-2 my-3 w-100">
      {{ t('auth.tryItFree') }}
    </router-link>
 
    <form class="signin-form" @submit.prevent="handleLogin">
      <hr class="w-100" />
      <span class="mb-2">{{ t('auth.alreadyMember') }}</span>
      <label for="email">{{ t('auth.email') }}</label>
      <input
        id="email"
        v-model="email"
        class="w-100"
        type="email"
        name="username"
        autocomplete="username"
        required
      />
      <label for="password">{{ t('auth.password') }}</label>
      <div class="password-wrapper">
        <input
          id="password"
          v-model="password"
          class="w-100"
          :type="showPassword ? 'text' : 'password'"
          name="password"
          autocomplete="current-password"
          required
        />
        <button
          type="button"
          class="password-toggle"
          :aria-label="showPassword ? t('auth.hidePassword') : t('auth.showPassword')"
          @click="showPassword = !showPassword"
        >
          <FontAwesomeIcon :icon="showPassword ? 'eye-slash' : 'eye'" />
        </button>
      </div>
      <router-link to="/forgot-password" class="forgot-password">{{
        t('auth.forgotPassword')
      }}</router-link>
      <button type="submit" class="btn btn-secondary shadow p-2 my-3" :disabled="loading">
        {{ t('auth.signIn') }}
      </button>
      <div v-if="error" class="alert alert-danger" role="alert">
        {{ error }}
      </div>
      <div v-if="resetSuccess" class="alert alert-success" role="alert">
        {{ t('auth.resetPasswordSuccess') }}
      </div>
    </form>
    <div class="row vertical-center">
      <div class="col-5"><hr /></div>
      <div class="col-2 separator-text">{{ t('auth.or') }}</div>
      <div class="col-5"><hr /></div>
    </div>
    <a href="#" class="btn btn-light signin-google shadow p-2 my-3 w-100" @click.prevent>
      <span class="icon">
        <svg
          width="18"
          height="18"
          viewBox="0 0 256 262"
          xmlns="http://www.w3.org/2000/svg"
          preserveAspectRatio="xMidYMid"
        >
          <path
            d="M254.69 106.22H129.775v51.002h73.14c-3.666 15.153-8.197 20.653-12.692 25.788-5.01 5.702-9.894 10.653-15.43 14.717l-1.413 2.294 43.225 30.734 2.993-1.17c22.808-22.018 36.388-55.09 36.388-98.727 0-8.456.243-16.632-1.297-24.638"
            fill="#4486F4"
          />
          <path
            d="M130.965 261.93c35.635 0 66.484-10.96 88.633-32.345l-44.805-31.858c-10.44 7.663-23.235 12.12-43.828 12.12-34.67 0-64.096-22.38-74.674-53.472l-2.52-1.466-39.713 32.06.18 3.354c21.65 42.487 65.774 71.605 116.728 71.605"
            fill="#34A853"
          />
          <path
            d="M52.083 130.965c0-9.032 1.538-17.7 4.333-25.78l.273-2.36L15.08 71.47l-1.506 1.43C4.9 90.4 0 110.106 0 130.964c0 21.375 5.154 41.536 14.237 59.36l42.054-33.95c-2.713-7.976-4.207-16.516-4.207-25.41"
            fill="#FBBC05"
          />
          <path
            d="M56.416 105.185c10.685-30.9 40.015-53.102 74.55-53.102 19.68 0 37.663 7.225 51.482 19.146l36.562-37.22C195.753 12.88 164.863 0 130.965 0 79.493 0 34.98 29.708 13.575 72.9l42.84 32.285"
            fill="#EC4235"
          />
        </svg>
      </span>
      <span class="text">{{ t('auth.signInWithGoogle') }}</span>
    </a>
    <a href="#" class="btn btn-light signin-facebook shadow p-2 mt-2 w-100" @click.prevent>
      <span class="icon">
        <svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
          <path
            d="M17.007 18H12.42v-6.97h2.34l.35-2.717h-2.69V6.578c0-.786.218-1.322 1.346-1.322h1.44v-2.43a19.322 19.322 0 0 0-2.097-.107c-2.075 0-3.495 1.265-3.495 3.59v2.003H7.27v2.716h2.345V18H.993A.994.994 0 0 1 0 17.006V.994C0 .444.445 0 .993 0h16.014c.548 0 .993.445.993.994v16.012c0 .55-.445.994-.993.994z"
            fill="#43619C"
            fill-rule="evenodd"
          />
        </svg>
      </span>
      <span class="text">{{ t('auth.signInWithFacebook') }}</span>
    </a>
  </AuthLayout>
</template>
 
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useUILanguage } from '@/composables/useUILanguage'
import AuthLayout from '@/components/AuthLayout.vue'
 
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const { t } = useUILanguage()
 
const email = ref('')
const password = ref('')
const showPassword = ref(false)
const loading = ref(false)
const error = ref('')
const resetSuccess = ref(false)
 
onMounted(() => {
  // Check if redirected from password reset
  Iif (route.query.resetSuccess === 'true') {
    resetSuccess.value = true
    // Clear query param from URL
    router.replace({ query: {} })
  }
})
 
async function handleLogin() {
  loading.value = true
  error.value = ''
 
  try {
    await authStore.login(email.value, password.value)
    router.push('/')
  } catch (err: any) {
    error.value = err.message || 'Login failed'
  } finally {
    loading.value = false
  }
}
</script>
 
<style scoped>
.icon {
  align-items: center;
  justify-content: center;
  margin-right: 8px;
}
 
.forgot-password {
  font-size: small;
}
 
.separator-text {
  text-align: center;
  color: #666666;
  font-size: small;
}
 
.password-wrapper {
  position: relative;
  width: 100%;
}
 
.password-toggle {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  color: #666;
  padding: 0 4px;
  line-height: 1;
}
 
.password-toggle:hover {
  color: #333;
}
</style>