158 lines
6.6 KiB
TypeScript
158 lines
6.6 KiB
TypeScript
export const dynamic = 'force-dynamic';
|
|
import { pb, type Diplomatenkennzeichen } from "@/lib/pb";
|
|
|
|
export const metadata = { title: "Diplomatenkennzeichen" };
|
|
|
|
const COUNTRY_NAMES: Record<string, string> = {
|
|
germany: "Deutschland", austria: "Österreich",
|
|
switzerland: "Schweiz", luxembourg: "Luxemburg",
|
|
};
|
|
const COUNTRY_FLAGS: Record<string, string> = {
|
|
germany: "🇩🇪", austria: "🇦🇹", switzerland: "🇨🇭", luxembourg: "🇱🇺",
|
|
};
|
|
const TYPE_LABELS: Record<string, string> = {
|
|
embassy: "Botschaft", consulate: "Konsulat", io: "Intern. Organisation",
|
|
};
|
|
const TYPE_COLORS: Record<string, string> = {
|
|
embassy: "bg-blue-50 text-blue-800 border-blue-200",
|
|
consulate: "bg-amber-50 text-amber-800 border-amber-200",
|
|
io: "bg-emerald-50 text-emerald-800 border-emerald-200",
|
|
};
|
|
|
|
interface Props { searchParams: { land?: string } }
|
|
|
|
async function getData(baseCountry: string) {
|
|
try {
|
|
// Ohne sort um 400 zu vermeiden, sortieren wir client-side
|
|
return pb.collection("diplomatenkennzeichen").getFullList<Diplomatenkennzeichen>({
|
|
filter: `base_country="${baseCountry}"`,
|
|
});
|
|
} catch { return []; }
|
|
}
|
|
|
|
async function getAvailableCountries(): Promise<string[]> {
|
|
try {
|
|
const all = await pb.collection("diplomatenkennzeichen").getFullList<Diplomatenkennzeichen>({
|
|
fields: "base_country",
|
|
});
|
|
return [...new Set(all.map((i: any) => i.base_country).filter(Boolean))].sort() as string[];
|
|
} catch { return ["germany"]; }
|
|
}
|
|
|
|
function sortByCode(a: Diplomatenkennzeichen, b: Diplomatenkennzeichen) {
|
|
const parseCode = (c: string) => {
|
|
const m = c.match(/^(?:0-)?(\d+)$/);
|
|
return m ? parseInt(m[1]) : -1;
|
|
};
|
|
const na = parseCode(a.code);
|
|
const nb = parseCode(b.code);
|
|
if (na >= 0 && nb >= 0) return na - nb;
|
|
if (na >= 0) return -1;
|
|
if (nb >= 0) return 1;
|
|
return a.code.localeCompare(b.code);
|
|
}
|
|
|
|
export default async function DiplomatenPage({ searchParams }: Props) {
|
|
const countries = await getAvailableCountries();
|
|
const activeLand = searchParams.land ?? countries[0] ?? "germany";
|
|
const rawItems = await getData(activeLand);
|
|
const items = [...rawItems].sort(sortByCode);
|
|
|
|
const grouped = items.reduce<Record<string, Diplomatenkennzeichen[]>>((acc, item) => {
|
|
const t = item.type ?? "embassy";
|
|
if (!acc[t]) acc[t] = [];
|
|
acc[t].push(item);
|
|
return acc;
|
|
}, {});
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto px-6 py-12">
|
|
<div className="mb-8">
|
|
<div className="text-xs font-mono text-[var(--muted)] tracking-widest uppercase mb-2">Datenbank</div>
|
|
<h1 style={{ fontFamily: "'Syne',sans-serif", fontWeight: 800, fontSize: "2.25rem", letterSpacing: "-0.03em" }}
|
|
className="text-[var(--ink)]">Diplomatenkennzeichen</h1>
|
|
<p className="text-[var(--muted)] mt-2 max-w-2xl">
|
|
Sonderkennzeichen für Botschaften, Konsulate und internationale Organisationen.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Länder-Tabs */}
|
|
<div className="flex gap-2 flex-wrap mb-8">
|
|
{countries.map(c => (
|
|
<a key={c} href={`?land=${c}`}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg border text-sm transition-colors ${
|
|
c === activeLand
|
|
? "bg-[var(--ink)] text-[var(--paper)] border-[var(--ink)]"
|
|
: "border-[var(--warm)] text-[var(--muted)] hover:border-[var(--ink)] hover:text-[var(--ink)]"
|
|
}`}
|
|
style={{ fontFamily: "'Syne',sans-serif", fontWeight: 500 }}>
|
|
<span>{COUNTRY_FLAGS[c] ?? "🌍"}</span>
|
|
<span>{COUNTRY_NAMES[c] ?? c}</span>
|
|
</a>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mb-6 flex gap-3 flex-wrap">
|
|
{Object.entries(TYPE_LABELS).map(([key, label]) => (
|
|
<span key={key} className={`px-2.5 py-1 rounded border text-xs font-medium ${TYPE_COLORS[key]}`}>
|
|
{label}: {grouped[key]?.length ?? 0}
|
|
</span>
|
|
))}
|
|
<span className="text-sm text-[var(--muted)] self-center">— {items.length} Einträge gesamt</span>
|
|
</div>
|
|
|
|
{items.length === 0 ? (
|
|
<div className="text-center py-20 text-[var(--muted)]">Keine Daten für dieses Land.</div>
|
|
) : (
|
|
Object.entries(TYPE_LABELS).map(([type, typeLabel]) => {
|
|
const entries = grouped[type];
|
|
if (!entries?.length) return null;
|
|
return (
|
|
<div key={type} className="mb-10">
|
|
<h2 className="text-base mb-3 flex items-center gap-2"
|
|
style={{ fontFamily: "'Syne',sans-serif", fontWeight: 700 }}>
|
|
<span className={`px-2.5 py-0.5 rounded border text-xs ${TYPE_COLORS[type]}`}>{typeLabel}</span>
|
|
<span className="text-[var(--muted)] text-sm font-normal">{entries.length} Einträge</span>
|
|
</h2>
|
|
<div className="border border-[var(--warm)] rounded-lg overflow-hidden bg-white/40">
|
|
<div className="overflow-x-auto">
|
|
<table className="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Kürzel</th>
|
|
<th>Land / Organisation</th>
|
|
<th className="hidden md:table-cell">Stadt</th>
|
|
<th className="hidden lg:table-cell">Bemerkung</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{entries.map((d) => (
|
|
<tr key={d.id}>
|
|
<td><span className="kz-badge font-mono text-xs">{d.code}</span></td>
|
|
<td>
|
|
<div className="font-medium text-sm">{d.country}</div>
|
|
{d.organisation && <div className="text-xs text-[var(--muted)]">{d.organisation}</div>}
|
|
</td>
|
|
<td className="text-sm text-[var(--muted)] hidden md:table-cell">{d.city}</td>
|
|
<td className="text-sm text-[var(--muted)] hidden lg:table-cell">{d.note}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
|
|
<div className="mt-8 p-5 border border-dashed border-[var(--warm)] rounded-lg text-center">
|
|
<p className="text-sm text-[var(--muted)] mb-2">Daten unvollständig oder fehlerhaft?</p>
|
|
<a href="https://git.denode.eu/denode/plates" target="_blank" rel="noopener"
|
|
className="text-sm text-[var(--accent)] hover:underline font-medium">
|
|
git.denode.eu/denode/plates →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|