|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { Box, TextField, TextFieldProps, Typography } from '@mui/material'; |
| 4 | +import DOMPurify from 'dompurify'; |
| 5 | +import { useTranslations } from 'next-intl'; |
| 6 | +import { useMemo } from 'react'; |
| 7 | + |
| 8 | +// Field validation rules based on backend DTOs |
| 9 | +const FIELD_VALIDATION_RULES = { |
| 10 | + name: { maxLength: 50 }, |
| 11 | + email: { maxLength: 255 }, |
| 12 | + password: { maxLength: 128 }, |
| 13 | + partnerAccessCode: { maxLength: 6 }, |
| 14 | + mfaVerificationCode: { maxLength: 6 }, |
| 15 | + accessCode: { maxLength: 6 }, |
| 16 | + partnerId: { maxLength: 36 }, |
| 17 | + signUpLanguage: { maxLength: 10 }, |
| 18 | + feedbackDescription: { maxLength: 5000 }, |
| 19 | + hopes: { maxLength: 5000 }, |
| 20 | + raceEthnNatn: { maxLength: 500 }, |
| 21 | + resourceId: { maxLength: 36 }, |
| 22 | + sessionId: { maxLength: 36 }, |
| 23 | + default: { maxLength: 500 }, |
| 24 | +} as const; |
| 25 | + |
| 26 | +interface SanitizedTextFieldProps extends Omit< |
| 27 | + TextFieldProps, |
| 28 | + 'onChange' | 'inputProps' | 'value' | 'defaultValue' |
| 29 | +> { |
| 30 | + onChange?: (value: string) => void; |
| 31 | + allowedTags?: string[]; |
| 32 | + allowedAttributes?: string[]; |
| 33 | + maxLength?: number; |
| 34 | + showCharacterCount?: boolean; |
| 35 | + inputProps?: TextFieldProps['inputProps']; |
| 36 | + value?: string | null; |
| 37 | + defaultValue?: string | null; |
| 38 | +} |
| 39 | + |
| 40 | +const SanitizedTextField = ({ |
| 41 | + onChange, |
| 42 | + allowedTags = [], |
| 43 | + allowedAttributes = [], |
| 44 | + maxLength, |
| 45 | + showCharacterCount = false, |
| 46 | + id, |
| 47 | + value, |
| 48 | + defaultValue, |
| 49 | + inputProps, |
| 50 | + ...restProps |
| 51 | +}: SanitizedTextFieldProps) => { |
| 52 | + const t = useTranslations('Shared'); |
| 53 | + const fieldMaxLength = useMemo(() => { |
| 54 | + if (maxLength) return maxLength; |
| 55 | + if (id && id in FIELD_VALIDATION_RULES) { |
| 56 | + return FIELD_VALIDATION_RULES[id as keyof typeof FIELD_VALIDATION_RULES].maxLength; |
| 57 | + } |
| 58 | + return FIELD_VALIDATION_RULES.default.maxLength; |
| 59 | + }, [maxLength, id]); |
| 60 | + |
| 61 | + const purifyConfig = useMemo( |
| 62 | + () => ({ |
| 63 | + ALLOWED_TAGS: allowedTags, |
| 64 | + ALLOWED_ATTR: allowedAttributes, |
| 65 | + KEEP_CONTENT: false, |
| 66 | + }), |
| 67 | + [allowedTags, allowedAttributes], |
| 68 | + ); |
| 69 | + |
| 70 | + const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { |
| 71 | + const inputValue = |
| 72 | + e.target.value.length > fieldMaxLength |
| 73 | + ? e.target.value.substring(0, fieldMaxLength) |
| 74 | + : e.target.value; |
| 75 | + |
| 76 | + const sanitized = DOMPurify.sanitize(inputValue, purifyConfig); |
| 77 | + onChange?.(sanitized); |
| 78 | + }; |
| 79 | + |
| 80 | + const effectiveValue = value ?? defaultValue ?? ''; |
| 81 | + const currentLength = effectiveValue.toString().length; |
| 82 | + const shouldShowCounter = |
| 83 | + showCharacterCount || (restProps.multiline && currentLength > fieldMaxLength * 0.8); |
| 84 | + |
| 85 | + const textFieldProps = { |
| 86 | + ...restProps, |
| 87 | + id, |
| 88 | + onChange: handleChange, |
| 89 | + inputProps: { maxLength: fieldMaxLength, ...inputProps }, |
| 90 | + ...(value !== undefined ? { value } : { defaultValue }), |
| 91 | + }; |
| 92 | + |
| 93 | + const characterCountStyle = { |
| 94 | + display: 'block', |
| 95 | + textAlign: 'right', |
| 96 | + mt: -2.475, |
| 97 | + ...(currentLength > fieldMaxLength && { color: 'error.main' }), |
| 98 | + }; |
| 99 | + |
| 100 | + return ( |
| 101 | + <Box mb={0}> |
| 102 | + <TextField {...textFieldProps} /> |
| 103 | + {shouldShowCounter && ( |
| 104 | + <Typography variant="caption" sx={characterCountStyle}> |
| 105 | + {t('characterCount', { current: currentLength, max: fieldMaxLength })} |
| 106 | + </Typography> |
| 107 | + )} |
| 108 | + </Box> |
| 109 | + ); |
| 110 | +}; |
| 111 | + |
| 112 | +export default SanitizedTextField; |
0 commit comments