Skip to content

Commit fc23d5f

Browse files
feat: implement contact and newsletter subscription forms with validation and API integration
1 parent 5c84faa commit fc23d5f

File tree

10 files changed

+485
-55
lines changed

10 files changed

+485
-55
lines changed

src/app/(about)/about/page.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import siteMetadata from "@/src/utils/siteMetaData"
66
export const metadata = {
77
title: "About Princeps Polycap",
88
description:
9-
"Origin story, mission, and ecosystem map for Princeps Polycapthe systems architect building Poly186.",
9+
"Origin story, mission, and ecosystem map for Princeps Polycap, the systems architect building Poly186.",
1010
}
1111

1212
export default function About() {
@@ -106,7 +106,7 @@ export default function About() {
106106
<p>
107107
Working overnight shifts on the Medtronic factory floor in Minnesota,
108108
I watched brilliant people spend entire careers on repetitive tasks
109-
that could be automated. That experience sparked Polya business OS
109+
that could be automated. That experience sparked Poly, a business OS
110110
powered by AI digital workers so organizations can redeploy humans to
111111
creative, strategic work.
112112
</p>
@@ -145,7 +145,7 @@ export default function About() {
145145

146146
<h2>Building in public</h2>
147147
<p>
148-
This site is the canonical log of how everything unfoldsthe wins, the
148+
This site is the canonical log of how everything unfolds: the wins, the
149149
prototypes that fail, the meta-commentary on using AI to build AI. If
150150
you&apos;re building something audacious, I hope this candid record
151151
gives you both tactical insight and moral permission to keep going.

src/app/(about)/contact/page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import siteMetadata from "@/src/utils/siteMetaData"
44

55
export const metadata = {
66
title: "Contact Princeps Polycap",
7-
description: `Reach out to collaborate on Poly, SESAP, or Automating Basic Needs—or email ${siteMetadata.email}.`,
7+
description: `Reach out to collaborate on Poly, SESAP, or Automating Basic Needs. Email ${siteMetadata.email} or schedule a call.`,
88
}
99

1010
export default function Contact() {

src/app/api/contact/route.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { NextResponse } from 'next/server';
2+
import connectDB from '@/src/lib/mongodb';
3+
import Contact from '@/src/models/Contact';
4+
5+
export async function POST(request) {
6+
try {
7+
await connectDB();
8+
9+
const body = await request.json();
10+
const { name, email, phone, projectDetails } = body;
11+
12+
if (!name || !email || !projectDetails) {
13+
return NextResponse.json(
14+
{ error: 'Name, email, and project details are required' },
15+
{ status: 400 }
16+
);
17+
}
18+
19+
const ipAddress = request.headers.get('x-forwarded-for') ||
20+
request.headers.get('x-real-ip') ||
21+
'unknown';
22+
const userAgent = request.headers.get('user-agent') || 'unknown';
23+
24+
const contact = await Contact.create({
25+
name,
26+
email,
27+
phone: phone || '',
28+
projectDetails,
29+
ipAddress,
30+
userAgent,
31+
source: 'website-contact-form',
32+
status: 'new'
33+
});
34+
35+
console.log(`New contact submission from ${email}`);
36+
37+
return NextResponse.json(
38+
{
39+
success: true,
40+
message: 'Your message has been received. I\'ll get back to you soon!',
41+
contactId: contact._id
42+
},
43+
{ status: 201 }
44+
);
45+
46+
} catch (error) {
47+
console.error('Contact form submission error:', error);
48+
49+
if (error.name === 'ValidationError') {
50+
return NextResponse.json(
51+
{ error: 'Invalid data provided', details: error.message },
52+
{ status: 400 }
53+
);
54+
}
55+
56+
return NextResponse.json(
57+
{ error: 'Failed to submit contact form. Please try again later.' },
58+
{ status: 500 }
59+
);
60+
}
61+
}
62+
63+
export async function GET() {
64+
return NextResponse.json(
65+
{ message: 'Contact API endpoint. Use POST to submit contact form.' },
66+
{ status: 200 }
67+
);
68+
}

src/app/api/newsletter/route.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { NextResponse } from 'next/server';
2+
import connectDB from '@/src/lib/mongodb';
3+
import Newsletter from '@/src/models/Newsletter';
4+
5+
export async function POST(request) {
6+
try {
7+
await connectDB();
8+
9+
const body = await request.json();
10+
const { email, source = 'website-footer' } = body;
11+
12+
if (!email) {
13+
return NextResponse.json(
14+
{ error: 'Email is required' },
15+
{ status: 400 }
16+
);
17+
}
18+
19+
const emailRegex = /^\S+@\S+\.\S+$/;
20+
if (!emailRegex.test(email)) {
21+
return NextResponse.json(
22+
{ error: 'Please provide a valid email address' },
23+
{ status: 400 }
24+
);
25+
}
26+
27+
const ipAddress = request.headers.get('x-forwarded-for') ||
28+
request.headers.get('x-real-ip') ||
29+
'unknown';
30+
const userAgent = request.headers.get('user-agent') || 'unknown';
31+
32+
const existingSubscriber = await Newsletter.findOne({ email: email.toLowerCase() });
33+
34+
if (existingSubscriber) {
35+
if (existingSubscriber.subscribed) {
36+
return NextResponse.json(
37+
{
38+
success: true,
39+
message: 'You\'re already subscribed to the field notes!',
40+
alreadySubscribed: true
41+
},
42+
{ status: 200 }
43+
);
44+
} else {
45+
existingSubscriber.subscribed = true;
46+
existingSubscriber.unsubscribedAt = null;
47+
existingSubscriber.source = source;
48+
await existingSubscriber.save();
49+
50+
return NextResponse.json(
51+
{
52+
success: true,
53+
message: 'Welcome back! Your subscription has been reactivated.',
54+
resubscribed: true
55+
},
56+
{ status: 200 }
57+
);
58+
}
59+
}
60+
61+
const subscriber = await Newsletter.create({
62+
email: email.toLowerCase(),
63+
source,
64+
ipAddress,
65+
userAgent,
66+
subscribed: true,
67+
verified: false
68+
});
69+
70+
console.log(`New newsletter subscriber: ${email}`);
71+
72+
return NextResponse.json(
73+
{
74+
success: true,
75+
message: 'Subscribed! Watch for field notes from the Poly186 build.',
76+
subscriberId: subscriber._id
77+
},
78+
{ status: 201 }
79+
);
80+
81+
} catch (error) {
82+
console.error('Newsletter subscription error:', error);
83+
84+
if (error.name === 'ValidationError') {
85+
return NextResponse.json(
86+
{ error: 'Invalid email address', details: error.message },
87+
{ status: 400 }
88+
);
89+
}
90+
91+
if (error.code === 11000) {
92+
return NextResponse.json(
93+
{ error: 'This email is already subscribed' },
94+
{ status: 409 }
95+
);
96+
}
97+
98+
return NextResponse.json(
99+
{ error: 'Failed to subscribe. Please try again later.' },
100+
{ status: 500 }
101+
);
102+
}
103+
}
104+
105+
export async function GET() {
106+
return NextResponse.json(
107+
{ message: 'Newsletter API endpoint. Use POST to subscribe.' },
108+
{ status: 200 }
109+
);
110+
}

src/components/About/AboutCoverSection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const AboutCoverSection = () => {
1818
From factory floor shifts to Poly186
1919
</h2>
2020
<p className='font-medium mt-4 text-base leading-relaxed'>
21-
Princeps Polycap is a self-taught Kenyan programmer documenting the real-time build-out of Polythe AI operating system that funds Poly186, SESAP, Automating Basic Needs, and the Terraforming Sahara moonshot. Every experiment, success, and failure gets shared openly so other founders can see how post-scarcity infrastructure is made.
21+
Princeps Polycap is a self-taught Kenyan programmer documenting the real-time build-out of Poly, the AI operating system that funds Poly186, SESAP, Automating Basic Needs, and the Terraforming Sahara moonshot. Every experiment, success, and failure gets shared openly so other founders can see how post-scarcity infrastructure is made.
2222
</p>
2323
</div>
2424
</section>

0 commit comments

Comments
 (0)