1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:19:35 +01:00
effigenix/frontend/apps/cli/src/components/shared/CountryPicker.tsx
Sebastian Frick a77f0ec5df feat(shared): Länderauswahl mit ISO 3166-1 Mapping und CountryPicker
Backend: Country-Record (Shared Kernel), InMemoryCountryRepository mit
~249 Ländern und DACH-Priorisierung, ListCountries-UseCase,
GET /api/countries?q= Endpoint.

Frontend: CountryPicker-Komponente mit Fuzzy-Suche, DACH-Favoriten bei
leerem Query. SupplierCreate-, CustomerCreate- und AddDeliveryAddress-
Screens verwenden jetzt den CountryPicker statt Freitext. Detail-Screens
zeigen den Ländercode in der Adressanzeige.

Closes #71
2026-02-24 09:28:56 +01:00

129 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useMemo } from 'react';
import { Box, Text, useInput } from 'ink';
import type { CountryDTO } from '@effigenix/api-client';
interface CountryPickerProps {
countries: CountryDTO[];
query: string;
onQueryChange: (q: string) => void;
onSelect: (country: CountryDTO) => void;
focus: boolean;
selectedName?: string;
maxVisible?: number;
}
const DACH_CODES = ['DE', 'AT', 'CH'];
export function CountryPicker({
countries,
query,
onQueryChange,
onSelect,
focus,
selectedName,
maxVisible = 5,
}: CountryPickerProps) {
const [cursor, setCursor] = useState(0);
const filtered = useMemo(() => {
if (!query) {
// Show DACH favorites when no query
return countries.filter((c) => DACH_CODES.includes(c.code)).slice(0, maxVisible);
}
const q = query.toLowerCase();
return countries.filter(
(c) => c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q),
).slice(0, maxVisible);
}, [countries, query, maxVisible]);
useInput((input, key) => {
if (!focus) return;
if (key.upArrow) {
setCursor((c) => Math.max(0, c - 1));
return;
}
if (key.downArrow) {
setCursor((c) => Math.min(filtered.length - 1, c + 1));
return;
}
if (key.return && filtered.length > 0) {
const selected = filtered[cursor];
if (selected) onSelect(selected);
return;
}
if (key.backspace || key.delete) {
onQueryChange(query.slice(0, -1));
setCursor(0);
return;
}
if (key.tab || key.escape || key.ctrl || key.meta) return;
if (input && !key.upArrow && !key.downArrow) {
onQueryChange(query + input);
setCursor(0);
}
}, { isActive: focus });
if (!focus && selectedName) {
return (
<Box flexDirection="column">
<Text color="gray">Land *</Text>
<Box>
<Text color="gray"> </Text>
<Text color="green"> {selectedName}</Text>
</Box>
</Box>
);
}
if (focus && selectedName && !query) {
return (
<Box flexDirection="column">
<Text color="cyan">Land * (tippen zum Ändern)</Text>
<Box>
<Text color="gray"> </Text>
<Text color="green"> {selectedName}</Text>
</Box>
{filtered.length > 0 && (
<Box flexDirection="column" paddingLeft={2}>
{filtered.map((c, i) => (
<Box key={c.code}>
<Text color={i === cursor ? 'cyan' : 'white'}>
{i === cursor ? '▶ ' : ' '}{c.code} {c.name}
</Text>
</Box>
))}
</Box>
)}
</Box>
);
}
return (
<Box flexDirection="column">
<Text color={focus ? 'cyan' : 'gray'}>Land * (Suche)</Text>
<Box>
<Text color="gray"> </Text>
<Text>{query || (focus ? '▌' : '')}</Text>
</Box>
{focus && filtered.length > 0 && (
<Box flexDirection="column" paddingLeft={2}>
{filtered.map((c, i) => (
<Box key={c.code}>
<Text color={i === cursor ? 'cyan' : 'white'}>
{i === cursor ? '▶ ' : ' '}{c.code} {c.name}
</Text>
</Box>
))}
</Box>
)}
{focus && query && filtered.length === 0 && (
<Box paddingLeft={2}>
<Text color="yellow">Kein Land gefunden.</Text>
</Box>
)}
</Box>
);
}