initial: bootstrap from BukidBountyApp base
This commit is contained in:
264
resources/js/Pages/UserRegistration.vue
Normal file
264
resources/js/Pages/UserRegistration.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user