307 lines
13 KiB
TypeScript
307 lines
13 KiB
TypeScript
'use client'
|
||
|
||
import { usePersonalUploadId } from './hooks/usePersonalUploadId'
|
||
import PageLayout from '../../../components/PageLayout'
|
||
import { useEffect, useState } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import useAuthStore from '../../../store/authStore'
|
||
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||
|
||
// Add back ID types for the dropdown
|
||
const ID_TYPES = [
|
||
{ value: 'national_id', label: 'National ID Card' },
|
||
{ value: 'passport', label: 'Passport' },
|
||
{ value: 'driver_license', label: "Driver's License" },
|
||
{ value: 'other', label: 'Other' },
|
||
]
|
||
|
||
export default function PersonalIdUploadPage() {
|
||
// NEW: guard company users from accessing personal page
|
||
const user = useAuthStore(s => s.user)
|
||
const router = useRouter()
|
||
const [blocked, setBlocked] = useState(false)
|
||
|
||
useEffect(() => {
|
||
const ut = (user as any)?.userType || (user as any)?.role
|
||
console.log('🧭 UploadID Guard [personal]: userType =', ut)
|
||
if (ut && ut !== 'personal') {
|
||
console.warn('🚫 UploadID Guard [personal]: access denied for userType:', ut, '-> redirecting to company upload')
|
||
setBlocked(true)
|
||
router.replace('/quickaction-dashboard/register-upload-id/company')
|
||
} else if (ut === 'personal') {
|
||
console.log('✅ UploadID Guard [personal]: access granted')
|
||
}
|
||
}, [user, router])
|
||
|
||
if (blocked) {
|
||
return (
|
||
<PageLayout>
|
||
<div className="min-h-[50vh] flex items-center justify-center">
|
||
<div className="text-center text-sm text-gray-600">
|
||
Redirecting to the correct upload page…
|
||
</div>
|
||
</div>
|
||
</PageLayout>
|
||
)
|
||
}
|
||
|
||
const {
|
||
idNumber, setIdNumber,
|
||
idType, setIdType,
|
||
expiry, setExpiry,
|
||
hasBack, setHasBack,
|
||
frontFile, backFile,
|
||
frontPreview, backPreview,
|
||
submitting, error, success,
|
||
frontInputRef, backInputRef,
|
||
handleFile, onDrop, clearFile, dropEvents, openPicker, submit,
|
||
inputBase,
|
||
} = usePersonalUploadId()
|
||
|
||
return (
|
||
<PageLayout>
|
||
<div className="relative flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||
{/* Background */}
|
||
<div className="fixed inset-0 -z-10">
|
||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
||
<defs>
|
||
<pattern id="personal-id-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse">
|
||
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" />
|
||
</pattern>
|
||
</defs>
|
||
<rect fill="url(#personal-id-pattern)" width="100%" height="100%" strokeWidth={0} />
|
||
</svg>
|
||
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
|
||
<div
|
||
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
|
||
style={{ clipPath: 'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={submit} className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden">
|
||
<div className="px-6 py-8 sm:px-12 lg:px-16">
|
||
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">
|
||
Personal Identity Verification
|
||
</h1>
|
||
<p className="text-sm text-gray-600 mb-8">
|
||
Please upload clear photos of both sides of your government‑issued ID
|
||
</p>
|
||
|
||
{/* Grid Fields: put all three inputs on the same line on md+ */}
|
||
<div className="grid gap-6 md:grid-cols-3">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
ID Number *
|
||
</label>
|
||
<input
|
||
value={idNumber}
|
||
onChange={e => setIdNumber(e.target.value)}
|
||
placeholder="Enter your ID number"
|
||
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
|
||
required
|
||
/>
|
||
<p className="mt-1 text-xs text-gray-600">
|
||
Enter the number exactly as shown on your ID
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
ID Type *
|
||
</label>
|
||
<select
|
||
value={idType}
|
||
onChange={e => setIdType(e.target.value)}
|
||
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
|
||
required
|
||
>
|
||
<option value="">Select ID type</option>
|
||
{ID_TYPES.map(t => (
|
||
<option key={t.value} value={t.value}>
|
||
{t.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Expiry Date *
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={expiry}
|
||
onChange={e => setExpiry(e.target.value)}
|
||
placeholder="tt.mm jjjj"
|
||
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Back side toggle */}
|
||
<div className="mt-8 flex items-center gap-3">
|
||
<span className="text-sm font-medium text-gray-700">
|
||
Does ID have a Backside?
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => setHasBack(v => !v)}
|
||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
|
||
hasBack ? 'bg-indigo-600' : 'bg-gray-300'
|
||
}`}
|
||
aria-pressed={hasBack}
|
||
>
|
||
<span
|
||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
|
||
hasBack ? 'translate-x-5' : 'translate-x-0'
|
||
}`}
|
||
/>
|
||
</button>
|
||
<span className="text-sm text-gray-700">{hasBack ? 'Yes' : 'No'}</span>
|
||
</div>
|
||
|
||
{/* Upload Areas: full width, 1 col if no back, 2 cols if back */}
|
||
<div className={`mt-8 grid gap-6 items-stretch ${hasBack ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
|
||
{/* Front */}
|
||
<div
|
||
{...dropEvents}
|
||
onDrop={e => onDrop(e, 'front')}
|
||
onClick={() => openPicker('front')}
|
||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
|
||
>
|
||
<input
|
||
ref={frontInputRef}
|
||
type="file"
|
||
accept="image/png,image/jpeg,image/jpg"
|
||
className="hidden"
|
||
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'front') }}
|
||
/>
|
||
{frontFile ? (
|
||
<div className="flex w-full max-w-full flex-col items-center">
|
||
{/* NEW preview */}
|
||
{frontPreview && (
|
||
<img
|
||
src={frontPreview}
|
||
alt="Front ID preview"
|
||
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
|
||
/>
|
||
)}
|
||
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{frontFile.name}</p>
|
||
<button
|
||
type="button"
|
||
onClick={e => { e.stopPropagation(); clearFile('front') }}
|
||
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
|
||
>
|
||
<XMarkIcon className="h-4 w-4" /> Remove
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
|
||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
||
Click to upload front side
|
||
</p>
|
||
<p className="mt-2 text-xs text-gray-500">
|
||
or drag and drop
|
||
<br />
|
||
PNG, JPG, JPEG up to 10MB
|
||
</p>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Back */}
|
||
{hasBack && (
|
||
<div
|
||
{...dropEvents}
|
||
onDrop={e => onDrop(e, 'back')}
|
||
onClick={() => openPicker('back')}
|
||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
|
||
>
|
||
<input
|
||
ref={backInputRef}
|
||
type="file"
|
||
accept="image/png,image/jpeg,image/jpg"
|
||
className="hidden"
|
||
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f, 'back') }}
|
||
/>
|
||
{backFile ? (
|
||
<div className="flex w-full max-w-full flex-col items-center">
|
||
{/* NEW preview */}
|
||
{backPreview && (
|
||
<img
|
||
src={backPreview}
|
||
alt="Back ID preview"
|
||
className="w-full max-h-56 object-contain rounded-md bg-white border border-gray-200"
|
||
/>
|
||
)}
|
||
<p className="mt-3 text-sm font-medium text-gray-800 break-all">{backFile.name}</p>
|
||
<button
|
||
type="button"
|
||
onClick={e => { e.stopPropagation(); clearFile('back') }}
|
||
className="mt-3 inline-flex items-center gap-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100"
|
||
>
|
||
<XMarkIcon className="h-4 w-4" /> Remove
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
|
||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
||
Click to upload back side
|
||
</p>
|
||
<p className="mt-2 text-xs text-gray-500">
|
||
or drag and drop
|
||
<br />
|
||
PNG, JPG, JPEG up to 10MB
|
||
</p>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Info Box, errors, success, submit */}
|
||
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
|
||
<p className="text-sm font-semibold text-indigo-900 mb-3">
|
||
Please ensure your ID documents:
|
||
</p>
|
||
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
|
||
<li>Are clearly visible and readable</li>
|
||
<li>Show all four corners</li>
|
||
<li>Are not expired</li>
|
||
<li>Have good lighting (no shadows or glare)</li>
|
||
<li>{hasBack ? 'Both front and back sides are uploaded' : 'Front side is uploaded'}</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
|
||
{error}
|
||
</div>
|
||
)}
|
||
{success && (
|
||
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
|
||
Upload saved successfully.
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-8 flex justify-end">
|
||
<button
|
||
type="submit"
|
||
disabled={submitting || success}
|
||
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
|
||
>
|
||
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</PageLayout>
|
||
)
|
||
}
|