diff --git a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx index 0a26ebb7e..a92437be3 100644 --- a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx +++ b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { CountrySelect, getCountryName } from '@ema-platform/ui'; import { Alert, Badge, @@ -84,7 +85,7 @@ function ApplicationWizard({ onDone }: { onDone: () => void }) { const [step, setStep] = useState(0); const [cocNo, setCocNo] = useState(''); const [issuer, setIssuer] = useState(''); - const [country, setCountry] = useState(''); + const [country, setCountry] = useState(null); const [cocType, setCocType] = useState(''); const [issueDate, setIssueDate] = useState(''); const [expiryDate, setExpiryDate] = useState(''); @@ -129,7 +130,7 @@ function ApplicationWizard({ onDone }: { onDone: () => void }) { setCocNo(e.currentTarget.value)} required /> - setCountry(e.currentTarget.value)} required /> + setIssuer(e.currentTarget.value)} required /> setCocType(e.currentTarget.value)} required /> setIssueDate(e.currentTarget.value)} required /> @@ -231,7 +232,7 @@ function ApplicationWizard({ onDone }: { onDone: () => void }) { {[ ['CoC Number', cocNo], - ['Country', country], + ['Country', getCountryName(country)], ['Issuer', issuer], ['CoC Type', cocType], ['Issue Date', issueDate], diff --git a/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx index 61ea55060..70fc7f583 100644 --- a/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx +++ b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx @@ -45,7 +45,7 @@ import { IconX, } from '@tabler/icons-react'; import { useNavigate, useParams } from 'react-router-dom'; -import { notify, useErrorHandler } from '@ema-platform/ui'; +import { CountrySelect, notify, useErrorHandler } from '@ema-platform/ui'; import type { Seafarer } from './SeafarerRegistryPage'; // --------------------------------------------------------------------------- @@ -506,6 +506,8 @@ function AddMedicalModal({ opened, onClose }: { opened: boolean; onClose: () => } function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: () => void }) { + const [flag, setFlag] = useState(null); + return ( @@ -515,7 +517,7 @@ function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: () - + diff --git a/apps/portal/src/app/features/waiver/pages/WaiverApplicationPage.tsx b/apps/portal/src/app/features/waiver/pages/WaiverApplicationPage.tsx index 2d039118c..02a3ba27c 100644 --- a/apps/portal/src/app/features/waiver/pages/WaiverApplicationPage.tsx +++ b/apps/portal/src/app/features/waiver/pages/WaiverApplicationPage.tsx @@ -34,7 +34,7 @@ import { IconShieldOff, IconShip, } from '@tabler/icons-react'; -import { notify } from '@ema-platform/ui'; +import { CountrySelect, getCountryName, notify } from '@ema-platform/ui'; const STEPS = [ { label: 'Waiver Type & Applicant' }, @@ -200,7 +200,7 @@ export function WaiverApplicationPage() { const [invoiceNumber, setInvoiceNumber] = useState(''); const [invoiceDate, setInvoiceDate] = useState(''); const [invoiceAmount, setInvoiceAmount] = useState(''); - const [countryOfOrigin, setCountryOfOrigin] = useState(''); + const [countryOfOrigin, setCountryOfOrigin] = useState(null); const [supplierName, setSupplierName] = useState(''); const [portOfLoading, setPortOfLoading] = useState(''); @@ -266,7 +266,7 @@ export function WaiverApplicationPage() { businessLicenseNumber, applicantAddress, reasonNonEsl, bankName, bankBranch, letterRecipient, letterLanguage, productName, productDescription, quantity, unitOfMeasurement, - invoiceNumber, invoiceDate, invoiceAmount, countryOfOrigin, supplierName, + invoiceNumber, invoiceDate, invoiceAmount, countryOfOrigin: getCountryName(countryOfOrigin), supplierName, portOfLoading, portOfDischarge, fromPortName, toPortName, vesselName, carrierName, billOfLadingNumber, estimatedArrivalDate, actualArrivalDate, }, @@ -366,7 +366,7 @@ export function WaiverApplicationPage() { setProductDescription(e.currentTarget.value)} /> setUnitOfMeasurement(e.currentTarget.value)} /> - setCountryOfOrigin(e.currentTarget.value)} /> + setSupplierName(e.currentTarget.value)} /> diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index fa10321ca..37584299b 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -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'; diff --git a/libs/ui/src/lib/input/CountrySelect.tsx b/libs/ui/src/lib/input/CountrySelect.tsx new file mode 100644 index 000000000..4848626be --- /dev/null +++ b/libs/ui/src/lib/input/CountrySelect.tsx @@ -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 ? : 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 }) => ( + + + {option.label} + +); + +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 ( +