561 lines
14 KiB
Vue
561 lines
14 KiB
Vue
<template>
|
|
<Toast :breakpoints="{ '767px': { width: '100%', right: '0', left: '0' } }" />
|
|
<div class="login-page">
|
|
<div class="login-backdrop" aria-hidden="true"></div>
|
|
|
|
<div class="login-shell">
|
|
<section class="login-brand">
|
|
<div class="brand-mark">
|
|
<img src="img/e-Rahn_Logo_FA-01.png" alt="" class="brand-seal" />
|
|
<img src="img/e-rahn-lelong.png" alt="E-Rahn Lelong" class="brand-wordmark" />
|
|
</div>
|
|
<h1 class="brand-title">ArrahnBid</h1>
|
|
<p class="brand-copy">
|
|
Platform bidaan Ar-Rahn yang lebih pantas, jelas, dan selamat untuk anda.
|
|
</p>
|
|
</section>
|
|
|
|
<section class="login-panel">
|
|
<header class="panel-header">
|
|
<h2>Log Masuk</h2>
|
|
<p>
|
|
Belum punya akaun?
|
|
<router-link :to="{ name: 'register' }">Daftar sekarang</router-link>
|
|
</p>
|
|
</header>
|
|
|
|
<form class="login-form" @submit.prevent="doLogin">
|
|
<label class="field">
|
|
<span class="field-label">ID Pengguna</span>
|
|
<div class="field-control" :class="{ 'is-invalid': v$.form.username.$error }">
|
|
<User class="field-icon" :size="18" :stroke-width="1.75" />
|
|
<InputText
|
|
v-model="form.username"
|
|
type="text"
|
|
placeholder="Masukkan ID pengguna"
|
|
autocomplete="username"
|
|
required
|
|
/>
|
|
</div>
|
|
<InlineMessage v-if="v$.form.username.$error" class="field-error">
|
|
Username diperlukan
|
|
</InlineMessage>
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span class="field-label">Kata Laluan</span>
|
|
<div class="field-control" :class="{ 'is-invalid': v$.form.password.$error }">
|
|
<Lock class="field-icon" :size="18" :stroke-width="1.75" />
|
|
<InputText
|
|
v-model="form.password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
placeholder="Masukkan kata laluan"
|
|
autocomplete="current-password"
|
|
required
|
|
/>
|
|
<button
|
|
class="reveal-btn"
|
|
type="button"
|
|
:aria-label="showPassword ? 'Sembunyikan kata laluan' : 'Tunjuk kata laluan'"
|
|
@click="showPassword = !showPassword"
|
|
>
|
|
<EyeOff v-if="showPassword" :size="18" :stroke-width="1.75" />
|
|
<Eye v-else :size="18" :stroke-width="1.75" />
|
|
</button>
|
|
</div>
|
|
<InlineMessage v-if="v$.form.password.$error" class="field-error">
|
|
Kata laluan diperlukan
|
|
</InlineMessage>
|
|
</label>
|
|
|
|
<div class="form-row">
|
|
<router-link class="forgot-link" :to="{ name: 'forgot-password' }">
|
|
Lupa kata laluan?
|
|
</router-link>
|
|
</div>
|
|
|
|
<button class="submit-btn" type="submit" :disabled="isLoading">
|
|
<span>{{ isLoading ? "Sedang log masuk..." : "Log Masuk" }}</span>
|
|
<LoaderCircle v-if="isLoading" class="spin" :size="18" :stroke-width="2" />
|
|
<ArrowRight v-else :size="18" :stroke-width="2" />
|
|
</button>
|
|
</form>
|
|
|
|
<footer class="panel-footer">
|
|
<FileText :size="16" :stroke-width="1.75" />
|
|
<terms-and-conditions />
|
|
</footer>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import axios from "axios";
|
|
import useVuelidate from "@vuelidate/core";
|
|
import { required } from "@vuelidate/validators";
|
|
import { ArrowRight, Eye, EyeOff, FileText, LoaderCircle, Lock, User } from "@lucide/vue";
|
|
import { useAuthenticationUserStore } from "@/store/modules/authentication";
|
|
|
|
export default {
|
|
name: "Login",
|
|
components: {
|
|
ArrowRight,
|
|
Eye,
|
|
EyeOff,
|
|
FileText,
|
|
LoaderCircle,
|
|
Lock,
|
|
User,
|
|
},
|
|
data() {
|
|
return {
|
|
v$: useVuelidate(),
|
|
form: {
|
|
username: "",
|
|
password: "",
|
|
},
|
|
isLoading: false,
|
|
showPassword: false,
|
|
};
|
|
},
|
|
setup() {
|
|
const authenticationStore = useAuthenticationUserStore();
|
|
return { authenticationStore };
|
|
},
|
|
mounted() {
|
|
this.showEmailVerificationToast();
|
|
},
|
|
methods: {
|
|
showEmailVerificationToast() {
|
|
const { email_verified, message } = this.$route.query;
|
|
if (email_verified === undefined) {
|
|
return;
|
|
}
|
|
|
|
const verified = String(email_verified) === "1";
|
|
this.$toast.add({
|
|
severity: verified ? "success" : "error",
|
|
summary: verified ? "Pengesahan Emel" : "Pengesahan Emel Gagal",
|
|
detail: message || (verified
|
|
? "Emel berjaya disahkan. Sila log masuk."
|
|
: "Pautan pengesahan tidak sah."),
|
|
life: 10000,
|
|
});
|
|
|
|
this.$router.replace({ query: {} });
|
|
},
|
|
doLogin() {
|
|
this.v$.$validate();
|
|
if (!this.v$.$error) {
|
|
this.isLoading = true;
|
|
axios
|
|
.post("/user/login", this.form)
|
|
.then((response) => {
|
|
if (response.data.statusAkaun == 1) {
|
|
this.authenticationStore.setSession({
|
|
token: response.data.token,
|
|
user: response.data.user,
|
|
});
|
|
this.$router.push({ name: "home" });
|
|
} else if (response.data.statusAkaun == 0) {
|
|
this.$toast.add({
|
|
severity: "info",
|
|
summary: "Gagal Log Masuk",
|
|
detail: "Tiada akaun dalam database. Sila daftar sekarang.",
|
|
life: 10000,
|
|
});
|
|
} else if (response.data.statusAkaun == 2) {
|
|
this.$toast.add({
|
|
severity: "error",
|
|
summary: "Gagal Log Masuk",
|
|
detail:
|
|
"Akaun anda telah dibanned. Sila hubungi pihak pengurusan untuk maklumat lebih lanjut.",
|
|
life: 10000,
|
|
});
|
|
} else if (response.data.statusAkaun == 3) {
|
|
this.$toast.add({
|
|
severity: "error",
|
|
summary: "Gagal Log Masuk",
|
|
detail:
|
|
"Account rejected. Please contact administrator for further information.",
|
|
life: 10000,
|
|
});
|
|
} else if (response.data.statusAkaun == 4) {
|
|
this.$toast.add({
|
|
severity: "info",
|
|
summary: "Gagal Log Masuk",
|
|
detail: "Registration in progress,status wait for approval",
|
|
life: 10000,
|
|
});
|
|
} else if (response.data.statusAkaun == 5) {
|
|
this.$toast.add({
|
|
severity: "error",
|
|
summary: "Gagal Log Masuk",
|
|
detail: "Kata laluan salah. Sila semak semula.",
|
|
life: 10000,
|
|
});
|
|
console.log(response);
|
|
} else if (response.data.statusAkaun == 6) {
|
|
this.$toast.add({
|
|
severity: "warn",
|
|
summary: "Akaun ini didaftar menggunakan id staff.",
|
|
detail:
|
|
"Sila log masuk menggunakan url https://adminlelong.erahn.com.my ",
|
|
life: 10000,
|
|
});
|
|
} else if (response.data.statusAkaun == 7) {
|
|
this.$toast.add({
|
|
severity: "warn",
|
|
summary: "Emel Belum Disahkan",
|
|
detail:
|
|
response.data.message ||
|
|
"Sila sahkan emel anda sebelum log masuk.",
|
|
life: 10000,
|
|
});
|
|
}
|
|
this.isLoading = false;
|
|
})
|
|
.catch((response) => {
|
|
console.log(response);
|
|
this.isLoading = false;
|
|
});
|
|
}
|
|
},
|
|
},
|
|
validations() {
|
|
return {
|
|
form: {
|
|
username: { required },
|
|
password: { required },
|
|
},
|
|
};
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=Sora:wght@500;600;700&display=swap");
|
|
|
|
.login-page {
|
|
--ink: #132a4f;
|
|
--navy: #1e417b;
|
|
--blue: #2f68c5;
|
|
--gold: #fec234;
|
|
--panel: rgba(255, 252, 247, 0.94);
|
|
--line: rgba(19, 42, 79, 0.12);
|
|
--muted: rgba(19, 42, 79, 0.68);
|
|
|
|
position: relative;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1.5rem;
|
|
overflow: hidden;
|
|
font-family: "Manrope", sans-serif;
|
|
color: var(--ink);
|
|
}
|
|
|
|
.login-backdrop {
|
|
position: absolute;
|
|
inset: 0;
|
|
background:
|
|
linear-gradient(115deg, rgba(19, 42, 79, 0.78) 0%, rgba(30, 65, 123, 0.55) 42%, rgba(254, 194, 52, 0.35) 100%),
|
|
url("/src/assets/background-lelong.png") center / cover no-repeat;
|
|
transform: scale(1.04);
|
|
animation: backdrop-drift 18s ease-in-out infinite alternate;
|
|
}
|
|
|
|
.login-shell {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: min(980px, 100%);
|
|
display: grid;
|
|
grid-template-columns: 1.05fr 0.95fr;
|
|
gap: 0;
|
|
border-radius: 28px;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
background: rgba(255, 255, 255, 0.08);
|
|
backdrop-filter: blur(10px);
|
|
animation: shell-enter 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
|
|
.login-brand {
|
|
padding: 3rem 2.75rem;
|
|
color: #fffdf8;
|
|
background:
|
|
linear-gradient(160deg, rgba(19, 42, 79, 0.28), rgba(47, 104, 197, 0.1)),
|
|
radial-gradient(circle at top left, rgba(254, 194, 52, 0.35), transparent 55%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-end;
|
|
min-height: 560px;
|
|
}
|
|
|
|
.brand-mark {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.85rem;
|
|
margin-bottom: auto;
|
|
animation: rise 0.8s 0.1s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
|
|
.brand-seal {
|
|
width: 52px;
|
|
height: 52px;
|
|
object-fit: contain;
|
|
filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.18));
|
|
}
|
|
|
|
.brand-wordmark {
|
|
width: min(180px, 50vw);
|
|
height: auto;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.brand-title {
|
|
margin: 0 0 0.75rem;
|
|
font-family: "Sora", sans-serif;
|
|
font-size: clamp(2.4rem, 4vw, 3.4rem);
|
|
font-weight: 700;
|
|
letter-spacing: -0.04em;
|
|
line-height: 1.05;
|
|
animation: rise 0.8s 0.18s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
|
|
.brand-copy {
|
|
margin: 0;
|
|
max-width: 28ch;
|
|
font-size: 1.05rem;
|
|
line-height: 1.55;
|
|
color: rgba(255, 253, 248, 0.88);
|
|
animation: rise 0.8s 0.26s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
|
|
.login-panel {
|
|
background: var(--panel);
|
|
padding: 2.75rem 2.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
animation: rise 0.8s 0.2s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
|
|
.panel-header h2 {
|
|
margin: 0 0 0.4rem;
|
|
font-family: "Sora", sans-serif;
|
|
font-size: 1.85rem;
|
|
letter-spacing: -0.03em;
|
|
}
|
|
|
|
.panel-header p {
|
|
margin: 0 0 1.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.panel-header a,
|
|
.forgot-link,
|
|
.panel-footer :deep(.terms-link) {
|
|
color: var(--blue);
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.panel-header a:hover,
|
|
.forgot-link:hover,
|
|
.panel-footer :deep(.terms-link):hover {
|
|
color: var(--navy);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.login-form {
|
|
display: grid;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.field {
|
|
display: grid;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
.field-label {
|
|
font-size: 0.92rem;
|
|
font-weight: 600;
|
|
color: var(--ink);
|
|
}
|
|
|
|
.field-control {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.65rem;
|
|
min-height: 3.15rem;
|
|
padding: 0 0.95rem;
|
|
border: 1px solid var(--line);
|
|
border-radius: 14px;
|
|
background: #fff;
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.field-control:focus-within {
|
|
border-color: var(--blue);
|
|
box-shadow: 0 0 0 3px rgba(47, 104, 197, 0.14);
|
|
}
|
|
|
|
.field-control.is-invalid {
|
|
border-color: #d14343;
|
|
}
|
|
|
|
.field-icon {
|
|
flex: 0 0 auto;
|
|
color: var(--navy);
|
|
opacity: 0.72;
|
|
}
|
|
|
|
.field-control :deep(.p-inputtext) {
|
|
flex: 1 1 auto;
|
|
width: 100%;
|
|
min-width: 0;
|
|
border: 0 !important;
|
|
box-shadow: none !important;
|
|
background: transparent !important;
|
|
padding: 0.85rem 0 !important;
|
|
font-family: inherit;
|
|
font-size: 0.98rem;
|
|
color: var(--ink);
|
|
}
|
|
|
|
.reveal-btn {
|
|
flex: 0 0 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0;
|
|
padding: 0.2rem;
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--navy);
|
|
opacity: 0.7;
|
|
cursor: pointer;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.reveal-btn:hover {
|
|
opacity: 1;
|
|
background: rgba(30, 65, 123, 0.06);
|
|
}
|
|
|
|
.field-error {
|
|
width: fit-content;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: -0.15rem;
|
|
}
|
|
|
|
.submit-btn {
|
|
margin-top: 0.35rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.55rem;
|
|
width: 100%;
|
|
min-height: 3.2rem;
|
|
border: 0;
|
|
border-radius: 14px;
|
|
background: linear-gradient(135deg, var(--navy) 0%, var(--blue) 100%);
|
|
color: #fff;
|
|
font-family: "Sora", sans-serif;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: transform 0.2s ease, filter 0.2s ease;
|
|
}
|
|
|
|
.submit-btn:hover:not(:disabled) {
|
|
filter: brightness(1.05);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.submit-btn:disabled {
|
|
opacity: 0.8;
|
|
cursor: wait;
|
|
}
|
|
|
|
.panel-footer {
|
|
margin-top: 1.75rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
color: var(--muted);
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
.spin {
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes shell-enter {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(18px) scale(0.985);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes rise {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(14px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes backdrop-drift {
|
|
from {
|
|
transform: scale(1.04) translate3d(0, 0, 0);
|
|
}
|
|
to {
|
|
transform: scale(1.08) translate3d(-1.2%, -0.8%, 0);
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.login-shell {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.login-brand {
|
|
min-height: auto;
|
|
padding: 2rem 1.6rem 1.4rem;
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: 2.2rem;
|
|
}
|
|
|
|
.brand-copy {
|
|
max-width: none;
|
|
}
|
|
|
|
.login-panel {
|
|
padding: 1.75rem 1.4rem 2rem;
|
|
}
|
|
}
|
|
</style>
|