feat: add CountrySelect component and integrate country selection in endorsement, seafarer, and waiver pages

This commit is contained in:
estifanos
2026-07-30 12:11:53 +00:00
parent 06eb9eca4f
commit 0db451bdec
7 changed files with 125 additions and 24 deletions

View File

@@ -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';

View 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}
/>
);
}