Files
setrip/features/organizer/components/verify-form.tsx
T
2026-05-21 11:59:02 +07:00

357 lines
11 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { IdCard, Image as ImageIcon, Landmark, Check } from "lucide-react";
import { submitVerificationAction } from "@/features/organizer/actions";
import { DateField } from "@/components/shared/date-picker";
type Initial = {
fullName: string;
nik: string;
birthDate: Date;
address: string;
ktpImageKey: string;
livenessKey: string;
bankName: string;
bankAccountNumber: string;
bankAccountName: string;
} | null;
type UploadKind = "ktp" | "liveness";
const ACCEPT_MIME = "image/jpeg,image/png,image/webp";
const MAX_BYTES = 5 * 1024 * 1024;
export function VerifyForm({ initial }: { initial: Initial }) {
const router = useRouter();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [ktpKey, setKtpKey] = useState(initial?.ktpImageKey ?? "");
const [livenessKey, setLivenessKey] = useState(initial?.livenessKey ?? "");
// `birthDate` dari DB tersimpan sebagai tengah malam UTC — baca pakai getter
// UTC supaya hari kalender yang tampil di picker tidak bergeser.
const [birthDate, setBirthDate] = useState<Date | null>(
initial
? new Date(
initial.birthDate.getUTCFullYear(),
initial.birthDate.getUTCMonth(),
initial.birthDate.getUTCDate()
)
: null
);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
if (!birthDate) {
setError("Tanggal lahir wajib diisi");
return;
}
if (!ktpKey || !livenessKey) {
setError("Foto KTP dan foto memegang kertas SETRIP wajib diunggah");
return;
}
setLoading(true);
const formData = new FormData(e.currentTarget);
formData.set("ktpImageKey", ktpKey);
formData.set("livenessKey", livenessKey);
const result = await submitVerificationAction(formData);
setLoading(false);
if (result.error) {
setError(result.error);
return;
}
router.refresh();
}
const inputCls =
"w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 transition-colors placeholder:text-neutral-400 focus:bg-white";
return (
<form
onSubmit={handleSubmit}
className="space-y-5 rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm sm:p-8"
>
{error && (
<div className="rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
{error}
</div>
)}
<section>
<h2 className="mb-3 flex items-center gap-2 text-base font-bold text-neutral-900">
<IdCard
size={18}
strokeWidth={1.75}
aria-hidden
className="text-primary-600"
/>
Data KTP
</h2>
<div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
Nama Lengkap (sesuai KTP)
</label>
<input
name="fullName"
type="text"
required
defaultValue={initial?.fullName ?? ""}
className={inputCls}
placeholder="Mis. Budi Santoso"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
NIK (16 digit)
</label>
<input
name="nik"
type="text"
inputMode="numeric"
pattern="\d{16}"
maxLength={16}
required
defaultValue={initial?.nik ?? ""}
className={inputCls}
placeholder="3201xxxxxxxxxxxx"
/>
</div>
<div>
<label
htmlFor="birthDate"
className="mb-1.5 block text-sm font-semibold text-neutral-700"
>
Tanggal Lahir
</label>
<DateField
id="birthDate"
name="birthDate"
value={birthDate}
onChange={setBirthDate}
maxDate={new Date()}
withMonthYearDropdown
required
placeholder="Pilih tanggal lahir"
/>
</div>
<div className="sm:col-span-2">
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
Alamat (sesuai KTP)
</label>
<textarea
name="address"
required
rows={3}
defaultValue={initial?.address ?? ""}
className={inputCls}
placeholder="Jalan, RT/RW, kelurahan, kota, provinsi"
/>
</div>
</div>
</section>
<section>
<h2 className="mb-3 flex items-center gap-2 text-base font-bold text-neutral-900">
<ImageIcon
size={18}
strokeWidth={1.75}
aria-hidden
className="text-primary-600"
/>
Foto
</h2>
<p className="mb-3 text-xs text-neutral-500">
Foto disimpan terenkripsi di server SeTrip dan hanya bisa dilihat oleh
tim admin saat review. Maks 5MB, JPG/PNG/WebP.
</p>
<div className="space-y-4">
<FileUpload
label="Foto KTP"
kind="ktp"
value={ktpKey}
onChange={setKtpKey}
onError={setError}
/>
<div>
<FileUpload
label="Foto kamu memegang kertas tulisan SETRIP"
kind="liveness"
value={livenessKey}
onChange={setLivenessKey}
onError={setError}
/>
<p className="mt-1.5 text-[11px] leading-relaxed text-neutral-500">
Tulis kata <span className="font-semibold">SETRIP</span> dengan
tangan di selembar kertas, lalu foto diri kamu sambil memegang
kertas itu pastikan wajah & tulisan terlihat jelas dalam satu
foto. Foto ini bukti bahwa pengajuan benar dilakukan oleh kamu
sendiri.
</p>
</div>
</div>
</section>
<section>
<h2 className="mb-3 flex items-center gap-2 text-base font-bold text-neutral-900">
<Landmark
size={18}
strokeWidth={1.75}
aria-hidden
className="text-primary-600"
/>
Rekening Bank
</h2>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
Nama Bank
</label>
<input
name="bankName"
type="text"
required
defaultValue={initial?.bankName ?? ""}
className={inputCls}
placeholder="Mis. BCA, Mandiri, BRI"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
Nomor Rekening
</label>
<input
name="bankAccountNumber"
type="text"
inputMode="numeric"
required
defaultValue={initial?.bankAccountNumber ?? ""}
className={inputCls}
placeholder="1234567890"
/>
</div>
<div className="sm:col-span-2">
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
Nama Pemilik Rekening
</label>
<input
name="bankAccountName"
type="text"
required
defaultValue={initial?.bankAccountName ?? ""}
className={inputCls}
placeholder="Sesuai buku tabungan"
/>
</div>
</div>
</section>
<button
type="submit"
disabled={loading}
className="w-full rounded-xl bg-primary-600 py-2.5 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:opacity-50"
>
{loading ? "Mengirim..." : "Ajukan Verifikasi"}
</button>
<p className="text-center text-xs text-neutral-500">
Data KTP & rekening hanya digunakan untuk verifikasi dan tidak akan
ditampilkan ke pengguna lain.
</p>
</form>
);
}
function FileUpload({
label,
kind,
value,
onChange,
onError,
}: {
label: string;
kind: UploadKind;
value: string;
onChange: (key: string) => void;
onError: (msg: string) => void;
}) {
const [busy, setBusy] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string>("");
async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
if (file.size > MAX_BYTES) {
onError(`${label} maksimal 5MB`);
e.target.value = "";
return;
}
if (!ACCEPT_MIME.split(",").includes(file.type)) {
onError(`${label} harus JPG, PNG, atau WebP`);
e.target.value = "";
return;
}
setBusy(true);
onError("");
try {
const fd = new FormData();
fd.set("kind", kind);
fd.set("file", file);
const res = await fetch("/api/upload/kyc", { method: "POST", body: fd });
const json = await res.json();
if (!res.ok) {
onError(json.error ?? `Gagal mengunggah ${label}`);
return;
}
onChange(json.key);
const obj = URL.createObjectURL(file);
setPreviewUrl((old) => {
if (old) URL.revokeObjectURL(old);
return obj;
});
} catch {
onError(`Gagal mengunggah ${label}`);
} finally {
setBusy(false);
e.target.value = "";
}
}
return (
<div>
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
{label}
</label>
<div className="flex items-center gap-3">
<label className="inline-flex cursor-pointer items-center rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">
{busy ? "Mengunggah..." : value ? "Ganti file" : "Pilih file"}
<input
type="file"
accept={ACCEPT_MIME}
onChange={onPick}
disabled={busy}
className="sr-only"
/>
</label>
{value && !busy && (
<span className="inline-flex items-center gap-1 text-xs text-emerald-600">
<Check size={13} strokeWidth={2.5} aria-hidden />
Terunggah
</span>
)}
</div>
{previewUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={previewUrl}
alt={`${label} preview`}
className="mt-2 max-h-40 rounded-lg border border-neutral-200"
/>
)}
</div>
);
}