832 lines
25 KiB
JavaScript
832 lines
25 KiB
JavaScript
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 = '#E6D7A2';
|
||
// inputStyle.cursor = 'not-allowed';
|
||
// inputStyle.opacity = '0.7';
|
||
// }
|
||
// 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 TextField = ({
|
||
label,
|
||
placeholder,
|
||
value,
|
||
onChange,
|
||
type = 'text',
|
||
width = 'auto',
|
||
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) => {
|
||
if (!readOnly) setFocused(true);
|
||
onFocus?.(event);
|
||
};
|
||
|
||
const handleBlur = (event) => {
|
||
setFocused(false);
|
||
onBlur?.(event);
|
||
};
|
||
|
||
// Base styles
|
||
const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
|
||
const inputStyle = { ...baseStyle, ...style };
|
||
|
||
if (leftIcon) inputStyle.paddingLeft = '40px';
|
||
if (isPasswordField && showToggle) inputStyle.paddingRight = '44px';
|
||
|
||
// Error, focus, idle, readOnly colors
|
||
const hasError = Boolean(error);
|
||
|
||
if (hasError) {
|
||
inputStyle.borderColor = '#B91C1C'; // red border
|
||
inputStyle.backgroundColor = '#FEF2F2';
|
||
inputStyle.color = '#B91C1C';
|
||
} else if (readOnly) {
|
||
inputStyle.borderColor = '#CBA344'; // standard
|
||
inputStyle.outline = 'none';
|
||
inputStyle.backgroundColor = '#FFFFFF'; // ✅ disabled color
|
||
inputStyle.cursor = 'not-allowed';
|
||
inputStyle.opacity = '0.8';
|
||
} else if (focused) {
|
||
inputStyle.borderColor = '#92722A'; // ✅ focused color
|
||
inputStyle.outline = 'none';
|
||
inputStyle.backgroundColor = '#FFFFFF';
|
||
} else {
|
||
inputStyle.borderColor = '#CBA344'; // ✅ standard color
|
||
inputStyle.outline = 'none';
|
||
inputStyle.backgroundColor = '#FFFFFF';
|
||
}
|
||
|
||
inputStyle.color = hasError ? '#B91C1C' : '#232528';
|
||
|
||
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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
|
||
<circle cx="12" cy="12" r="3" />
|
||
</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="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>
|
||
)}
|
||
</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 = '#92722A',
|
||
allowClear = false,
|
||
onClear,
|
||
clearValue = '',
|
||
error = '',
|
||
readOnly = false,
|
||
required = false,
|
||
forceDropdownBelow = 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 [isOpen, setIsOpen] = React.useState(false);
|
||
const [searchTerm, setSearchTerm] = React.useState('');
|
||
const dropdownRef = React.useRef(null);
|
||
const inputRef = React.useRef(null);
|
||
const normalizedClear = clearValue ?? '';
|
||
const hasValue = value !== undefined && value !== null && value !== '' && value !== normalizedClear;
|
||
const showClear = allowClear && hasValue && !readOnly;
|
||
|
||
if (rightIcon) {
|
||
inputStyle.paddingRight = '40px';
|
||
}
|
||
|
||
// Filter options based on search term
|
||
const filteredOptions = React.useMemo(() => {
|
||
if (!searchTerm) return options;
|
||
return options.filter(opt =>
|
||
opt.label.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
}, [options, searchTerm]);
|
||
|
||
// Close dropdown when clicking outside
|
||
React.useEffect(() => {
|
||
const handleClickOutside = (event) => {
|
||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||
setIsOpen(false);
|
||
setSearchTerm('');
|
||
}
|
||
};
|
||
|
||
if (isOpen) {
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
}
|
||
|
||
return () => {
|
||
document.removeEventListener('mousedown', handleClickOutside);
|
||
};
|
||
}, [isOpen]);
|
||
|
||
const handleFocus = (event) => {
|
||
setFocused(true);
|
||
onFocus?.(event);
|
||
};
|
||
|
||
const handleBlur = (event) => {
|
||
setFocused(false);
|
||
onBlur?.(event);
|
||
};
|
||
|
||
const handleClear = (event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
if (onClear) {
|
||
onClear();
|
||
} else if (onChange) {
|
||
onChange({ target: { value: normalizedClear } });
|
||
}
|
||
};
|
||
|
||
const handleSelect = (selectedValue) => {
|
||
if (!readOnly) {
|
||
onChange({ target: { value: selectedValue } });
|
||
setIsOpen(false);
|
||
setSearchTerm('');
|
||
}
|
||
};
|
||
|
||
const toggleDropdown = () => {
|
||
if (!readOnly) {
|
||
setIsOpen(!isOpen);
|
||
setSearchTerm('');
|
||
}
|
||
};
|
||
|
||
const getDisplayValue = () => {
|
||
const selected = options.find(opt => opt.value === value);
|
||
return selected ? selected.label : (placeholder || '');
|
||
};
|
||
|
||
const hasError = Boolean(error);
|
||
inputStyle.borderColor = hasError
|
||
? '#B91C1C'
|
||
: focused
|
||
? focusBorderColors[variant]
|
||
: idleBorderColors[variant] || idleBorderColors.default;
|
||
if (hasError) {
|
||
inputStyle.backgroundColor = '#E6D7A2';
|
||
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';
|
||
}
|
||
|
||
// Custom dropdown implementation
|
||
if (forceDropdownBelow) {
|
||
return (
|
||
<div className={`w-full ${wrapperStyle ? '' : ''}`} style={wrapperStyle || {}} ref={dropdownRef}>
|
||
{label && (
|
||
<label htmlFor={id || name} className="block text-sm font-medium text-gray-900 mb-1">
|
||
{label}
|
||
{required && <span className="text-red-700 ml-1">*</span>}
|
||
</label>
|
||
)}
|
||
<div className="relative w-full">
|
||
<div
|
||
className={`w-full text-sm focus:outline-none ${className}`}
|
||
style={{
|
||
...inputStyle,
|
||
height: 'auto', // override fixed height
|
||
minHeight: '40px',
|
||
cursor: readOnly ? 'not-allowed' : 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
padding: '8px 16px',
|
||
}}
|
||
onClick={toggleDropdown}
|
||
onFocus={handleFocus}
|
||
onBlur={handleBlur}
|
||
tabIndex={readOnly ? -1 : 0}
|
||
>
|
||
<span
|
||
style={{
|
||
color: hasValue ? '#232528' : '#9CA3AF',
|
||
flex: 1,
|
||
whiteSpace: 'normal',
|
||
lineHeight: '20px'
|
||
}}
|
||
>
|
||
{getDisplayValue()}
|
||
</span>
|
||
{showClear ? (
|
||
<button
|
||
type="button"
|
||
className="h-5 w-5 rounded-full text-[#9CA3AF] hover:text-[#4B5563]"
|
||
onClick={handleClear}
|
||
aria-label="Clear selection"
|
||
>
|
||
×
|
||
</button>
|
||
) : (
|
||
rightIcon && <img src={rightIcon} alt="" className="h-4 w-4" />
|
||
)}
|
||
</div>
|
||
|
||
{isOpen && !readOnly && (
|
||
<div
|
||
className="absolute z-50 w-full mt-1 bg-white border border-[#CBA344] rounded-md shadow-lg max-h-60 overflow-auto"
|
||
style={{
|
||
top: '100%',
|
||
left: 0,
|
||
right: 0
|
||
}}
|
||
>
|
||
{/* Search input */}
|
||
<div className="p-2 border-b border-gray-200">
|
||
<input
|
||
type="text"
|
||
placeholder="Search..."
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[#92722A] focus:border-transparent"
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
onClick={(e) => e.stopPropagation()}
|
||
/>
|
||
</div>
|
||
|
||
{/* Options list */}
|
||
{filteredOptions.length === 0 ? (
|
||
<div className="px-3 py-2 text-gray-500 text-sm">No options found</div>
|
||
) : (
|
||
filteredOptions.map((opt) => (
|
||
<div
|
||
key={opt.value}
|
||
className={`px-3 py-2 cursor-pointer text-sm hover:bg-[#F9FAFB] ${
|
||
opt.value === value ? 'bg-[#E6D7A2] text-[#232528]' : 'text-[#232528]'
|
||
}`}
|
||
onClick={() => handleSelect(opt.value)}
|
||
>
|
||
{opt.label}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{readOnly && <div className="absolute inset-0 pointer-events-none" />}
|
||
{error && <p className="mt-1 text-xs text-[#B91C1C]">{error}</p>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Original native select implementation
|
||
return (
|
||
<div className={`w-full ${wrapperStyle ? '' : ''}`} style={wrapperStyle || {}}>
|
||
{label && (
|
||
<label htmlFor={id || name} className="block text-sm font-medium text-gray-900 mb-1">
|
||
{label}
|
||
{required && <span className="text-red-700 ml-1">*</span>}
|
||
</label>
|
||
)}
|
||
<div className="relative w-full">
|
||
<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}
|
||
required={required}
|
||
>
|
||
{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,
|
||
required=false,
|
||
...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;
|
||
}
|
||
// Format the minDate prop if provided
|
||
const formatDateForInput = (dateString) => {
|
||
if (!dateString) return '';
|
||
const date = new Date(dateString);
|
||
return date.toISOString().split('T')[0];
|
||
};
|
||
|
||
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}
|
||
{required && <span className="text-[#B91C1C] ml-1">*</span>}
|
||
</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}
|
||
min={formatDateForInput(rest.minDate)}
|
||
required={required}
|
||
{...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>
|
||
);
|
||
};
|