profit-planet-frontend/src/app/quickaction-dashboard/register-upload-id/personal/page.tsx
2025-10-22 19:41:11 +02:00

307 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 governmentissued 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>
)
}