265 lines
8.1 KiB
Vue
265 lines
8.1 KiB
Vue
<script setup>
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
usePageTitle('User Registration');
|
|
|
|
import { ref, computed } from 'vue'
|
|
import axios from 'axios'
|
|
import { useModal } from '../composables/Core/useModal'
|
|
import { h } from 'vue'
|
|
|
|
const modal = useModal()
|
|
|
|
// Form state
|
|
const mobileNumber = ref('')
|
|
const nickname = ref('')
|
|
const name = ref('')
|
|
const password = ref('')
|
|
const confirmPassword = ref('')
|
|
|
|
// Validation states
|
|
const isMobileValid = ref(false)
|
|
const isPasswordValid = ref(false)
|
|
const isConfirmPasswordValid = ref(false)
|
|
const isCheckingMobile = ref(false)
|
|
|
|
const isNameValid = computed(() => name.value.trim().length > 0)
|
|
const hasValidMobileNumberFormat = (number) => /^(09|\+639)\d{9}$/.test(number)
|
|
|
|
const missingFields = computed(() => {
|
|
const fields = []
|
|
if (!mobileNumber.value) {
|
|
fields.push('Mobile Number')
|
|
} else if (!isMobileValid.value) {
|
|
if (isCheckingMobile.value) {
|
|
fields.push('Checking Mobile Number...')
|
|
} else {
|
|
fields.push('Valid and Available Mobile Number')
|
|
}
|
|
}
|
|
if (!name.value.trim()) fields.push('Name')
|
|
if (password.value.length < 6) fields.push('Password (min 6 characters)')
|
|
if (password.value !== confirmPassword.value || !confirmPassword.value) {
|
|
fields.push('Passwords matching')
|
|
}
|
|
return fields
|
|
})
|
|
|
|
const isLoading = ref(false)
|
|
|
|
const debounce = (fn, delay) => {
|
|
let timeoutId
|
|
return (...args) => {
|
|
if (timeoutId) clearTimeout(timeoutId)
|
|
timeoutId = setTimeout(() => fn(...args), delay)
|
|
}
|
|
}
|
|
|
|
const checkMobileExists = async () => {
|
|
const number = mobileNumber.value
|
|
if (!number || !hasValidMobileNumberFormat(number)) {
|
|
isMobileValid.value = false
|
|
return false
|
|
}
|
|
try {
|
|
isCheckingMobile.value = true
|
|
const res = await axios.post('/api/public/user/check-mobile', { mobile_number: number })
|
|
if (res.data?.exists === true) {
|
|
isMobileValid.value = false
|
|
return false
|
|
}
|
|
isMobileValid.value = true
|
|
validateAllInputs()
|
|
return true
|
|
} catch {
|
|
isMobileValid.value = false
|
|
return false
|
|
} finally {
|
|
isCheckingMobile.value = false
|
|
}
|
|
}
|
|
|
|
const debouncedCheckMobile = debounce(checkMobileExists, 600)
|
|
const handleMobileChange = () => { isMobileValid.value = false; debouncedCheckMobile(); }
|
|
|
|
const validatePassword = () => {
|
|
isPasswordValid.value = password.value.length >= 6
|
|
validateAllInputs()
|
|
return isPasswordValid.value
|
|
}
|
|
const validateConfirmPassword = () => {
|
|
isConfirmPasswordValid.value = password.value === confirmPassword.value && confirmPassword.value.length >= 6
|
|
validateAllInputs()
|
|
return isConfirmPasswordValid.value
|
|
}
|
|
const validateAllInputs = () => {}
|
|
|
|
const handleFormSubmit = () => {
|
|
if (missingFields.value.length > 0) {
|
|
modal.open({
|
|
title: 'Missing Requirements',
|
|
body: h('div', { class: 'p-3' }, [
|
|
h('div', { class: 'd-flex align-items-center mb-3 text-warning' }, [
|
|
h('i', { class: 'fas fa-exclamation-circle fa-2x me-3' }),
|
|
h('span', { class: 'fw_7 h5 mb-0' }, 'Required Fields')
|
|
]),
|
|
h('p', { class: 'text-muted mb-3' }, 'Please complete the following:'),
|
|
h('div', { class: 'd-flex flex-wrap gap-2' },
|
|
missingFields.value.map(field =>
|
|
h('span', { class: 'badge bg-light text-danger border border-danger-subtle rounded-pill px-3 py-2 fw_6' }, field)
|
|
)
|
|
)
|
|
]),
|
|
footer: h('button', {
|
|
class: 'btn btn-primary w-100 py-3 rounded-xl fw_7 shadow-sm',
|
|
onClick: () => modal.close()
|
|
}, 'I Understand')
|
|
})
|
|
return
|
|
}
|
|
registerUser()
|
|
}
|
|
|
|
const registerUser = async () => {
|
|
try {
|
|
isLoading.value = true
|
|
const payload = {
|
|
mobile_number: String(mobileNumber.value),
|
|
password: password.value,
|
|
nickname: nickname.value || '',
|
|
name: name.value,
|
|
}
|
|
|
|
const response = await axios.post('/api/public/user/register', payload)
|
|
|
|
if (response.data?.success) {
|
|
// Navigate to login page on success
|
|
setTimeout(() => {
|
|
window.location.href = '/login'
|
|
}, 1500)
|
|
}
|
|
} catch (error) {
|
|
const data = error.response?.data
|
|
const messages = []
|
|
if (data?.errors && typeof data.errors === 'object') {
|
|
for (const fieldMessages of Object.values(data.errors)) {
|
|
if (Array.isArray(fieldMessages)) messages.push(...fieldMessages)
|
|
else if (typeof fieldMessages === 'string') messages.push(fieldMessages)
|
|
}
|
|
}
|
|
if (data?.message) messages.push(data.message)
|
|
const msgText = messages.length ? messages.join('\n') : 'Registration failed. Please try again.'
|
|
modal.open({
|
|
title: 'Registration Failed',
|
|
body: h('div', { class: 'p-3 text-danger' }, msgText),
|
|
footer: null
|
|
})
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const clearForm = () => {
|
|
mobileNumber.value = ''
|
|
nickname.value = ''
|
|
name.value = ''
|
|
password.value = ''
|
|
confirmPassword.value = ''
|
|
isMobileValid.value = false
|
|
isPasswordValid.value = false
|
|
isConfirmPasswordValid.value = false
|
|
}
|
|
|
|
const goToLogin = () => { window.location.href = '/login' }
|
|
</script>
|
|
|
|
<template>
|
|
<div class="user-registration-page pb-5">
|
|
<br><br>
|
|
|
|
<div class="tf-container">
|
|
<h2 class="fw_6 text-center mb-4">Create Your Account</h2>
|
|
<p class="text-muted text-center mb-4">Register to start exploring BukidBounty</p>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<!-- Mobile Number -->
|
|
<div class="col-12">
|
|
<div class="input-group">
|
|
<span class="input-group-text" :class="isMobileValid ? 'bg-success' : ''">
|
|
<i v-if="isCheckingMobile" class="fas fa-spinner fa-spin"></i>
|
|
<i v-else-if="isMobileValid" class="fas fa-check text-white"></i>
|
|
</span>
|
|
<input type="text" class="form-control" placeholder="Mobile Number (e.g., 09123456789)"
|
|
v-model="mobileNumber" @input="handleMobileChange">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nickname -->
|
|
<div class="col-12">
|
|
<input type="text" class="form-control" placeholder="Nick Name (Optional)"
|
|
v-model.trim="nickname" :disabled="isLoading">
|
|
</div>
|
|
|
|
<!-- Name -->
|
|
<div class="col-12">
|
|
<input type="text" class="form-control" placeholder="Full Name (Required)"
|
|
v-model.trim="name" @input="validateAllInputs" :disabled="isLoading">
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<div class="col-12">
|
|
<input type="password" class="form-control" placeholder="Password (min 6 characters)"
|
|
v-model.trim="password" @input="validatePassword(); validateConfirmPassword()" :disabled="isLoading">
|
|
</div>
|
|
|
|
<!-- Confirm Password -->
|
|
<div class="col-12">
|
|
<div class="input-group">
|
|
<span class="input-group-text" :class="isConfirmPasswordValid ? 'bg-success' : ''"></span>
|
|
<input type="password" class="form-control" placeholder="Confirm Password"
|
|
v-model.trim="confirmPassword" @input="validateConfirmPassword()" :disabled="isLoading">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 mt-3">
|
|
<AnimatedButton
|
|
id="RegisterNowButton"
|
|
btnClass="btn btn-primary w-100 py-3 shadow-lg rounded-xl fw_7"
|
|
@click="handleFormSubmit"
|
|
:loading="isLoading"
|
|
>
|
|
Register Account
|
|
</AnimatedButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-center mt-4">
|
|
<p class="text-muted">
|
|
Already have an account?
|
|
<a href="/login" class="text-primary fw_6">Log in here</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.card {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.rounded-xl {
|
|
border-radius: 15px;
|
|
}
|
|
|
|
.headline-gradient {
|
|
background: linear-gradient(135deg, #42b983 0%, #2c3e50 100%);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
</style>
|