402 lines
9.9 KiB
Vue
402 lines
9.9 KiB
Vue
<template>
|
|
<div class="verify-shell">
|
|
<div class="verify-card">
|
|
<div class="verify-card__accent" aria-hidden="true"></div>
|
|
|
|
<div class="verify-header">
|
|
<button type="button" class="verify-back" @click="$router.go(-1)">
|
|
<span class="verify-back__icon" aria-hidden="true">
|
|
<svg viewBox="0 0 24 24" focusable="false">
|
|
<path
|
|
d="M15.5 19.5a1 1 0 0 1-.7-.3l-7-7a1 1 0 0 1 0-1.4l7-7a1 1 0 1 1 1.4 1.4L9.9 11.5l6.3 6.3a1 1 0 0 1-.7 1.7Z" />
|
|
</svg>
|
|
</span>
|
|
Kembali
|
|
</button>
|
|
|
|
<h1 class="verify-title">Kod Verifikasi</h1>
|
|
<p class="verify-subtitle">
|
|
<template v-if="!otpSent">
|
|
Sahkan nombor telefon anda. Kod akan dihantar ke nombor berakhir
|
|
<span class="verify-phone">+6 •••• •••• {{ phoneLastFour }}</span>
|
|
</template>
|
|
<template v-else>
|
|
Masukkan kod pengesahan yang dihantar ke nombor berakhir
|
|
<span class="verify-phone">+6 •••• •••• {{ phoneLastFour }}</span>
|
|
</template>
|
|
</p>
|
|
<p class="verify-note">
|
|
Jika nombor ini tidak betul atau anda tidak menerima OTP, sila pergi ke kaunter IT.
|
|
</p>
|
|
</div>
|
|
|
|
<form @submit.prevent="login" id="login_form" class="verify-form" novalidate>
|
|
<div v-if="otpSent && showDebugOtp" class="verify-debug">
|
|
OTP local development: <b>{{ debugOtp }}</b>
|
|
</div>
|
|
|
|
<div v-if="!otpSent" class="verify-actions">
|
|
<button type="button" class="btn verify-btn" :disabled="otpSending" @click="requestOtp">
|
|
<span v-if="otpSending" class="spinner-border spinner-border-sm mr-2" role="status"
|
|
aria-hidden="true"></span>
|
|
Hantar OTP
|
|
</button>
|
|
</div>
|
|
|
|
<div v-show="otpSent" class="verify-otp">
|
|
<v-otp-input ref="otpInput" input-classes="otp-input" separator="-" :num-inputs="6"
|
|
:should-auto-focus="true" :is-input-num="true" @on-change="handleOnChange"
|
|
@on-complete="handleOnComplete" />
|
|
</div>
|
|
|
|
<div v-if="otpSent" class="verify-links">
|
|
<button type="button" class="verify-link" @click="resendVerify">Hantar kembali OTP</button>
|
|
</div>
|
|
|
|
<div v-if="otpSent" class="verify-actions">
|
|
<button ref="submitbtn" type="submit" class="btn verify-btn verify-btn--primary" disabled>
|
|
Sahkan & Teruskan
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
export default {
|
|
data: function () {
|
|
return {
|
|
loading: false,
|
|
otpSent: false,
|
|
otpSending: false,
|
|
debugOtp: null
|
|
}
|
|
},
|
|
computed: {
|
|
showDebugOtp: function () {
|
|
return process.env.NODE_ENV !== 'production' && !!this.debugOtp;
|
|
},
|
|
phoneLastFour: function () {
|
|
var digits = String(this.$route.params.notel || '').replace(/\D/g, '');
|
|
if (digits.length >= 4) {
|
|
return digits.slice(-4);
|
|
}
|
|
return digits.length ? digits : '----';
|
|
}
|
|
},
|
|
methods: {
|
|
handleOnComplete(value) {
|
|
console.log("OTP completed: ", value);
|
|
this.$refs.submitbtn.disabled = false;
|
|
},
|
|
handleOnChange(value) {
|
|
console.log("OTP changed: ", value);
|
|
this.$refs.submitbtn.disabled = true;
|
|
},
|
|
handleClearInput() {
|
|
this.$refs.otpInput.clearInput();
|
|
},
|
|
requestOtp: function () {
|
|
this.sendOtp({ isResend: false });
|
|
},
|
|
resendVerify: function () {
|
|
this.sendOtp({ isResend: true });
|
|
},
|
|
sendOtp: function (opts) {
|
|
var vm = this;
|
|
var isResend = opts && opts.isResend;
|
|
this.otpSending = true;
|
|
axios.post(config.API + 'voter/login', {
|
|
no_kp: this.$route.params.nokp,
|
|
send_otp: true
|
|
})
|
|
.then(function (response) {
|
|
if (vm.util.showResult(response, 'success')) {
|
|
vm.otpSent = true;
|
|
if (response.data.debug_otp) {
|
|
vm.debugOtp = response.data.debug_otp;
|
|
}
|
|
if (isResend && vm.$refs.otpInput) {
|
|
vm.$refs.otpInput.clearInput();
|
|
}
|
|
vm.$nextTick(function () {
|
|
if (vm.$refs.submitbtn) {
|
|
vm.$refs.submitbtn.disabled = true;
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch(function (error) {
|
|
vm.util.showResult(error, 'error');
|
|
})
|
|
.then(function () {
|
|
vm.otpSending = false;
|
|
});
|
|
},
|
|
startLoading: function () {
|
|
this.util.notify('Logging in', 'loading');
|
|
this.loading = true;
|
|
},
|
|
stopLoading: function () {
|
|
$.notifyClose();
|
|
this.loading = false;
|
|
},
|
|
login: function () {
|
|
if (this.loading || !this.otpSent) return;
|
|
|
|
let vm = this;
|
|
|
|
this.startLoading();
|
|
console.log('OTP INPUT : ');
|
|
let otp = this.mergeOTP(this.$refs.otpInput.otp);
|
|
console.log(otp);
|
|
axios.post(config.API + 'voter/verify', {
|
|
'no_kp': this.$route.params.nokp,
|
|
'token': otp
|
|
})
|
|
.then(response => {
|
|
vm.stopLoading();
|
|
if (this.util.showResult(response, 'success')) {
|
|
localStorage.removeItem('admin_role');
|
|
localStorage.removeItem('admin_session');
|
|
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
|
this.util.setAuthorization();
|
|
vm.$router.push({ name: 'Voter Home' });
|
|
}
|
|
})
|
|
.catch(error => {
|
|
vm.stopLoading();
|
|
this.util.showResult(error, 'error');
|
|
})
|
|
|
|
},
|
|
mergeOTP: function (OTP) {
|
|
return OTP.join('');
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
<style scoped>
|
|
.verify-shell {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 32px 16px;
|
|
background: #f6f7fb;
|
|
}
|
|
|
|
.verify-card {
|
|
width: 100%;
|
|
max-width: 520px;
|
|
background: #ffffff;
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
border-radius: 20px;
|
|
box-shadow:
|
|
0 18px 55px rgba(2, 6, 23, 0.12),
|
|
0 2px 10px rgba(2, 6, 23, 0.06);
|
|
padding: 22px 22px 18px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.verify-card__accent {
|
|
position: absolute;
|
|
inset: 0 0 auto 0;
|
|
height: 4px;
|
|
background: linear-gradient(90deg, #22c55e 0%, #3b82f6 55%, #a855f7 100%);
|
|
}
|
|
|
|
.verify-header {
|
|
padding-top: 6px;
|
|
}
|
|
|
|
.verify-back {
|
|
appearance: none;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 8px 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: rgba(15, 23, 42, 0.7);
|
|
font-weight: 800;
|
|
}
|
|
|
|
.verify-back:hover {
|
|
color: rgba(15, 23, 42, 0.85);
|
|
}
|
|
|
|
.verify-back__icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
display: inline-flex;
|
|
color: currentColor;
|
|
}
|
|
|
|
.verify-back__icon svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
fill: currentColor;
|
|
}
|
|
|
|
.verify-title {
|
|
margin: 6px 0 0;
|
|
font-size: 22px;
|
|
font-weight: 900;
|
|
letter-spacing: -0.02em;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.verify-subtitle {
|
|
margin: 8px 0 0;
|
|
font-size: 14px;
|
|
color: rgba(15, 23, 42, 0.72);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.verify-phone {
|
|
display: inline-block;
|
|
margin-left: 6px;
|
|
font-weight: 900;
|
|
color: rgba(15, 23, 42, 0.88);
|
|
}
|
|
|
|
.verify-note {
|
|
margin: 10px 0 0;
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
color: rgba(15, 23, 42, 0.56);
|
|
}
|
|
|
|
.verify-form {
|
|
margin-top: 18px;
|
|
}
|
|
|
|
.verify-debug {
|
|
margin: 0 0 14px;
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
|
background: rgba(59, 130, 246, 0.08);
|
|
color: rgba(15, 23, 42, 0.9);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.verify-otp {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin: 18px 0 14px;
|
|
max-width: 360px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
|
|
/* v-otp-input renders inputs inside a child component.
|
|
Use deep selectors so scoped styles apply. */
|
|
::v-deep .otp-input {
|
|
width: 40px !important;
|
|
min-width: 40px !important;
|
|
max-width: 40px !important;
|
|
height: 46px !important;
|
|
padding: 6px !important;
|
|
margin: 0 6px !important;
|
|
font-size: 18px !important;
|
|
font-weight: 900 !important;
|
|
border-radius: 14px !important;
|
|
border: 1px solid rgba(15, 23, 42, 0.14) !important;
|
|
text-align: center !important;
|
|
box-sizing: border-box !important;
|
|
transition: box-shadow 160ms ease, border-color 160ms ease, transform 160ms ease;
|
|
}
|
|
|
|
::v-deep .otp-input.error {
|
|
border-color: rgba(239, 68, 68, 0.9) !important;
|
|
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.14) !important;
|
|
}
|
|
|
|
::v-deep .otp-input:focus {
|
|
outline: none !important;
|
|
border-color: rgba(59, 130, 246, 0.55) !important;
|
|
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14) !important;
|
|
}
|
|
|
|
::v-deep .otp-input::-webkit-inner-spin-button,
|
|
::v-deep .otp-input::-webkit-outer-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
|
|
.verify-links {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin: 10px 0 16px;
|
|
}
|
|
|
|
.verify-link {
|
|
appearance: none;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 6px 10px;
|
|
color: rgba(29, 78, 216, 0.92);
|
|
font-weight: 900;
|
|
}
|
|
|
|
.verify-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.verify-actions {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.verify-btn {
|
|
width: 100%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 14px;
|
|
padding: 13px 14px;
|
|
font-weight: 900;
|
|
letter-spacing: 0.01em;
|
|
color: #ffffff;
|
|
background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%);
|
|
border: 0;
|
|
box-shadow: 0 12px 24px rgba(29, 78, 216, 0.22);
|
|
transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease;
|
|
}
|
|
|
|
.verify-btn:hover {
|
|
filter: brightness(1.03);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 16px 34px rgba(29, 78, 216, 0.26);
|
|
}
|
|
|
|
.verify-btn:disabled {
|
|
opacity: 0.72;
|
|
filter: saturate(0.9);
|
|
transform: none;
|
|
box-shadow: none;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
@media (max-width: 576px) {
|
|
.verify-shell {
|
|
padding: 20px 14px;
|
|
}
|
|
|
|
.verify-card {
|
|
padding: 20px 18px 16px;
|
|
}
|
|
|
|
::v-deep .otp-input {
|
|
width: 38px !important;
|
|
min-width: 38px !important;
|
|
max-width: 38px !important;
|
|
height: 44px !important;
|
|
margin: 0 5px !important;
|
|
}
|
|
}
|
|
</style>
|