TKK_E32230908/resources/views/user/member/create.blade.php

675 lines
25 KiB
PHP

@extends('layouts.dashboard')
@section('content')
<link href="https://fonts.googleapis.com/css2?family=Inter:wght=400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/css/intlTelInput.css">
<style>
html, body {
-ms-overflow-style: none;
scrollbar-width: none;
}
.page-wrapper {
font-family: 'Inter', sans-serif;
background-color: #f8fafc;
padding: 40px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.main-card {
width: 100%;
max-width: 1000px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
overflow: hidden;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card-header-pro {
padding: 30px 40px;
background: #ffffff;
border-bottom: 1px solid #f1f5f9;
}
.card-header-pro h2 {
font-weight: 800;
color: #1e293b;
margin: 0;
}
.form-body { padding: 40px; }
.section-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.8rem;
font-weight: 700;
color: #6366f1;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 25px;
}
.section-title::after {
content: "";
flex: 1;
height: 1px;
background: #e2e8f0;
}
.label-text {
font-size: 0.875rem;
font-weight: 600;
color: #475569;
margin-bottom: 8px;
display: block;
}
.input-pro {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #cbd5e1;
transition: all 0.2s ease;
font-size: 0.95rem;
}
.input-pro:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
}
.input-pro.is-invalid {
border-color: #ef4444 !important;
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1) !important;
}
.input-pro.is-valid {
border-color: #10b981 !important;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1) !important;
}
.input-pro[readonly] {
background-color: #f1f5f9;
cursor: not-allowed;
}
.iti { width: 100%; }
.iti__country-list { z-index: 10; }
.phone-feedback {
font-size: 0.8rem;
margin-top: 6px;
display: none;
align-items: center;
gap: 6px;
font-weight: 500;
}
.phone-feedback.valid-msg { color: #10b981; }
.phone-feedback.invalid-msg { color: #ef4444; }
.photo-upload-wrapper {
display: flex;
flex-direction: column;
gap: 10px;
background: #f8fafc;
padding: 15px;
border-radius: 12px;
border: 1px dashed #cbd5e1;
}
.photo-upload-main {
display: flex;
align-items: center;
gap: 20px;
}
.img-preview {
width: 70px;
height: 70px;
border-radius: 50%;
object-fit: cover;
display: none;
border: 2px solid #6366f1;
}
.form-footer {
padding: 25px 40px;
background: #f8fafc;
border-top: 1px solid #f1f5f9;
display: flex;
justify-content: flex-end;
gap: 15px;
}
.btn-save {
background: #6366f1;
color: white;
padding: 12px 30px;
border-radius: 10px;
font-weight: 600;
border: none;
transition: 0.3s;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-save:hover:not(:disabled) {
background: #4f46e5;
transform: translateY(-1px);
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3);
}
.btn-save:disabled {
background: #94a3b8;
cursor: not-allowed;
}
.alert-custom {
padding: 15px 20px;
border-radius: 10px;
margin-bottom: 25px;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 10px;
}
.alert-danger { background: #fee2e2; color: #b91c1c; border: 1px solid #fecaca; }
@media (max-width: 768px) {
.form-footer { flex-direction: column; }
.btn-save { width: 100%; justify-content: center; }
}
</style>
<div class="page-wrapper">
<div class="main-card">
<div class="card-header-pro">
<h2>Pendaftaran Anggota</h2>
<p class="text-muted mb-0">Lengkapi profil Anda. Satu akun hanya dapat mendaftar satu kali.</p>
</div>
<div class="form-body">
@if(session('error'))
<div class="alert-custom alert-danger">
<i class="fas fa-exclamation-triangle"></i>
{{ session('error') }}
</div>
@endif
<div id="clientErrorAlert" class="alert-custom alert-danger" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<span id="clientErrorMessage"></span>
</div>
@if($errors->any())
<div class="alert-custom alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form id="memberForm" action="{{ route('user.member.store') }}" method="POST" enctype="multipart/form-data" novalidate>
@csrf
<div class="section-title">Informasi Pribadi</div>
<div class="row g-4 mb-5">
<div class="col-md-6">
<label class="label-text">Nama Lengkap</label>
<input type="text" class="input-pro" value="{{ Auth::user()->name }}" readonly>
<input type="hidden" name="nama" value="{{ Auth::user()->name }}">
</div>
<div class="col-md-6">
<label class="label-text">Email</label>
<input type="email" class="input-pro" value="{{ Auth::user()->email }}" readonly>
<input type="hidden" name="email" value="{{ Auth::user()->email }}">
</div>
<div class="col-md-6">
<label class="label-text">NIK / NIP</label>
<input type="text" name="nik_nip" id="nik_nip" class="input-pro" placeholder="Masukkan NIK (16 digit) atau NIP (18 digit)..." minlength="16" maxlength="18" required value="{{ old('nik_nip') }}">
</div>
<div class="col-md-6">
<label class="label-text">Jenis Kelamin</label>
<!-- Mengganti class form-control menjadi input-pro agar seragam & ditambahkan atribut required -->
<select name="gender" id="gender" class="input-pro form-select" required>
<option value="">-- Pilih Jenis Kelamin --</option>
<option value="Laki-laki" {{ old('gender') == 'Laki-laki' ? 'selected' : '' }}>Laki-laki</option>
<option value="Perempuan" {{ old('gender') == 'Perempuan' ? 'selected' : '' }}>Perempuan</option>
</select>
</div>
<div class="col-md-6">
<label class="label-text">Nomor Telepon / WhatsApp</label>
<input type="tel" id="no_hp_display" class="input-pro" placeholder="8123456xxx" required autocomplete="off">
<input type="hidden" name="no_hp" id="no_hp_hidden" value="{{ old('no_hp') }}">
<div id="valid-msg" class="phone-feedback valid-msg"><i class="fas fa-check-circle"></i> Nomor sah</div>
<div id="invalid-msg" class="phone-feedback invalid-msg"><i class="fas fa-times-circle"></i> Nomor tidak valid</div>
</div>
<div class="col-12">
<label class="label-text">Foto Profil</label>
<div class="photo-upload-wrapper">
<div class="photo-upload-main">
<img id="imgPreview" src="#" alt="Preview" class="img-preview">
<input type="file" name="foto" id="foto" class="form-control border-0 bg-transparent" accept="image/jpeg,image/png,image/jpg" onchange="previewImage(this)" required>
</div>
<small style="color: #64748b; font-size: 0.825rem; display: block; margin-top: 4px;">
<i class="fas fa-info-circle" style="color: #6366f1;"></i> Format berkas yang didukung: JPG, JPEG, PNG. Ukuran berkas maksimal <strong>5 MB</strong>.
</small>
</div>
</div>
</div>
<div class="section-title">
Alamat Domisili
<span id="loader" class="ms-2" style="display:none;"><i class="fas fa-spinner fa-spin"></i> Memuat...</span>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="label-text">Provinsi</label>
<select id="provinsi" name="provinsi_id" class="input-pro form-select" required>
<option value="">Pilih Provinsi</option>
</select>
<input type="hidden" name="provinsi" id="provinsi_nama">
</div>
<div class="col-md-6">
<label class="label-text">Kota / Kabupaten</label>
<select id="kota" name="kota_id" class="input-pro form-select" required disabled>
<option value="">Pilih Kota</option>
</select>
<input type="hidden" name="kota" id="kota_nama">
</div>
<div class="col-md-6">
<label class="label-text">Kecamatan</label>
<select id="kecamatan" name="kecamatan_id" class="input-pro form-select" required disabled>
<option value="">Pilih Kecamatan</option>
</select>
<input type="hidden" name="kecamatan" id="kecamatan_nama">
</div>
<div class="col-md-6">
<label class="label-text">Desa / Kelurahan</label>
<select id="desa" name="desa_id" class="input-pro form-select" required disabled>
<option value="">Pilih Desa</option>
</select>
<input type="hidden" name="desa" id="desa_nama">
</div>
<div class="col-md-4">
<label class="label-text">Kode Pos</label>
<input type="text" name="kode_pos" id="kode_pos" class="input-pro" placeholder="Masukkan 5 digit kode pos..." maxlength="5" required value="{{ old('kode_pos') }}">
</div>
<div class="col-md-8">
<label class="label-text">Alamat Lengkap</label>
<input type="text" name="alamat" id="alamat" class="input-pro" placeholder="Lengkapi Alamat Lengkap Anda" minlength="10" maxlength="255" required value="{{ old('alamat') }}">
</div>
</div>
<div class="form-footer mt-4">
<button type="button" class="btn-cancel" onclick="window.history.back()" style="background:none; border:none; color:#64748b; cursor:pointer;">Batal</button>
<button type="submit" id="btnSubmit" class="btn-save">
<i class="fas fa-check-circle"></i> Simpan Data Anggota
</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/intlTelInput.min.js"></script>
<script>
const phoneInput = document.getElementById("no_hp_display");
const hiddenPhoneInput = document.getElementById("no_hp_hidden");
const clientAlert = document.getElementById("clientErrorAlert");
const clientMessage = document.getElementById("clientErrorMessage");
const validMsg = document.getElementById("valid-msg");
const invalidMsg = document.getElementById("invalid-msg");
const iti = window.intlTelInput(phoneInput, {
initialCountry: "id",
separateDialCode: true,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
function resetPhoneValidation() {
phoneInput.classList.remove("is-valid", "is-invalid");
validMsg.style.display = "none";
invalidMsg.style.display = "none";
}
function checkPhoneNumberRealtime() {
resetPhoneValidation();
const value = phoneInput.value.trim();
if (value) {
if (iti.isValidNumber()) {
phoneInput.classList.add("is-valid");
validMsg.style.display = "flex";
hiddenPhoneInput.value = iti.getNumber();
} else {
phoneInput.classList.add("is-invalid");
invalidMsg.style.display = "flex";
hiddenPhoneInput.value = "";
}
} else {
hiddenPhoneInput.value = "";
}
}
phoneInput.addEventListener('keyup', checkPhoneNumberRealtime);
phoneInput.addEventListener('blur', checkPhoneNumberRealtime);
phoneInput.addEventListener('change', checkPhoneNumberRealtime);
phoneInput.addEventListener('countrychange', checkPhoneNumberRealtime);
phoneInput.addEventListener('input', function() {
let value = this.value.replace(/[^0-9]/g, '');
if (value.startsWith('62')) {
value = value.substring(2);
}
while (value.startsWith('0')) {
value = value.substring(1);
}
this.value = value;
});
document.getElementById('nik_nip').addEventListener('input', function() {
this.value = this.value.replace(/[^0-9]/g, '');
});
document.getElementById('kode_pos').addEventListener('input', function() {
this.value = this.value.replace(/[^0-9]/g, '');
});
document.querySelectorAll('.input-pro:not([readonly])').forEach(input => {
input.addEventListener('blur', function() {
this.value = this.value.trim();
});
});
function previewImage(input) {
const preview = document.getElementById('imgPreview');
clientAlert.style.display = 'none';
if (input.files && input.files[0]) {
const file = input.files[0];
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
if (!allowedTypes.includes(file.type)) {
showAlert("Format berkas tidak valid! Hanya diperbolehkan gambar berformat JPG, JPEG, atau PNG.", input);
input.value = "";
preview.style.display = 'none';
return;
}
const maxByteSize = 5 * 1024 * 1024;
if (file.size > maxByteSize) {
showAlert("Ukuran gambar terlalu besar! Batas maksimal ukuran berkas adalah 5 MB.", input);
input.value = "";
preview.style.display = 'none';
return;
}
const reader = new FileReader();
reader.onload = (e) => {
preview.src = e.target.result;
preview.style.display = 'block';
input.classList.remove('is-invalid');
}
reader.readAsDataURL(file);
}
}
function showAlert(message, focusElement = null) {
clientAlert.style.display = 'flex';
clientMessage.innerText = message;
window.scrollTo({ top: 0, behavior: 'smooth' });
if (focusElement) {
focusElement.classList.add('is-invalid');
focusElement.focus();
}
}
// Menghapus status error saat user mengetik/memilih ulang
document.querySelectorAll('.input-pro, select, input[type="file"]').forEach(item => {
item.addEventListener('input', function() {
this.classList.remove('is-invalid');
});
item.addEventListener('change', function() {
this.classList.remove('is-invalid');
});
});
const form = document.getElementById('memberForm');
const btnSubmit = document.getElementById('btnSubmit');
form.addEventListener('submit', function(e) {
clientAlert.style.display = 'none';
const nikNip = document.getElementById('nik_nip');
const gender = document.getElementById('gender');
const phoneDisplay = document.getElementById('no_hp_display');
const foto = document.getElementById('foto');
const kodePos = document.getElementById('kode_pos');
const alamat = document.getElementById('alamat');
const provinsi = document.getElementById('provinsi');
const kota = document.getElementById('kota');
const kecamatan = document.getElementById('kecamatan');
const desa = document.getElementById('desa');
// 1. Validasi NIK / NIP wajib diisi & sesuai panjang digit
if (!nikNip.value.trim()) {
e.preventDefault();
showAlert("NIK / NIP wajib diisi!", nikNip);
return false;
}
const valLen = nikNip.value.trim().length;
if (valLen !== 16 && valLen !== 18) {
e.preventDefault();
showAlert("Identitas tidak valid! Panjang data harus tepat 16 digit jika NIK, atau 18 digit jika NIP.", nikNip);
return false;
}
// 2. Validasi Jenis Kelamin
if (!gender.value) {
e.preventDefault();
showAlert("Harap pilih jenis kelamin Anda.", gender);
return false;
}
// 3. Validasi Nomor Telepon
if (!phoneDisplay.value.trim()) {
e.preventDefault();
showAlert("Nomor Telepon / WhatsApp wajib diisi!", phoneDisplay);
return false;
}
if (!iti.isValidNumber()) {
e.preventDefault();
showAlert("Format nomor Telepon / WhatsApp tidak sah atau jumlah digit salah!", phoneDisplay);
return false;
}
// 4. Validasi Foto Profil Wajib Diunggah
if (foto.files.length === 0) {
e.preventDefault();
showAlert("Foto profil wajib diunggah!", foto);
return false;
}
// 5. Validasi Struktur Alamat Dropdown
if (!provinsi.value) { e.preventDefault(); showAlert("Harap pilih Provinsi Anda.", provinsi); return false; }
if (!kota.value) { e.preventDefault(); showAlert("Harap pilih Kota / Kabupaten Anda.", kota); return false; }
if (!kecamatan.value) { e.preventDefault(); showAlert("Harap pilih Kecamatan Anda.", kecamatan); return false; }
if (!desa.value) { e.preventDefault(); showAlert("Harap pilih Desa / Kelurahan Anda.", desa); return false; }
// 6. Validasi Kode Pos
if (!kodePos.value.trim()) {
e.preventDefault();
showAlert("Kode Pos wajib diisi!", kodePos);
return false;
}
if (kodePos.value.trim().length !== 5) {
e.preventDefault();
showAlert("Kode Pos Indonesia tidak valid! Harus berisikan tepat 5 digit angka.", kodePos);
return false;
}
// 7. Validasi Alamat Fisik
// Menghapus spasi di ujung input dan mengecek teks asli tambahan selain template otomatis wilayah
const alamatVal = alamat.value.trim();
if (!alamatVal) {
e.preventDefault();
showAlert("Alamat Lengkap wajib diisi!", alamat);
return false;
}
if (alamatVal.length < 15) {
e.preventDefault();
showAlert("Tuliskan alamat domisili secara lebih detail dan lengkap (Minimal 15 karakter).", alamat);
return false;
}
btnSubmit.disabled = true;
btnSubmit.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Memproses...';
});
// =========================================================================
// MODUL API WILAYAH INDONESIA - EMSIFA
// =========================================================================
const apiBase = 'https://www.emsifa.com/api-wilayah-indonesia/api';
const sel = {
prov: document.getElementById('provinsi'),
kota: document.getElementById('kota'),
kec: document.getElementById('kecamatan'),
desa: document.getElementById('desa'),
load: document.getElementById('loader')
};
const hid = {
prov: document.getElementById('provinsi_nama'),
kota: document.getElementById('kota_nama'),
kec: document.getElementById('kecamatan_nama'),
desa: document.getElementById('desa_nama')
};
function sortWilayah(array) {
return array.sort((a, b) => a.name.localeCompare(b.name));
}
async function fetchData(url) {
sel.load.style.display = 'inline';
try {
const response = await fetch(url);
return await response.json();
} catch (e) {
console.error("Gagal memuat data wilayah");
return [];
} finally {
sel.load.style.display = 'none';
}
}
window.addEventListener('load', async () => {
let data = await fetchData(`${apiBase}/provinces.json`);
data = sortWilayah(data);
data.forEach(p => sel.prov.add(new Option(p.name, p.id)));
});
sel.prov.addEventListener('change', async function() {
resetSelect(['kota', 'kec', 'desa']);
if (this.value) {
hid.prov.value = this.options[this.selectedIndex].text;
let data = await fetchData(`${apiBase}/regencies/${this.value}.json`);
data = sortWilayah(data);
sel.kota.disabled = false;
data.forEach(r => sel.kota.add(new Option(r.name, r.id)));
}
});
sel.kota.addEventListener('change', async function() {
resetSelect(['kec', 'desa']);
if (this.value) {
hid.kota.value = this.options[this.selectedIndex].text;
let data = await fetchData(`${apiBase}/districts/${this.value}.json`);
data = sortWilayah(data);
sel.kec.disabled = false;
data.forEach(d => sel.kec.add(new Option(d.name, d.id)));
}
});
sel.kec.addEventListener('change', async function() {
resetSelect(['desa']);
if (this.value) {
hid.kec.value = this.options[this.selectedIndex].text;
let data = await fetchData(`${apiBase}/villages/${this.value}.json`);
data = sortWilayah(data);
sel.desa.disabled = false;
data.forEach(v => sel.desa.add(new Option(v.name, v.id)));
}
});
sel.desa.addEventListener('change', async function() {
const alamatInput = document.getElementById('alamat');
const kodePosInput = document.getElementById('kode_pos');
if (this.value) {
hid.desa.value = this.options[this.selectedIndex].text;
const namaProv = hid.prov.value;
const namaKota = hid.kota.value;
const namaKec = hid.kec.value;
const namaDesa = hid.desa.value;
alamatInput.value = `Desa/Kel. ${namaDesa}, Kec. ${namaKec}, ${namaKota}, Provinsi ${namaProv}. `;
alamatInput.focus();
try {
const query = encodeURIComponent(`${namaDesa} ${namaKec}`);
const response = await fetch(`https://api.allorigins.win/get?url=${encodeURIComponent('https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&countrycodes=id&q=' + query)}`);
const wrapper = await response.json();
const geoData = JSON.parse(wrapper.contents);
if (geoData && geoData.length > 0 && geoData[0].address && geoData[0].address.postcode) {
const postal = geoData[0].address.postcode.replace(/[^0-9]/g, '');
if(postal.length === 5) {
kodePosInput.value = postal;
}
}
} catch (err) {
console.log("Menunggu input manual kode pos oleh user.");
}
} else {
alamatInput.value = '';
kodePosInput.value = '';
}
});
function resetSelect(keys) {
keys.forEach(k => {
sel[k].innerHTML = `<option value="">Pilih ${k.charAt(0).toUpperCase() + k.slice(1)}</option>`;
sel[k].disabled = true;
});
document.getElementById('alamat').value = '';
document.getElementById('kode_pos').value = '';
}
</script>
@endsection