feat: full site build — Project/Melody schema (Option A), admin CRUD, public sections, uploads, email+SMTP, internal analytics, legal pages, docs

This commit is contained in:
2026-08-05 20:53:40 +02:00
parent d87f3033c6
commit c26f41511f
122 changed files with 15068 additions and 41 deletions
+39
View File
@@ -0,0 +1,39 @@
"use server";
import { headers } from "next/headers";
import { prisma } from "@/lib/db";
import { recordAnalyticsEvent } from "@/lib/analytics";
import { sendContactMessageEmail } from "@/lib/email";
import { consumeContactRateLimit, getClientIp } from "@/lib/rate-limit";
import { contactSchema } from "@/lib/validations/contact";
export type ContactActionResult = {
success?: true;
warning?: "email-not-sent";
error?: "invalid" | "rate-limit";
};
export async function submitContactMessage(input: unknown): Promise<ContactActionResult> {
const parsed = contactSchema.safeParse(input);
if (!parsed.success) return { error: "invalid" };
if (parsed.data.website) return { success: true };
const ip = getClientIp(headers());
if (!consumeContactRateLimit(`contact:${ip}`)) return { error: "rate-limit" };
const { name, email, message } = parsed.data;
await prisma.contactMessage.create({ data: { name, email, message } });
try {
await recordAnalyticsEvent({ type: "CONTACT_SUBMITTED", path: "/contact" });
} catch (error) {
console.error("Contact analytics failed", error);
}
try {
await sendContactMessageEmail({ name, email, message });
return { success: true };
} catch (error) {
console.error("Contact email delivery failed", error);
return { success: true, warning: "email-not-sent" };
}
}
+17 -2
View File
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { getActiveLocale, getDictionary, isLocale, type Locale } from "@/lib/i18n";
import { buildPageMetadata } from "@/lib/metadata";
import { getContactChannels, isComingSoonMode } from "@/lib/site";
import { getContactChannels, isSiteClosedMode } from "@/lib/site";
import { notFound, redirect } from "next/navigation";
import ContactForm from "@/components/public/contact-form";
export function generateMetadata({ params }: { params: { locale: string } }): Metadata {
if (!isLocale(params.locale)) {
@@ -17,7 +18,7 @@ export default function ContactPage({ params }: { params: { locale: string } })
notFound();
}
if (isComingSoonMode()) {
if (isSiteClosedMode()) {
redirect("/");
}
@@ -38,6 +39,20 @@ export default function ContactPage({ params }: { params: { locale: string } })
<p>{dictionary.contact.availabilityDescription}</p>
</article>
<ContactForm
labels={{
name: dictionary.contact.formName,
email: dictionary.contact.formEmail,
message: dictionary.contact.formMessage,
submit: dictionary.contact.formSubmit,
sending: dictionary.contact.formSending,
success: dictionary.contact.formSuccess,
savedWarning: dictionary.contact.formSavedWarning,
invalid: dictionary.contact.formInvalid,
rateLimit: dictionary.contact.formRateLimit,
}}
/>
<div className="section-heading">
<h2>{dictionary.contact.channelsTitle}</h2>
</div>