384 lines
13 KiB
React
384 lines
13 KiB
React
import { useState, useRef, useEffect } from 'react';
|
|
|
|
// Composant drapeau via flag-icons CSS (cdnjs)
|
|
// Usage : <FlagIcon code="FR" />
|
|
const FlagIcon = ({ code, size = 20 }) => (
|
|
<span
|
|
className={`fi fi-${(code || 'fr').toLowerCase()}`}
|
|
style={{ width: size, height: Math.round(size * 0.75), display: 'inline-block', flexShrink: 0, borderRadius: 2 }}
|
|
/>
|
|
);
|
|
|
|
// Liste complète ISO 3166-1 alpha-2 — noms en français
|
|
const COUNTRIES = [
|
|
{ code: 'AF', name: 'Afghanistan' },
|
|
{ code: 'ZA', name: 'Afrique du Sud' },
|
|
{ code: 'AL', name: 'Albanie' },
|
|
{ code: 'DZ', name: 'Algérie' },
|
|
{ code: 'DE', name: 'Allemagne' },
|
|
{ code: 'AD', name: 'Andorre' },
|
|
{ code: 'AO', name: 'Angola' },
|
|
{ code: 'AG', name: 'Antigua-et-Barbuda' },
|
|
{ code: 'SA', name: 'Arabie saoudite' },
|
|
{ code: 'AR', name: 'Argentine' },
|
|
{ code: 'AM', name: 'Arménie' },
|
|
{ code: 'AU', name: 'Australie' },
|
|
{ code: 'AT', name: 'Autriche' },
|
|
{ code: 'AZ', name: 'Azerbaïdjan' },
|
|
{ code: 'BS', name: 'Bahamas' },
|
|
{ code: 'BH', name: 'Bahreïn' },
|
|
{ code: 'BD', name: 'Bangladesh' },
|
|
{ code: 'BB', name: 'Barbade' },
|
|
{ code: 'BY', name: 'Biélorussie' },
|
|
{ code: 'BE', name: 'Belgique' },
|
|
{ code: 'BZ', name: 'Belize' },
|
|
{ code: 'BJ', name: 'Bénin' },
|
|
{ code: 'BT', name: 'Bhoutan' },
|
|
{ code: 'BO', name: 'Bolivie' },
|
|
{ code: 'BA', name: 'Bosnie-Herzégovine' },
|
|
{ code: 'BW', name: 'Botswana' },
|
|
{ code: 'BR', name: 'Brésil' },
|
|
{ code: 'BN', name: 'Brunéi' },
|
|
{ code: 'BG', name: 'Bulgarie' },
|
|
{ code: 'BF', name: 'Burkina Faso' },
|
|
{ code: 'BI', name: 'Burundi' },
|
|
{ code: 'CV', name: 'Cap-Vert' },
|
|
{ code: 'KH', name: 'Cambodge' },
|
|
{ code: 'CM', name: 'Cameroun' },
|
|
{ code: 'CA', name: 'Canada' },
|
|
{ code: 'CF', name: 'République centrafricaine' },
|
|
{ code: 'CL', name: 'Chili' },
|
|
{ code: 'CN', name: 'Chine' },
|
|
{ code: 'CY', name: 'Chypre' },
|
|
{ code: 'CO', name: 'Colombie' },
|
|
{ code: 'KM', name: 'Comores' },
|
|
{ code: 'CG', name: 'Congo' },
|
|
{ code: 'CD', name: 'Congo (RDC)' },
|
|
{ code: 'KP', name: 'Corée du Nord' },
|
|
{ code: 'KR', name: 'Corée du Sud' },
|
|
{ code: 'CR', name: 'Costa Rica' },
|
|
{ code: 'HR', name: 'Croatie' },
|
|
{ code: 'CU', name: 'Cuba' },
|
|
{ code: 'DK', name: 'Danemark' },
|
|
{ code: 'DJ', name: 'Djibouti' },
|
|
{ code: 'DO', name: 'République dominicaine' },
|
|
{ code: 'DM', name: 'Dominique' },
|
|
{ code: 'EG', name: 'Égypte' },
|
|
{ code: 'SV', name: 'Salvador' },
|
|
{ code: 'AE', name: 'Émirats arabes unis' },
|
|
{ code: 'EC', name: 'Équateur' },
|
|
{ code: 'ER', name: 'Érythrée' },
|
|
{ code: 'ES', name: 'Espagne' },
|
|
{ code: 'EE', name: 'Estonie' },
|
|
{ code: 'SZ', name: 'Eswatini' },
|
|
{ code: 'ET', name: 'Éthiopie' },
|
|
{ code: 'FJ', name: 'Fidji' },
|
|
{ code: 'FI', name: 'Finlande' },
|
|
{ code: 'FR', name: 'France' },
|
|
{ code: 'GA', name: 'Gabon' },
|
|
{ code: 'GM', name: 'Gambie' },
|
|
{ code: 'GE', name: 'Géorgie' },
|
|
{ code: 'GH', name: 'Ghana' },
|
|
{ code: 'GR', name: 'Grèce' },
|
|
{ code: 'GD', name: 'Grenade' },
|
|
{ code: 'GT', name: 'Guatemala' },
|
|
{ code: 'GN', name: 'Guinée' },
|
|
{ code: 'GW', name: 'Guinée-Bissau' },
|
|
{ code: 'GQ', name: 'Guinée équatoriale' },
|
|
{ code: 'GY', name: 'Guyana' },
|
|
{ code: 'HT', name: 'Haïti' },
|
|
{ code: 'HN', name: 'Honduras' },
|
|
{ code: 'HU', name: 'Hongrie' },
|
|
{ code: 'IN', name: 'Inde' },
|
|
{ code: 'ID', name: 'Indonésie' },
|
|
{ code: 'IQ', name: 'Irak' },
|
|
{ code: 'IR', name: 'Iran' },
|
|
{ code: 'IE', name: 'Irlande' },
|
|
{ code: 'IS', name: 'Islande' },
|
|
{ code: 'IL', name: 'Israël' },
|
|
{ code: 'IT', name: 'Italie' },
|
|
{ code: 'JM', name: 'Jamaïque' },
|
|
{ code: 'JP', name: 'Japon' },
|
|
{ code: 'JO', name: 'Jordanie' },
|
|
{ code: 'KZ', name: 'Kazakhstan' },
|
|
{ code: 'KE', name: 'Kenya' },
|
|
{ code: 'KG', name: 'Kirghizistan' },
|
|
{ code: 'KI', name: 'Kiribati' },
|
|
{ code: 'KW', name: 'Koweït' },
|
|
{ code: 'LA', name: 'Laos' },
|
|
{ code: 'LS', name: 'Lesotho' },
|
|
{ code: 'LV', name: 'Lettonie' },
|
|
{ code: 'LB', name: 'Liban' },
|
|
{ code: 'LR', name: 'Liberia' },
|
|
{ code: 'LY', name: 'Libye' },
|
|
{ code: 'LI', name: 'Liechtenstein' },
|
|
{ code: 'LT', name: 'Lituanie' },
|
|
{ code: 'LU', name: 'Luxembourg' },
|
|
{ code: 'MK', name: 'Macédoine du Nord' },
|
|
{ code: 'MG', name: 'Madagascar' },
|
|
{ code: 'MY', name: 'Malaisie' },
|
|
{ code: 'MW', name: 'Malawi' },
|
|
{ code: 'MV', name: 'Maldives' },
|
|
{ code: 'ML', name: 'Mali' },
|
|
{ code: 'MT', name: 'Malte' },
|
|
{ code: 'MA', name: 'Maroc' },
|
|
{ code: 'MH', name: 'Îles Marshall' },
|
|
{ code: 'MU', name: 'Maurice' },
|
|
{ code: 'MR', name: 'Mauritanie' },
|
|
{ code: 'MX', name: 'Mexique' },
|
|
{ code: 'FM', name: 'Micronésie' },
|
|
{ code: 'MD', name: 'Moldavie' },
|
|
{ code: 'MC', name: 'Monaco' },
|
|
{ code: 'MN', name: 'Mongolie' },
|
|
{ code: 'ME', name: 'Monténégro' },
|
|
{ code: 'MZ', name: 'Mozambique' },
|
|
{ code: 'MM', name: 'Myanmar' },
|
|
{ code: 'NA', name: 'Namibie' },
|
|
{ code: 'NR', name: 'Nauru' },
|
|
{ code: 'NP', name: 'Népal' },
|
|
{ code: 'NI', name: 'Nicaragua' },
|
|
{ code: 'NE', name: 'Niger' },
|
|
{ code: 'NG', name: 'Nigeria' },
|
|
{ code: 'NO', name: 'Norvège' },
|
|
{ code: 'NZ', name: 'Nouvelle-Zélande' },
|
|
{ code: 'OM', name: 'Oman' },
|
|
{ code: 'UG', name: 'Ouganda' },
|
|
{ code: 'UZ', name: 'Ouzbékistan' },
|
|
{ code: 'PK', name: 'Pakistan' },
|
|
{ code: 'PW', name: 'Palaos' },
|
|
{ code: 'PA', name: 'Panama' },
|
|
{ code: 'PG', name: 'Papouasie-Nouvelle-Guinée' },
|
|
{ code: 'PY', name: 'Paraguay' },
|
|
{ code: 'NL', name: 'Pays-Bas' },
|
|
{ code: 'PE', name: 'Pérou' },
|
|
{ code: 'PH', name: 'Philippines' },
|
|
{ code: 'PL', name: 'Pologne' },
|
|
{ code: 'PT', name: 'Portugal' },
|
|
{ code: 'QA', name: 'Qatar' },
|
|
{ code: 'RO', name: 'Roumanie' },
|
|
{ code: 'GB', name: 'Royaume-Uni' },
|
|
{ code: 'RU', name: 'Russie' },
|
|
{ code: 'RW', name: 'Rwanda' },
|
|
{ code: 'KN', name: 'Saint-Kitts-et-Nevis' },
|
|
{ code: 'SM', name: 'Saint-Marin' },
|
|
{ code: 'VC', name: 'Saint-Vincent-et-les-Grenadines' },
|
|
{ code: 'LC', name: 'Sainte-Lucie' },
|
|
{ code: 'SB', name: 'Îles Salomon' },
|
|
{ code: 'WS', name: 'Samoa' },
|
|
{ code: 'ST', name: 'Sao Tomé-et-Principe' },
|
|
{ code: 'SN', name: 'Sénégal' },
|
|
{ code: 'RS', name: 'Serbie' },
|
|
{ code: 'SC', name: 'Seychelles' },
|
|
{ code: 'SL', name: 'Sierra Leone' },
|
|
{ code: 'SG', name: 'Singapour' },
|
|
{ code: 'SK', name: 'Slovaquie' },
|
|
{ code: 'SI', name: 'Slovénie' },
|
|
{ code: 'SO', name: 'Somalie' },
|
|
{ code: 'SD', name: 'Soudan' },
|
|
{ code: 'SS', name: 'Soudan du Sud' },
|
|
{ code: 'LK', name: 'Sri Lanka' },
|
|
{ code: 'SE', name: 'Suède' },
|
|
{ code: 'CH', name: 'Suisse' },
|
|
{ code: 'SR', name: 'Suriname' },
|
|
{ code: 'SY', name: 'Syrie' },
|
|
{ code: 'TJ', name: 'Tadjikistan' },
|
|
{ code: 'TZ', name: 'Tanzanie' },
|
|
{ code: 'TD', name: 'Tchad' },
|
|
{ code: 'CZ', name: 'Tchéquie' },
|
|
{ code: 'TH', name: 'Thaïlande' },
|
|
{ code: 'TL', name: 'Timor oriental' },
|
|
{ code: 'TG', name: 'Togo' },
|
|
{ code: 'TO', name: 'Tonga' },
|
|
{ code: 'TT', name: 'Trinité-et-Tobago' },
|
|
{ code: 'TN', name: 'Tunisie' },
|
|
{ code: 'TM', name: 'Turkménistan' },
|
|
{ code: 'TR', name: 'Turquie' },
|
|
{ code: 'TV', name: 'Tuvalu' },
|
|
{ code: 'UA', name: 'Ukraine' },
|
|
{ code: 'UY', name: 'Uruguay' },
|
|
{ code: 'VU', name: 'Vanuatu' },
|
|
{ code: 'VE', name: 'Venezuela' },
|
|
{ code: 'VN', name: 'Viêt Nam' },
|
|
{ code: 'YE', name: 'Yémen' },
|
|
{ code: 'ZM', name: 'Zambie' },
|
|
{ code: 'ZW', name: 'Zimbabwe' },
|
|
{ code: 'US', name: 'États-Unis' },
|
|
].sort((a, b) => a.name.localeCompare(b.name, 'fr'));
|
|
|
|
export { COUNTRIES, FlagIcon };
|
|
|
|
/**
|
|
* CountrySelect — combobox pays avec drapeaux emoji et recherche par frappe
|
|
*
|
|
* Props :
|
|
* value : code ISO 2 lettres (ex. 'FR')
|
|
* onChange : (code) => void
|
|
* required : bool (optionnel)
|
|
*/
|
|
export default function CountrySelect({ value, onChange, required, showCode }) {
|
|
const [open, setOpen] = useState(false);
|
|
const [search, setSearch] = useState('');
|
|
const containerRef = useRef(null);
|
|
const inputRef = useRef(null);
|
|
const listRef = useRef(null);
|
|
|
|
const selected = COUNTRIES.find(c => c.code === value);
|
|
|
|
const filtered = search
|
|
? COUNTRIES.filter(c =>
|
|
c.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
c.code.toLowerCase().includes(search.toLowerCase())
|
|
)
|
|
: COUNTRIES;
|
|
|
|
// Ferme le dropdown si clic en dehors
|
|
useEffect(() => {
|
|
const handler = (e) => {
|
|
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
setOpen(false);
|
|
setSearch('');
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handler);
|
|
return () => document.removeEventListener('mousedown', handler);
|
|
}, []);
|
|
|
|
const select = (code) => {
|
|
onChange(code);
|
|
setOpen(false);
|
|
setSearch('');
|
|
};
|
|
|
|
const handleOpen = () => {
|
|
setOpen(true);
|
|
setSearch('');
|
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
};
|
|
|
|
// Navigation clavier dans la liste
|
|
const handleKeyDown = (e) => {
|
|
if (e.key === 'Escape') { setOpen(false); setSearch(''); }
|
|
};
|
|
|
|
return (
|
|
<div ref={containerRef} style={{ position: 'relative' }}>
|
|
{/* Trigger */}
|
|
<button
|
|
type="button"
|
|
onClick={open ? () => { setOpen(false); setSearch(''); } : handleOpen}
|
|
style={{
|
|
width: '100%',
|
|
textAlign: 'left',
|
|
padding: '0 10px',
|
|
height: 36,
|
|
background: 'var(--surface-2, var(--surface))',
|
|
border: open ? '1px solid var(--primary)' : '1px solid var(--border)',
|
|
borderRadius: 6,
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
fontSize: 14,
|
|
color: 'var(--text)',
|
|
outline: open ? '2px solid var(--primary)' : 'none',
|
|
outlineOffset: 1,
|
|
}}
|
|
>
|
|
{selected ? (
|
|
<>
|
|
<FlagIcon code={selected.code} />
|
|
<span>{selected.name}</span>
|
|
{showCode && <span style={{ color: 'var(--text-muted)', fontSize: 12 }}>({selected.code})</span>}
|
|
</>
|
|
) : (
|
|
<span style={{ color: 'var(--text-muted)' }}>Sélectionner un pays…</span>
|
|
)}
|
|
<svg style={{ marginLeft: 'auto', flexShrink: 0, opacity: 0.5, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</button>
|
|
|
|
{/* Dropdown */}
|
|
{open && (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: 'calc(100% + 4px)',
|
|
left: 0,
|
|
right: 0,
|
|
zIndex: 500,
|
|
background: 'var(--surface)',
|
|
border: '1px solid var(--border)',
|
|
borderRadius: 8,
|
|
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{/* Champ de recherche */}
|
|
<div style={{ padding: '8px 8px 4px' }}>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Rechercher un pays…"
|
|
style={{
|
|
width: '100%',
|
|
boxSizing: 'border-box',
|
|
padding: '6px 10px',
|
|
border: '1px solid var(--border)',
|
|
borderRadius: 6,
|
|
background: 'var(--surface-2, var(--surface))',
|
|
color: 'var(--text)',
|
|
fontSize: 13,
|
|
outline: 'none',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Liste des pays */}
|
|
<div
|
|
ref={listRef}
|
|
style={{
|
|
maxHeight: 128,
|
|
overflowY: 'auto',
|
|
padding: '4px 4px 8px',
|
|
}}
|
|
>
|
|
{filtered.length === 0 && (
|
|
<div style={{ padding: '8px 12px', color: 'var(--text-muted)', fontSize: 13 }}>
|
|
Aucun résultat
|
|
</div>
|
|
)}
|
|
{filtered.map(c => (
|
|
<button
|
|
key={c.code}
|
|
type="button"
|
|
onClick={() => select(c.code)}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
width: '100%',
|
|
padding: '6px 10px',
|
|
border: 'none',
|
|
borderRadius: 5,
|
|
background: c.code === value ? 'var(--primary-light, rgba(99,102,241,0.1))' : 'transparent',
|
|
color: c.code === value ? 'var(--primary)' : 'var(--text)',
|
|
cursor: 'pointer',
|
|
fontSize: 13,
|
|
textAlign: 'left',
|
|
fontWeight: c.code === value ? 600 : 400,
|
|
}}
|
|
>
|
|
<FlagIcon code={c.code} />
|
|
<span>{c.name}</span>
|
|
<span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-muted)', opacity: 0.7 }}>{c.code}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|