Initialize project with Payload CMS
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 32s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-27 21:01:36 +01:00
parent 9d0442e88f
commit 96cc1b0736
492 changed files with 9690 additions and 113187 deletions

View File

@@ -0,0 +1,17 @@
import configPromise from '@payload-config';
import { RootPage } from '@payloadcms/next/views';
import { importMap } from '../importMap';
type Args = {
params: Promise<{
segments: string[];
}>;
searchParams: Promise<{
[key: string]: string | string[];
}>;
};
const Page = ({ params, searchParams }: Args) =>
RootPage({ config: configPromise, importMap, params, searchParams });
export default Page;

View File

@@ -0,0 +1 @@
export const importMap = {};

View File

@@ -0,0 +1,14 @@
import config from '@payload-config';
import {
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_DELETE,
} from '@payloadcms/next/routes';
export const GET = REST_GET(config);
export const POST = REST_POST(config);
export const DELETE = REST_DELETE(config);
export const PATCH = REST_PATCH(config);
export const OPTIONS = REST_OPTIONS(config);

View File

@@ -0,0 +1,30 @@
import configPromise from '@payload-config';
import { RootLayout } from '@payloadcms/next/layouts';
import React from 'react';
import '@payloadcms/next/css';
import { handleServerFunctions } from '@payloadcms/next/layouts';
import { importMap } from './admin/importMap';
type Args = {
children: React.ReactNode;
};
const serverFunction: any = async function (args: any) {
'use server';
return handleServerFunctions({
...args,
config: configPromise,
importMap,
});
};
const Layout = ({ children }: Args) => {
return (
<RootLayout config={configPromise} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
);
};
export default Layout;

View File

@@ -0,0 +1,93 @@
'use server';
import { sendEmail } from '@/lib/mail/mailer';
import { render, ContactFormNotification, ConfirmationMessage } from '@mintel/mail';
import React from 'react';
export async function sendContactFormAction(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const message = formData.get('message') as string;
const productName = formData.get('productName') as string | null;
if (!name || !email || !message) {
console.warn('Missing required fields in contact form');
return { success: false, error: 'Missing required fields' };
}
// 1. Save to CMS
try {
const { getPayload } = await import('payload');
const configPromise = (await import('@payload-config')).default;
const payload = await getPayload({ config: configPromise });
await payload.create({
collection: 'form-submissions',
data: {
name,
email,
message,
type: productName ? 'product_quote' : 'contact',
productName: productName || undefined,
},
});
console.log('Successfully saved form submission to Payload CMS');
} catch (error) {
console.error('Failed to store submission in Payload CMS', { error });
}
// 2. Send Emails
console.log('Sending branded emails', { email, productName });
const notificationSubject = productName
? `Product Inquiry: ${productName}`
: 'New Contact Form Submission';
const confirmationSubject = 'Thank you for your inquiry';
try {
// 2a. Send notification to Mintel/Client
const notificationHtml = await render(
React.createElement(ContactFormNotification, {
name,
email,
message,
productName: productName || undefined,
}),
);
const notificationResult = await sendEmail({
replyTo: email,
subject: notificationSubject,
html: notificationHtml,
});
if (!notificationResult.success) {
console.error('Notification email FAILED', { error: notificationResult.error });
}
// 2b. Send confirmation to Customer
const confirmationHtml = await render(
React.createElement(ConfirmationMessage, {
name,
clientName: 'Cable Creations',
}),
);
const confirmationResult = await sendEmail({
to: email,
subject: confirmationSubject,
html: confirmationHtml,
});
if (!confirmationResult.success) {
console.error('Confirmation email FAILED', { error: confirmationResult.error });
}
return { success: true };
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error('Failed to send branded emails', { error: errorMsg });
return { success: false, error: errorMsg };
}
}

View File

@@ -0,0 +1,62 @@
import nodemailer from 'nodemailer';
let transporterInstance: nodemailer.Transporter | null = null;
function getTransporter() {
if (transporterInstance) return transporterInstance;
if (!process.env.MAIL_HOST) {
throw new Error('MAIL_HOST is not configured. Please check your environment variables.');
}
transporterInstance = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: Number(process.env.MAIL_PORT) || 587,
secure: Number(process.env.MAIL_PORT) === 465,
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
},
});
return transporterInstance;
}
interface SendEmailOptions {
to?: string | string[];
replyTo?: string;
subject: string;
html: string;
}
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
const recipients = to || process.env.MAIL_RECIPIENTS;
if (!recipients) {
console.error('No email recipients configured', { subject });
return { success: false as const, error: 'No recipients configured' };
}
if (!process.env.MAIL_FROM) {
console.error('MAIL_FROM is not configured', { subject, recipients });
return { success: false as const, error: 'MAIL_FROM is not configured' };
}
const mailOptions = {
from: process.env.MAIL_FROM,
to: recipients,
replyTo,
subject,
html,
};
try {
const info = await getTransporter().sendMail(mailOptions);
console.log('Email sent successfully', { messageId: info.messageId, subject, recipients });
return { success: true, messageId: info.messageId };
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error('Error sending email', { error: errorMsg, subject, recipients });
return { success: false, error: errorMsg };
}
}

View File

@@ -1,5 +1,6 @@
import type { NextConfig } from "next";
import createMDX from "@next/mdx";
import { withPayload } from "@payloadcms/next/withPayload";
const nextConfig: NextConfig = {
/* config options here */
@@ -11,4 +12,4 @@ const withMDX = createMDX({
});
// Merge MDX config with Next.js config
export default withMDX(nextConfig);
export default withPayload(withMDX(nextConfig));

View File

@@ -6,27 +6,41 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"cms:migrate": "payload migrate",
"cms:generate:types": "payload generate:types"
},
"dependencies": {
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@mintel/mail": "^1.8.21",
"@next/mdx": "^16.1.3",
"@payloadcms/db-postgres": "^3.77.0",
"@payloadcms/email-nodemailer": "^3.77.0",
"@payloadcms/next": "^3.77.0",
"@payloadcms/richtext-lexical": "^3.77.0",
"@payloadcms/ui": "^3.77.0",
"@types/mdx": "^2.0.13",
"graphql": "^16.12.0",
"next": "16.1.3",
"nodemailer": "^7.0.12",
"payload": "^3.77.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"remark-frontmatter": "^5.0.0",
"remark-mdx-frontmatter": "^5.2.0"
"remark-mdx-frontmatter": "^5.2.0",
"sharp": "^0.34.5"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/nodemailer": "^7.0.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/sharp": "^0.31.1",
"eslint": "^9",
"eslint-config-next": "16.1.3",
"tailwindcss": "^4",
"typescript": "^5"
}
}
}

View File

@@ -0,0 +1,66 @@
import { buildConfig } from 'payload';
import { postgresAdapter } from '@payloadcms/db-postgres';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import sharp from 'sharp';
import path from 'path';
import { fileURLToPath } from 'url';
import { nodemailerAdapter } from '@payloadcms/email-nodemailer';
if (process.env.NODE_ENV === 'production') {
sharp.cache(false);
}
import { Users } from './src/payload/collections/Users';
import { Media } from './src/payload/collections/Media';
import { FormSubmissions } from './src/payload/collections/FormSubmissions';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
admin: {
user: Users.slug,
importMap: {
baseDir: path.resolve(dirname),
},
meta: {
titleSuffix: ' Cable Creations',
icons: [{ rel: 'icon', type: 'image/x-icon', url: '/favicon.ico' }],
},
},
collections: [Users, Media, FormSubmissions],
editor: lexicalEditor({}),
secret: process.env.PAYLOAD_SECRET || 'fallback-secret-for-dev',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: postgresAdapter({
pool: {
connectionString:
process.env.DATABASE_URI ||
process.env.POSTGRES_URI ||
`postgresql://${process.env.PAYLOAD_DB_USER || 'payload'}:${process.env.PAYLOAD_DB_PASSWORD || 'payload'}@127.0.0.1:5432/${process.env.PAYLOAD_DB_NAME || 'payload'}`,
},
}),
email: process.env.MAIL_HOST
? nodemailerAdapter({
defaultFromAddress:
process.env.MAIL_FROM?.replace(/.*<|>.*/g, '') || 'postmaster@mg.mintel.me',
defaultFromName: process.env.MAIL_FROM?.split('<')[0]?.trim() || 'Cable Creations',
transportOptions: {
host: process.env.MAIL_HOST || 'smtp.eu.mailgun.org',
port: Number(process.env.MAIL_PORT) || 587,
...(process.env.MAIL_USERNAME
? {
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
},
}
: {}),
},
})
: undefined,
sharp,
plugins: [],
});

View File

@@ -0,0 +1,67 @@
import type { CollectionConfig } from 'payload';
export const FormSubmissions: CollectionConfig = {
slug: 'form-submissions',
admin: {
useAsTitle: 'name',
defaultColumns: ['name', 'email', 'type', 'createdAt'],
description: 'Captured leads from Contact and Product Quote forms.',
},
access: {
// Only Admins can view and delete leads via dashboard.
read: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
update: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
delete: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
// Next.js server actions handle secure inserts natively. No public client create access.
create: () => false,
},
fields: [
{
name: 'name',
type: 'text',
required: true,
admin: {
readOnly: true,
},
},
{
name: 'email',
type: 'email',
required: true,
admin: {
readOnly: true,
},
},
{
name: 'type',
type: 'select',
options: [
{ label: 'General Contact', value: 'contact' },
{ label: 'Product Quote', value: 'product_quote' },
],
required: true,
admin: {
position: 'sidebar',
readOnly: true,
},
},
{
name: 'productName',
type: 'text',
admin: {
position: 'sidebar',
readOnly: true,
condition: (data) => data.type === 'product_quote',
description: 'The specific product the user requested a quote for.',
},
},
{
name: 'message',
type: 'textarea',
required: true,
admin: {
readOnly: true,
},
},
],
};

View File

@@ -0,0 +1,47 @@
import type { CollectionConfig } from 'payload';
export const Media: CollectionConfig = {
slug: 'media',
access: {
read: () => true,
},
admin: {
useAsTitle: 'filename',
defaultColumns: ['filename', 'alt', 'updatedAt'],
},
upload: {
staticDir: 'public/media',
adminThumbnail: 'thumbnail',
imageSizes: [
{
name: 'thumbnail',
width: 600,
height: undefined,
position: 'centre',
},
{
name: 'card',
width: 768,
height: undefined,
position: 'centre',
},
{
name: 'tablet',
width: 1024,
height: undefined,
position: 'centre',
},
],
},
fields: [
{
name: 'alt',
type: 'text',
required: true,
},
{
name: 'caption',
type: 'text',
},
],
};

View File

@@ -0,0 +1,12 @@
import type { CollectionConfig } from 'payload';
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
// Email added by default
],
};

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -19,7 +23,12 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
],
"@payload-config": [
"./payload.config.ts"
]
}
},
"include": [
@@ -31,5 +40,7 @@
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
"exclude": [
"node_modules"
]
}