fcsc_ipi_frontend/ipi-survey-platform/src/components/common/FormControls.jsx
2025-10-30 10:23:11 +05:30

526 lines
15 KiB
JavaScript
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.

import React from 'react';
const caretDownActiveSrc = '/assets/images/caretdown-active.svg';
const calendarIconSrc = '/assets/images/uil_calender.svg';
const baseInputStyles = {
default: {
height: '40px',
border: '2px solid #CBA344',
borderRadius: '4px',
paddingLeft: '16px',
paddingRight: '16px',
transition: 'border-color 0.2s ease, color 0.2s ease',
backgroundColor: '#FFFFFF',
},
toolbar: {
height: '40px',
border: '1px solid #C3C6CB',
borderRadius: '8px',
paddingLeft: '16px',
paddingRight: '16px',
transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
backgroundColor: '#FFFFFF',
color: '#5F646D',
},
};
const focusBorderColors = {
default: '#92722A',
toolbar: '#92722A',
};
const idleBorderColors = {
default: '#CBA344',
toolbar: '#C3C6CB',
};
export const TextField = ({
label,
placeholder,
value,
onChange,
type = 'text',
width = 'auto', // 'auto' | number | string
className = '',
style = {},
name,
id,
leftIcon,
onFocus,
onBlur,
variant = 'default',
required = false,
showToggle = false,
toggleLabels = { show: 'Show', hide: 'Hide' },
error = '',
readOnly = false,
}) => {
const wrapperStyle = {};
if (width !== 'auto') {
wrapperStyle.width = typeof width === 'number' ? `${width}px` : width;
}
const [focused, setFocused] = React.useState(false);
const isPasswordField = type === 'password';
const [showPassword, setShowPassword] = React.useState(false);
React.useEffect(() => {
if (!isPasswordField) {
setShowPassword(false);
}
}, [isPasswordField]);
const handleFocus = (event) => {
setFocused(true);
onFocus?.(event);
};
const handleBlur = (event) => {
setFocused(false);
onBlur?.(event);
};
const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
const inputStyle = { ...baseStyle, ...style };
if (leftIcon) {
inputStyle.paddingLeft = '40px';
}
if (isPasswordField && showToggle) {
inputStyle.paddingRight = '44px';
}
const hasError = Boolean(error);
inputStyle.borderColor = hasError
? '#B91C1C'
: focused && !readOnly
? focusBorderColors[variant]
: idleBorderColors[variant] || idleBorderColors.default;
if (hasError) {
inputStyle.backgroundColor = '#FEF2F2';
inputStyle.color = '#B91C1C';
}
if (!hasError && variant !== 'toolbar') {
inputStyle.color = '#232528';
}
if (readOnly) {
inputStyle.backgroundColor = '#F9FAFB';
}
if (!hasError && variant === 'toolbar') {
inputStyle.color = '#5F646D';
}
return (
<div style={wrapperStyle} className="w-full">
{label && (
<label htmlFor={id || name} className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">
{label}
{required && <span className="text-[#B91C1C] ml-1">*</span>}
</label>
)}
<div className="relative">
<input
id={id || name}
name={name}
type={isPasswordField ? (showPassword ? 'text' : 'password') : type}
value={value}
onChange={onChange}
placeholder={placeholder}
className={`w-full text-sm focus:outline-none ${className}`}
style={inputStyle}
onFocus={handleFocus}
onBlur={handleBlur}
required={required}
readOnly={readOnly}
/>
{leftIcon && (
<img src={leftIcon} alt="" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" />
)}
{isPasswordField && showToggle && (
<button
type="button"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#92722A] hover:text-[#7b5c1f]"
aria-label={showPassword ? toggleLabels.hide : toggleLabels.show}
title={showPassword ? toggleLabels.hide : toggleLabels.show}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 3l18 18" />
<path strokeLinecap="round" strokeLinejoin="round" d="M10.58 10.58A2 2 0 0012 14a2 2 0 001.42-.58M16.68 16.68C15.28 17.54 13.69 18 12 18 7 18 3.73 14.82 2 12c.58-.9 1.34-1.83 2.26-2.68M9.88 4.13C10.56 4.05 11.27 4 12 4c5 0 8.27 3.18 10 6-.46.72-1.03 1.43-1.71 2.1" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
<circle cx="12" cy="12" r="3" />
</svg>
)}
</button>
)}
</div>
{error && <p className="mt-1 text-xs text-[#B91C1C]">{error}</p>}
</div>
);
};
export const SelectField = ({
label,
value,
onChange,
options = [],
width = 'auto',
className = '',
style = {},
name,
id,
rightIcon = caretDownActiveSrc,
placeholder,
onFocus,
onBlur,
variant = 'default',
allowClear = false,
onClear,
clearValue = '',
error = '',
readOnly = false,
}) => {
const wrapperStyle = {};
if (width !== 'auto') {
wrapperStyle.width = typeof width === 'number' ? `${width}px` : width;
}
const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
const inputStyle = { ...baseStyle, ...style };
const [focused, setFocused] = React.useState(false);
const normalizedClear = clearValue ?? '';
const hasValue = value !== undefined && value !== null && value !== '' && value !== normalizedClear;
const showClear = allowClear && hasValue && !readOnly;
if (rightIcon) {
inputStyle.paddingRight = '40px';
}
const handleFocus = (event) => {
setFocused(true);
onFocus?.(event);
};
const handleBlur = (event) => {
setFocused(false);
onBlur?.(event);
};
const hasError = Boolean(error);
inputStyle.borderColor = hasError
? '#B91C1C'
: focused
? focusBorderColors[variant]
: idleBorderColors[variant] || idleBorderColors.default;
if (hasError) {
inputStyle.backgroundColor = '#FEF2F2';
inputStyle.color = '#B91C1C';
} else if (variant === 'toolbar') {
inputStyle.color = hasValue && value !== 'All' && value !== 'Status' && value !== 'Year' && value !== 'Quarter' && value !== 'Emirates' ? '#232528' : '#5F646D';
} else {
inputStyle.color = hasValue ? '#232528' : '#232528';
}
if (readOnly) {
inputStyle.backgroundColor = '#F9FAFB';
inputStyle.cursor = 'not-allowed';
}
const handleClear = (event) => {
event.preventDefault();
event.stopPropagation();
if (onClear) {
onClear();
} else if (onChange) {
onChange({ target: { value: normalizedClear } });
}
};
return (
<div style={wrapperStyle} className="w-full">
{label && (
<label htmlFor={id || name} className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">
{label}
</label>
)}
<div className="relative" style={{ width: '100%' }}>
<select
id={id || name}
name={name}
value={value}
onChange={(event) => {
if (!readOnly) {
onChange(event);
}
}}
className={`w-full appearance-none text-sm focus:outline-none ${className}`}
style={inputStyle}
onFocus={handleFocus}
onBlur={handleBlur}
>
{placeholder && (
<option value="" disabled hidden>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{showClear ? (
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 rounded-full text-[#9CA3AF] hover:text-[#4B5563]"
onClick={handleClear}
aria-label="Clear selection"
>
×
</button>
) : (
rightIcon && <img src={rightIcon} alt="" className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4" />
)}
</div>
{readOnly && <div className="absolute inset-0 pointer-events-none" />}
{error && <p className="mt-1 text-xs text-[#B91C1C]">{error}</p>}
</div>
);
};
const formatPhoneDigits = (digits) => {
if (!digits) return '';
const cleaned = digits.replace(/\D/g, '').slice(0, 8);
const part1 = cleaned.slice(0, 2);
const part2 = cleaned.slice(2, 5);
const part3 = cleaned.slice(5, 8);
return [part1, part2, part3].filter(Boolean).join(' ');
};
export const PhoneField = ({
label,
countryCode = '+971',
onCountryCodeChange,
countryOptions = [{ label: '+971', value: '+971' }],
placeholder = 'XX XXX XXX',
value,
onChange,
width = 'auto',
required = false,
error = '',
readOnly = false,
className = '',
}) => {
const wrapperStyle = {};
if (width !== 'auto') {
wrapperStyle.width = typeof width === 'number' ? `${width}px` : width;
}
const [focused, setFocused] = React.useState(false);
const handleFocus = () => {
if (!readOnly) setFocused(true);
};
const handleBlur = () => {
setFocused(false);
};
const hasError = Boolean(error);
const borderColor = hasError ? '#B91C1C' : focused ? '#92722A' : '#CBA344';
const containerStyle = {
display: 'flex',
alignItems: 'center',
border: `2px solid ${borderColor}`,
borderRadius: '4px',
backgroundColor: readOnly ? '#F9FAFB' : hasError ? '#FEF2F2' : '#FFFFFF',
transition: 'border-color 0.2s ease',
height: '40px',
paddingRight: '8px',
pointerEvents: readOnly ? 'none' : 'auto',
};
const selectStyle = {
border: 'none',
background: 'transparent',
paddingLeft: '12px',
paddingRight: '24px',
fontSize: '14px',
color: hasError ? '#B91C1C' : '#232528',
appearance: 'none',
WebkitAppearance: 'none',
MozAppearance: 'none',
cursor: readOnly ? 'not-allowed' : 'pointer',
height: '100%',
};
const inputStyle = {
flex: 1,
border: 'none',
outline: 'none',
background: 'transparent',
fontSize: '14px',
color: hasError ? '#B91C1C' : '#232528',
padding: '0 0 0 8px',
};
const arrowStyle = {
position: 'absolute',
right: '8px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
height: '12px',
width: '12px',
};
const handleSelectChange = (event) => {
onCountryCodeChange?.(event.target.value);
};
const handleInputChange = (event) => {
const digits = event.target.value.replace(/\D/g, '') || '';
const formatted = formatPhoneDigits(digits);
onChange?.(formatted);
};
const displayValue = formatPhoneDigits((value || '').replace(/\D/g, ''));
return (
<div style={wrapperStyle} className="w-full">
{label && (
<label className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">
{label}
{required && <span className="text-[#B91C1C] ml-1">*</span>}
</label>
)}
<div style={containerStyle} className={`relative ${className}`} onFocus={handleFocus} onBlur={handleBlur}>
<div className="relative h-full flex items-center">
<select value={countryCode} onChange={handleSelectChange} style={selectStyle} disabled={readOnly}>
{countryOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<img src={caretDownActiveSrc} alt="" style={arrowStyle} />
</div>
<input
type="text"
value={displayValue}
onChange={handleInputChange}
placeholder={placeholder}
style={inputStyle}
disabled={readOnly}
/>
</div>
{error && <p className="mt-1 text-xs text-[#B91C1C]">{error}</p>}
</div>
);
};
export const DateField = ({
label,
value,
onChange,
placeholder,
width = 'auto',
className = '',
style = {},
name,
id,
rightIcon = calendarIconSrc,
onBlur,
onFocus,
...rest
}) => {
const inputRef = React.useRef(null);
const [isDateMode, setIsDateMode] = React.useState(Boolean(value));
const [focused, setFocused] = React.useState(false);
React.useEffect(() => {
if (typeof document === 'undefined') return;
if (document.getElementById('date-field-style')) return;
const styleElement = document.createElement('style');
styleElement.id = 'date-field-style';
styleElement.innerHTML = `
input.date-field::-webkit-calendar-picker-indicator {
opacity: 0;
position: absolute;
right: 0;
width: 40px;
height: 100%;
cursor: pointer;
}
input.date-field::-webkit-clear-button { display: none; }
input.date-field::-webkit-inner-spin-button { display: none; }
input.date-field { -webkit-appearance: none; }
input.date-field::placeholder { color: #9CA3AF; }
`;
document.head.appendChild(styleElement);
}, []);
const wrapperStyle = {};
if (width !== 'auto') {
wrapperStyle.width = typeof width === 'number' ? `${width}px` : width;
}
const inputStyle = {
...(baseInputStyles?.default || {}),
...style,
color: value ? '#232528' : '#232528',
borderColor: focused ? '#92722A' : '#CBA344',
};
if (rightIcon) {
inputStyle.paddingRight = '40px';
}
React.useEffect(() => {
setIsDateMode(Boolean(value));
}, [value]);
const triggerPicker = () => {
setIsDateMode(true);
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.showPicker?.();
inputRef.current.focus?.();
}
});
};
const handleFocus = (event) => {
onFocus?.(event);
setFocused(true);
if (!isDateMode) {
triggerPicker();
}
};
const handleBlur = (event) => {
onBlur?.(event);
setFocused(false);
if (!value) {
setIsDateMode(false);
}
};
return (
<div style={wrapperStyle} className="w-full">
{label && (
<label htmlFor={id || name} className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">
{label}
</label>
)}
<div className="relative">
<input
ref={inputRef}
id={id || name}
name={name}
type={isDateMode ? 'date' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
className={`date-field w-full text-sm focus:outline-none ${className}`}
style={inputStyle}
onFocus={handleFocus}
onBlur={handleBlur}
{...rest}
/>
{rightIcon && (
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 grid place-items-center text-transparent"
onClick={triggerPicker}
tabIndex={-1}
aria-hidden="true"
>
<img src={rightIcon} alt="" className="pointer-events-none h-4 w-4" />
</button>
)}
</div>
</div>
);
};