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:
@@ -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" };
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user