diff --git a/app/api/docs/SwaggerUIWrapper.tsx b/app/api/docs/SwaggerUIWrapper.tsx index e01affa..8b5c289 100644 --- a/app/api/docs/SwaggerUIWrapper.tsx +++ b/app/api/docs/SwaggerUIWrapper.tsx @@ -113,7 +113,7 @@ export default function SwaggerUIWrapper({ specUrl }: SwaggerUIWrapperProps) { RemitWise API Documentation

- Complete reference for integrating with RemitWise's remittance and financial planning services. + Complete reference for integrating with RemitWise's remittance and financial planning services. Build secure, scalable applications with our comprehensive API.

diff --git a/app/api/v1/admin/webhooks/dlq/route.ts b/app/api/v1/admin/webhooks/dlq/route.ts index da481cc..b69a688 100644 --- a/app/api/v1/admin/webhooks/dlq/route.ts +++ b/app/api/v1/admin/webhooks/dlq/route.ts @@ -41,7 +41,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ success: true, data: { - events: dlqData.items.map((event) => ({ + events: dlqData.items.map((event: any) => ({ id: event.id, source: event.source, eventType: event.eventType, diff --git a/app/dashboard/goals/components/SavingsGoalModal.tsx b/app/dashboard/goals/components/SavingsGoalModal.tsx new file mode 100644 index 0000000..d7d59bc --- /dev/null +++ b/app/dashboard/goals/components/SavingsGoalModal.tsx @@ -0,0 +1,300 @@ +'use client' + +import React, { useState, useEffect, useRef } from 'react' +import { + X, + GraduationCap, + HeartPulse, + Home, + Plane, + AlertCircle +} from 'lucide-react' +import { SavingsGoal, SavingsGoalFormData } from '../types' +import { + validateGoalName, + validateGoalDescription, + validateAmount, + validateFutureDate +} from '@/lib/validation/savings-goals' +import { useClientTranslator } from '@/lib/i18n/client' + +interface SavingsGoalModalProps { + isOpen: boolean + onClose: () => void + onSave: (goalData: Partial) => void + editingGoal: SavingsGoal | null +} + +const ICONS = [ + { type: 'education', icon: , gradient: { from: '#DC2626', to: '#B91C1C' } }, + { type: 'medical', icon: , gradient: { from: '#F87171', to: '#EF4444' } }, + { type: 'home', icon: , gradient: { from: '#DC2626', to: '#B91C1C' } }, + { type: 'vacation', icon: , gradient: { from: '#F87171', to: '#EF4444' } }, +] as const + +export default function SavingsGoalModal({ + isOpen, + onClose, + onSave, + editingGoal, +}: SavingsGoalModalProps) { + const { t } = useClientTranslator() + const [formData, setFormData] = useState({ + title: '', + description: '', + targetAmount: 0, + targetDate: '', + iconType: 'education', + }) + + const [errors, setErrors] = useState>({}) + const modalRef = useRef(null) + + useEffect(() => { + if (editingGoal) { + // Find iconType from icon or title if possible, else default + let iconType: SavingsGoalFormData['iconType'] = 'education' + if (editingGoal.title.toLowerCase().includes('medical') || editingGoal.title.toLowerCase().includes('health')) iconType = 'medical' + else if (editingGoal.title.toLowerCase().includes('home') || editingGoal.title.toLowerCase().includes('house')) iconType = 'home' + else if (editingGoal.title.toLowerCase().includes('vacation') || editingGoal.title.toLowerCase().includes('trip')) iconType = 'vacation' + + setFormData({ + title: editingGoal.title, + description: editingGoal.description, + targetAmount: editingGoal.targetAmount, + targetDate: editingGoal.targetDate, + iconType, + }) + } else { + setFormData({ + title: '', + description: '', + targetAmount: 0, + targetDate: '', + iconType: 'education', + }) + } + setErrors({}) + }, [editingGoal, isOpen]) + + // Focus trap and ESC key + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + if (e.key === 'Tab') { + const focusableElements = modalRef.current?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) + if (!focusableElements) return + + const firstElement = focusableElements[0] as HTMLElement + const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement + + if (e.shiftKey) { + if (document.activeElement === firstElement) { + lastElement.focus() + e.preventDefault() + } + } else { + if (document.activeElement === lastElement) { + firstElement.focus() + e.preventDefault() + } + } + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [isOpen, onClose]) + + if (!isOpen) return null + + const validate = (): boolean => { + const newErrors: Record = {} + + const titleVal = validateGoalName(formData.title) + if (!titleVal.isValid) newErrors.title = titleVal.error ? t(`errors.${titleVal.error}`) : t('errors.invalid_name') + + const descVal = validateGoalDescription(formData.description) + if (!descVal.isValid) newErrors.description = descVal.error ? t(`errors.${descVal.error}`) : t('errors.goal_description_invalid') + + const amountVal = validateAmount(formData.targetAmount) + if (!amountVal.isValid) newErrors.targetAmount = amountVal.error ? t(`errors.${amountVal.error}`) : t('errors.invalid_amount') + + const dateVal = validateFutureDate(formData.targetDate) + if (!dateVal.isValid) newErrors.targetDate = dateVal.error ? t(`errors.${dateVal.error}`) : t('errors.goal_invalid_date') + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (validate()) { + const selectedIcon = ICONS.find(i => i.type === formData.iconType) || ICONS[0] + onSave({ + title: formData.title, + description: formData.description, + targetAmount: formData.targetAmount, + targetDate: formData.targetDate, + icon: selectedIcon.icon, + iconGradient: selectedIcon.gradient, + }) + } + } + + return ( +
+
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="modal-title" + > + + + + +
+ {/* Goal Title */} +
+ + setFormData({ ...formData, title: e.target.value })} + className={`w-full rounded-xl bg-white/5 border ${errors.title ? 'border-red-500' : 'border-white/10'} px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-red-500/50 transition-all motion-reduce:transition-none`} + placeholder={t('savingsGoals.modal.titlePlaceholder')} + autoFocus + /> + {errors.title && ( +

+ {errors.title} +

+ )} +
+ + {/* Description */} +
+ +