feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -0,0 +1,145 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { BackLink } from "@/components/back-link";
|
||||
import { CONTENT_REGISTRY, type ContentFieldDef } from "@/lib/content-registry";
|
||||
import { updateSiteContentAction } from "../actions";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
|
||||
const ROWS_BY_TYPE: Record<ContentFieldDef["type"], number> = {
|
||||
text: 1,
|
||||
textarea: 3,
|
||||
"list-simple": 4,
|
||||
"list-titled": 5,
|
||||
"list-numbered": 6,
|
||||
"richtext-sections": 14,
|
||||
};
|
||||
|
||||
async function loadCurrentValues(): Promise<Map<string, { valueDe: string; valueEn: string | null }>> {
|
||||
const rows = await prisma.siteContent.findMany();
|
||||
return new Map(rows.map((r) => [r.key, { valueDe: r.valueDe, valueEn: r.valueEn }]));
|
||||
}
|
||||
|
||||
export default async function AdminInhaltePage(props: PageProps<"/admin/inhalte">) {
|
||||
const locale = await getLocale();
|
||||
const t = getDictionary(locale);
|
||||
const { saved } = await props.searchParams;
|
||||
const current = await loadCurrentValues();
|
||||
|
||||
const groups: { key: ContentFieldDef["group"]; heading: string }[] = [
|
||||
{ key: "company", heading: t.adminContentPage.groupCompany },
|
||||
{ key: "home", heading: t.adminContentPage.groupHome },
|
||||
{ key: "legal", heading: t.adminContentPage.groupLegal },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackLink href="/admin" label={t.adminContentPage.backToDashboard} />
|
||||
<h1 className="text-2xl text-forest mb-2">{t.adminContentPage.title}</h1>
|
||||
<p className="text-sm text-ink/65 mb-8 max-w-2xl">{t.adminContentPage.intro}</p>
|
||||
|
||||
{saved ? (
|
||||
<p className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">
|
||||
{t.adminContentPage.saved}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{groups.map((group) => {
|
||||
const fields = CONTENT_REGISTRY.filter((f) => f.group === group.key);
|
||||
return (
|
||||
<form
|
||||
key={group.key}
|
||||
action={updateSiteContentAction}
|
||||
className="mb-10 bg-background border border-ink/10 p-6 space-y-6"
|
||||
>
|
||||
<input type="hidden" name="group" value={group.key} />
|
||||
<h2 className="text-lg text-forest">{group.heading}</h2>
|
||||
|
||||
{fields.map((def) => {
|
||||
const row = current.get(def.key);
|
||||
const rows = ROWS_BY_TYPE[def.type];
|
||||
const label = locale === "de" ? def.labelDe : def.labelEn;
|
||||
|
||||
if (!def.localized) {
|
||||
return (
|
||||
<label key={def.key} className="block text-sm">
|
||||
<span className="text-ink/70">{label}</span>
|
||||
{def.type === "textarea" ? (
|
||||
<textarea
|
||||
name={def.key}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={def.key}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
const isTextarea = def.type !== "text";
|
||||
return (
|
||||
<div key={def.key} className="border-t border-ink/10 pt-5 first:border-t-0 first:pt-0">
|
||||
<p className="text-sm text-ink/70 mb-2">{label}</p>
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<label className="block text-xs">
|
||||
<span className="text-ink/50">{t.adminContentPage.deLabel}</span>
|
||||
{isTextarea ? (
|
||||
<textarea
|
||||
name={`de::${def.key}`}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`de::${def.key}`}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
<label className="block text-xs">
|
||||
<span className="text-ink/50">{t.adminContentPage.enLabel}</span>
|
||||
{isTextarea ? (
|
||||
<textarea
|
||||
name={`en::${def.key}`}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`en::${def.key}`}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
||||
{t.adminContentPage.save}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user