mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-27 15:30:57 +00:00
feat: add CountrySelect component and integrate country selection in endorsement, seafarer, and waiver pages
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export * from './lib/input/BilingualInput';
|
||||
export * from './lib/input/CountrySelect';
|
||||
export * from './lib/feedback/ConfirmModal';
|
||||
export * from './lib/feedback/ApiErrorAlert';
|
||||
export * from './lib/feedback/notify';
|
||||
|
||||
84
libs/ui/src/lib/input/CountrySelect.tsx
Normal file
84
libs/ui/src/lib/input/CountrySelect.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Group, Select, Text, type ComboboxItem, type SelectProps } from '@mantine/core';
|
||||
import { getData, getName, overwrite } from 'country-list';
|
||||
import * as Flags from 'country-flag-icons/react/3x2';
|
||||
|
||||
// Friendlier labels for a few ISO-official names ("Korea, Republic of" → "South Korea").
|
||||
// Full list still comes from country-list; this only renames.
|
||||
overwrite([
|
||||
{ code: 'BO', name: 'Bolivia' },
|
||||
{ code: 'IR', name: 'Iran' },
|
||||
{ code: 'KP', name: 'North Korea' },
|
||||
{ code: 'KR', name: 'South Korea' },
|
||||
{ code: 'RU', name: 'Russia' },
|
||||
{ code: 'SY', name: 'Syria' },
|
||||
{ code: 'TW', name: 'Taiwan' },
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'VN', name: 'Vietnam' },
|
||||
]);
|
||||
|
||||
// Built once at module load — static data, no hook memoization needed.
|
||||
const COUNTRIES = getData()
|
||||
.map(({ code, name }) => ({ value: code, label: name }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
/** English country name for an alpha-2 code; '' when unset/unknown. */
|
||||
export function getCountryName(code: string | null | undefined): string {
|
||||
return code ? (getName(code) ?? '') : '';
|
||||
}
|
||||
|
||||
function CountryFlag({ code }: { code: string }) {
|
||||
const Flag = Flags[code as keyof typeof Flags];
|
||||
return Flag ? <Flag style={{ width: 22, borderRadius: 2, display: 'block' }} /> : null;
|
||||
}
|
||||
|
||||
// Case-insensitive substring on name, plus ISO code prefix ("et" → Ethiopia).
|
||||
const filterCountries: SelectProps['filter'] = ({ options, search }) => {
|
||||
const q = search.trim().toLowerCase();
|
||||
if (!q) return options;
|
||||
return (options as ComboboxItem[]).filter(
|
||||
(o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().startsWith(q),
|
||||
);
|
||||
};
|
||||
|
||||
// Module scope → stable reference, no re-render churn.
|
||||
const renderCountryOption: SelectProps['renderOption'] = ({ option }) => (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<CountryFlag code={option.value} />
|
||||
<Text fz="sm">{option.label}</Text>
|
||||
</Group>
|
||||
);
|
||||
|
||||
export interface CountrySelectProps {
|
||||
value: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
description?: React.ReactNode;
|
||||
error?: React.ReactNode;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function CountrySelect({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Select a country',
|
||||
...rest
|
||||
}: CountrySelectProps) {
|
||||
return (
|
||||
<Select
|
||||
data={COUNTRIES}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
leftSection={value ? <CountryFlag code={value} /> : undefined}
|
||||
renderOption={renderCountryOption}
|
||||
filter={filterCountries}
|
||||
nothingFoundMessage="No countries found"
|
||||
searchable
|
||||
clearable
|
||||
maxDropdownHeight={320}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user