fix(admin): persist data directory and pass env vars

This commit is contained in:
Schubert Ferenc 2026-07-03 22:25:04 +02:00
parent 99e8201745
commit 1d7e7c41c9
39 changed files with 1297 additions and 32 deletions

View file

@ -0,0 +1,50 @@
"use client";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useState, type FormEvent } from "react";
export default function AdminLoginForm({ nextPath = "/admin" }: { nextPath?: string }) {
const router = useRouter();
const [error, setError] = useState("");
const [pending, setPending] = useState(false);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError("");
setPending(true);
try {
const response = await fetch("/api/admin/login", {
method: "POST",
body: new FormData(event.currentTarget),
});
const result = await response.json() as { message?: string };
if (!response.ok) throw new Error(result.message ?? "Anmeldung fehlgeschlagen.");
router.push(nextPath);
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Anmeldung fehlgeschlagen.");
} finally {
setPending(false);
}
}
return (
<main className="admin-login-page">
<form className="admin-login-card" onSubmit={submit}>
<Image src="/funktechnik_schubert_logo.jpg" alt="Funktechnik Schubert" width={1672} height={941} priority />
<h1>Administration Login</h1>
<label>
E-Mail
<input name="email" type="email" autoComplete="username" required />
</label>
<label>
Passwort
<input name="password" type="password" autoComplete="current-password" required />
</label>
{error && <p className="error">{error}</p>}
<button className="button" type="submit" disabled={pending}>{pending ? "Anmeldung..." : "Anmelden"}</button>
</form>
</main>
);
}

View file

@ -0,0 +1,48 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import type { ReactNode } from "react";
const navItems = [
["Dashboard", "/admin"],
["Kontaktanfragen", "/admin/kontaktanfragen"],
["Reparaturanfragen", "/admin/reparaturanfragen"],
["Website-Inhalte", "/admin/website"],
["Medien", "/admin/medien"],
["Einstellungen", "/admin/einstellungen"],
["SMTP", "/admin/smtp"],
["SEO", "/admin/seo"],
["System", "/admin/system"],
] as const;
export default function AdminShell({ children }: { children: ReactNode }) {
const pathname = usePathname();
const router = useRouter();
async function logout() {
await fetch("/api/admin/logout", { method: "POST" });
router.push("/admin/login");
router.refresh();
}
return (
<div className="admin-shell">
<aside className="admin-sidebar">
<Link className="admin-logo" href="/admin">
<Image src="/funktechnik_schubert_logo.jpg" alt="Funktechnik Schubert" width={1672} height={941} />
</Link>
<nav className="admin-nav" aria-label="Administration">
{navItems.map(([label, href]) => (
<Link key={href} href={href} aria-current={pathname === href ? "page" : undefined}>
{label}
</Link>
))}
</nav>
<button className="admin-logout" type="button" onClick={logout}>Abmelden</button>
</aside>
<div className="admin-main">{children}</div>
</div>
);
}

View file

@ -0,0 +1,34 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import type { InquiryStatus } from "@/lib/admin/types";
type Props = {
id: string;
type: "contact" | "repair";
status: InquiryStatus;
};
export default function StatusSelect({ id, type, status }: Props) {
const router = useRouter();
const [value, setValue] = useState(status);
async function update(nextStatus: InquiryStatus) {
setValue(nextStatus);
const response = await fetch(`/api/admin/${type}/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: nextStatus }),
});
if (response.ok) router.refresh();
}
return (
<select className="status-select" value={value} onChange={(event) => update(event.target.value as InquiryStatus)}>
<option value="new">Neu</option>
<option value="in_progress">In Prüfung</option>
<option value="done">Erledigt</option>
</select>
);
}