Compare commits

...

2 commits

Author SHA1 Message Date
27fb01f619 responsive design 2026-05-20 21:52:36 +02:00
338f04fda3 import script for backups updated 2026-05-20 21:15:36 +02:00
7 changed files with 68 additions and 26 deletions

View file

@ -1,7 +0,0 @@
# PocketBase Admin-Zugangsdaten
# Kopieren nach .env und ausfüllen:
# cp .env.example .env
PB_URL=http://localhost:4444
PB_EMAIL=admin@example.com
PB_PASSWORD=

View file

@ -4,8 +4,8 @@ services:
restart: unless-stopped
volumes:
- pb_data:/pb/pb_data
# ports:
#- "4444:8090"
ports:
- "4444:8090"
nextjs:
build: ./frontend
restart: unless-stopped

View file

@ -14,7 +14,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Nav />
<main className="min-h-screen">{children}</main>
<footer className="border-t border-[var(--warm)] mt-24 py-10 px-6">
<div className="max-w-6xl mx-auto flex items-center justify-between text-sm text-[var(--muted)]">
<div className="max-w-6xl mx-auto flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 text-sm text-[var(--muted)]">
<span style={{ fontFamily: "'Syne', sans-serif", fontWeight: 600 }}>
Kennzeichensammler
</span>

View file

@ -1,8 +1,10 @@
export const dynamic = 'force-dynamic';
import { unstable_noStore as noStore } from 'next/cache';
import Link from "next/link";
import { pb, COUNTRY_LABELS, COUNTRY_FLAGS } from "@/lib/pb";
async function getStats() {
noStore();
try {
const [kz, diplo, gesehen] = await Promise.all([
pb.collection("kennzeichen").getList(1, 1, { filter: "active=true" }),
@ -20,6 +22,7 @@ async function getStats() {
}
async function getLasteSeen() {
noStore();
try {
return pb.collection("gesehen").getList(1, 5, { sort: "-datum" });
} catch {
@ -64,7 +67,7 @@ export default async function Home() {
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-20">
<div className="grid grid-cols-3 gap-3 sm:gap-4 mb-20">
{[
{ label: "Kennzeichen", value: stats.kennzeichen.toLocaleString("de") },
{ label: "Diplomaten­kz.", value: stats.diplomaten.toLocaleString("de") },

View file

@ -39,7 +39,7 @@ export default async function SammlungPage() {
<h1 style={{ fontFamily:"'Syne',sans-serif", fontWeight:800, fontSize:"2.25rem", letterSpacing:"-0.03em" }} className="text-[var(--ink)]">Meine Sammlung</h1>
</div>
<div className="border border-[var(--warm)] rounded-lg p-8 bg-white/40 mb-8">
<div className="flex items-end justify-between mb-4">
<div className="flex flex-wrap items-end justify-between gap-2 mb-4">
<div>
<div style={{ fontFamily:"'Syne',sans-serif", fontWeight:800, fontSize:"3rem", lineHeight:1 }} className="text-[var(--ink)]">{stats.total.toLocaleString("de")}</div>
<div className="text-[var(--muted)] text-sm mt-1">von {stats.totalKz.toLocaleString("de")} Kennzeichen gesehen</div>

View file

@ -1,30 +1,35 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import clsx from "clsx";
const links = [
{ href: "/", label: "Start" },
{ href: "/kennzeichen", label: "Datenbank" },
{ href: "/diplomatenkennzeichen",label: "Diplomaten" },
{ href: "/sammlung", label: "Sammlung" },
{ href: "/blog", label: "Blog" },
{ href: "/", label: "Start" },
{ href: "/kennzeichen", label: "Datenbank" },
{ href: "/diplomatenkennzeichen", label: "Diplomaten" },
{ href: "/sammlung", label: "Sammlung" },
{ href: "/blog", label: "Blog" },
];
export default function Nav() {
const path = usePathname();
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-50 border-b border-[var(--warm)] bg-[var(--paper)]/90 backdrop-blur-sm">
<div className="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<Link
href="/"
style={{ fontFamily: "'Syne', sans-serif", fontWeight: 700, fontSize: 15, letterSpacing: "0em" }}
style={{ fontFamily: "'Syne', sans-serif", fontWeight: 700, fontSize: 15 }}
className="text-[var(--ink)] hover:text-[var(--accent)] transition-colors"
onClick={() => setOpen(false)}
>
Kennzeichensammler
</Link>
<nav className="flex items-center gap-1">
{/* Desktop nav */}
<nav className="hidden sm:flex items-center gap-1">
{links.map((l) => (
<Link
key={l.href}
@ -41,7 +46,40 @@ export default function Nav() {
</Link>
))}
</nav>
{/* Mobile hamburger */}
<button
className="sm:hidden flex flex-col gap-1.5 p-2 -mr-2"
onClick={() => setOpen((o) => !o)}
aria-label="Menü"
>
<span className={clsx("block w-5 h-0.5 bg-[var(--ink)] transition-all origin-center", open && "rotate-45 translate-y-2")} />
<span className={clsx("block w-5 h-0.5 bg-[var(--ink)] transition-all", open && "opacity-0")} />
<span className={clsx("block w-5 h-0.5 bg-[var(--ink)] transition-all origin-center", open && "-rotate-45 -translate-y-2")} />
</button>
</div>
{/* Mobile menu */}
{open && (
<div className="sm:hidden border-t border-[var(--warm)] bg-[var(--paper)] px-4 py-3 flex flex-col gap-1">
{links.map((l) => (
<Link
key={l.href}
href={l.href}
onClick={() => setOpen(false)}
className={clsx(
"px-3 py-2.5 rounded-md text-sm transition-colors",
path === l.href
? "bg-[var(--ink)] text-[var(--paper)]"
: "text-[var(--muted)] hover:text-[var(--ink)] hover:bg-[var(--warm)]"
)}
style={{ fontFamily: "'Syne', sans-serif", fontWeight: 500 }}
>
{l.label}
</Link>
))}
</div>
)}
</header>
);
}

View file

@ -385,13 +385,6 @@ def import_backup(pb, backup_path, db_path):
if land in ("global", "global2", "laender"):
kz_code = normalize_diplo_code(kz_code)
# Duplikat prüfen
existing = pb.find_record("gesehen",
f'kennzeichen_code="{kz_code}" && land="{folder}"')
if existing:
skipped += 1
continue
# Kennzeichen-Namen aus Referenz holen
try:
rcur.execute(f"SELECT ort FROM {land} WHERE id=?", (kennid,))
@ -400,6 +393,21 @@ def import_backup(pb, backup_path, db_path):
except:
kz_name = ""
existing = pb.find_record("gesehen",
f'kennzeichen_code="{kz_code}" && land="{folder}"')
if existing:
# Datum aktualisieren wenn neue Sichtung neuer ist
if datum_iso > (existing.get("datum") or ""):
requests.patch(
f"{pb.url}/api/collections/gesehen/records/{existing['id']}",
headers=pb.h(),
json={"datum": datum_iso},
)
imported += 1
else:
skipped += 1
continue
pb.create("gesehen", {
"kennzeichen_code": kz_code,
"kennzeichen_name": kz_name,