diff --git a/frontend/src/components/KYCSubmissionForm.tsx b/frontend/src/components/KYCSubmissionForm.tsx new file mode 100644 index 0000000..623d7a6 --- /dev/null +++ b/frontend/src/components/KYCSubmissionForm.tsx @@ -0,0 +1,531 @@ +"use client"; + +import React, { useState, useCallback } from "react"; +import { motion, AnimatePresence, type Variants } from "framer-motion"; +import { useTranslations } from "next-intl"; + +/** + * Form data interface for KYC submission + */ +interface KYCFormData { + fullName: string; + email: string; + dateOfBirth: string; + address: string; + idType: string; + idNumber: string; + documentUpload: File | null; +} + +/** + * Props for KYCSubmissionForm component + */ +interface KYCSubmissionFormProps { + onSubmit?: (data: KYCFormData) => Promise; + onCancel?: () => void; +} + +/** + * Animation variants for form container + */ +const containerVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + ease: [0.16, 1, 0.3, 1], + staggerChildren: 0.1, + }, + }, + exit: { + opacity: 0, + y: -20, + transition: { duration: 0.3 }, + }, +}; + +/** + * Animation variants for form fields + */ +const fieldVariants: Variants = { + hidden: { opacity: 0, x: -20 }, + visible: { + opacity: 1, + x: 0, + transition: { duration: 0.3, ease: [0.16, 1, 0.3, 1] }, + }, +}; + +/** + * Animation variants for submit button + */ +const submitVariants: Variants = { + idle: { scale: 1 }, + loading: { + scale: [1, 0.95, 1], + transition: { + duration: 1.5, + repeat: Infinity, + ease: "easeInOut", + }, + }, + success: { + scale: [1, 1.05, 1], + transition: { duration: 0.5 }, + }, +}; + +/** + * KYCSubmissionForm Component + * + * Form for Know Your Customer verification with framer-motion animations + * and comprehensive screen reader support. + */ +export const KYCSubmissionForm: React.FC = ({ + onSubmit, + onCancel, +}) => { + const t = useTranslations(); + const [formData, setFormData] = useState({ + fullName: "", + email: "", + dateOfBirth: "", + address: "", + idType: "", + idNumber: "", + documentUpload: null, + }); + const [errors, setErrors] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitStatus, setSubmitStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + const [announcementText, setAnnouncementText] = useState(""); + + /** + * Validate form data + */ + const validateForm = useCallback((): boolean => { + const newErrors: Partial = {}; + + if (!formData.fullName.trim()) { + newErrors.fullName = "Full name is required"; + } + if (!formData.email.trim()) { + newErrors.email = "Email is required"; + } else if (!/\S+@\S+\.\S+/.test(formData.email)) { + newErrors.email = "Email is invalid"; + } + if (!formData.dateOfBirth) { + newErrors.dateOfBirth = "Date of birth is required"; + } + if (!formData.address.trim()) { + newErrors.address = "Address is required"; + } + if (!formData.idType) { + newErrors.idType = "ID type is required"; + } + if (!formData.idNumber.trim()) { + newErrors.idNumber = "ID number is required"; + } + if (!formData.documentUpload) { + newErrors.documentUpload = "Document upload is required"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }, [formData]); + + /** + * Handle form submission + */ + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + setAnnouncementText(t("kyc.validationError") || "Please fix the errors in the form"); + return; + } + + setIsSubmitting(true); + setSubmitStatus("loading"); + setAnnouncementText(t("kyc.submitting") || "Submitting KYC form..."); + + try { + await onSubmit?.(formData); + setSubmitStatus("success"); + setAnnouncementText(t("kyc.submitSuccess") || "KYC form submitted successfully!"); + } catch (error) { + setSubmitStatus("error"); + setAnnouncementText(t("kyc.submitError") || "Failed to submit KYC form. Please try again."); + } finally { + setIsSubmitting(false); + } + }, [formData, validateForm, onSubmit, t]); + + /** + * Handle input changes + */ + const handleInputChange = useCallback((field: keyof KYCFormData, value: string | File | null) => { + setFormData(prev => ({ ...prev, [field]: value })); + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: undefined })); + } + }, [errors]); + + /** + * Handle file upload + */ + const handleFileChange = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0] || null; + handleInputChange("documentUpload", file); + }, [handleInputChange]); + + return ( +
+ {/* Screen reader announcements */} +
+ {announcementText} +
+ + + {/* Form header */} + +

+ {t("kyc.formTitle") || "KYC Verification"} +

+

+ {t("kyc.formDescription") || "Please provide your information for identity verification"} +

+
+ + {/* Full Name */} + + + handleInputChange("fullName", e.target.value)} + className={`w-full rounded-xl border px-4 py-3 focus:outline-none focus:ring-2 focus:ring-pluto-400 transition-all ${ + errors.fullName ? "border-red-500" : "border-pluto-200" + }`} + aria-describedby={errors.fullName ? "fullName-error" : undefined} + aria-invalid={!!errors.fullName} + required + /> + + {errors.fullName && ( + + {errors.fullName} + + )} + + + + {/* Email */} + + + handleInputChange("email", e.target.value)} + className={`w-full rounded-xl border px-4 py-3 focus:outline-none focus:ring-2 focus:ring-pluto-400 transition-all ${ + errors.email ? "border-red-500" : "border-pluto-200" + }`} + aria-describedby={errors.email ? "email-error" : undefined} + aria-invalid={!!errors.email} + required + /> + + {errors.email && ( + + {errors.email} + + )} + + + + {/* Date of Birth */} + + + handleInputChange("dateOfBirth", e.target.value)} + className={`w-full rounded-xl border px-4 py-3 focus:outline-none focus:ring-2 focus:ring-pluto-400 transition-all ${ + errors.dateOfBirth ? "border-red-500" : "border-pluto-200" + }`} + aria-describedby={errors.dateOfBirth ? "dateOfBirth-error" : undefined} + aria-invalid={!!errors.dateOfBirth} + required + /> + + {errors.dateOfBirth && ( + + {errors.dateOfBirth} + + )} + + + + {/* Address */} + + +