Files
setrip/features/organizer/components/verify-form.tsx
T

329 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
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 text-base font-bold text-neutral-900">📇 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 text-base font-bold text-neutral-900">🖼 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 text-base font-bold text-neutral-900">🏦 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="text-xs text-neutral-500"> 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>
);
}