added subscription functionality

This commit is contained in:
2025-04-07 18:25:19 -05:00
parent abeb8cbd26
commit 3b10fad560
9 changed files with 909 additions and 223 deletions
+58 -62
View File
@@ -1,74 +1,70 @@
import React from 'react';
import Link from 'next/link';
import SubscribeForm from './SubscribeForm'; // Import the reusable SubscribeForm
const Footer: React.FC = () => {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-primary-800 text-white mt-16">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 className="text-xl font-bold mb-4 text-white">Confessions of Grace</h3>
<p className="text-primary-300">
A blog dedicated to exploring the doctrines of grace and Reformed theology.
</p>
</div>
<div>
<h3 className="text-xl font-bold mb-4 text-white">Navigation</h3>
<ul className="space-y-2">
<li>
<Link href="/" className="text-primary-300 hover:text-white">
Home
</Link>
</li>
<li>
<Link href="/about" className="text-primary-300 hover:text-white">
About
</Link>
</li>
<li>
<Link href="/archive" className="text-primary-300 hover:text-white">
Archive
</Link>
</li>
<li>
<Link href="/confession" className="text-primary-300 hover:text-white">
1689 Confession
</Link>
</li>
<li>
<Link href="/resources" className="text-primary-300 hover:text-white">
Resources
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-xl font-bold mb-4 text-white">Subscribe</h3>
<p className="text-primary-300 mb-4">
Stay updated with the latest posts.
</p>
<div className="flex">
<input
type="email"
placeholder="Your email"
className="px-4 py-2 w-full rounded-l-md bg-primary-700 border border-primary-600 text-white placeholder-primary-400"
/>
<button className="bg-accent hover:bg-accent-dark px-4 py-2 rounded-r-md">
Subscribe
</button>
<footer className="bg-primary-800 text-white mt-16">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 className="text-xl font-bold mb-4 text-white">Confessions of Grace</h3>
<p className="text-primary-300">
A blog dedicated to exploring the doctrines of grace and Reformed theology.
</p>
</div>
<div>
<h3 className="text-xl font-bold mb-4 text-white">Navigation</h3>
<ul className="space-y-2">
<li>
<Link href="/" className="text-primary-300 hover:text-white">
Home
</Link>
</li>
<li>
<Link href="/about" className="text-primary-300 hover:text-white">
About
</Link>
</li>
<li>
<Link href="/archive" className="text-primary-300 hover:text-white">
Archive
</Link>
</li>
<li>
<Link href="/confession" className="text-primary-300 hover:text-white">
1689 Confession
</Link>
</li>
<li>
<Link href="/resources" className="text-primary-300 hover:text-white">
Resources
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-xl font-bold mb-4 text-white">Subscribe</h3>
<p className="text-primary-300 mb-4">
Stay updated with the latest posts.
</p>
<SubscribeForm
placeholder="Your email"
buttonLabel="Subscribe"
className="flex flex-col space-y-2"
/> {/* Use the common SubscribeForm */}
</div>
</div>
<div className="border-t border-primary-700 mt-8 pt-8 text-center text-primary-400">
<p>&copy; {currentYear} Confessions of Grace. All rights reserved.</p>
<p className="mt-2 text-sm">
"For by grace you have been saved through faith. And this is not your own doing; it is the gift of God." Ephesians 2:8
</p>
</div>
</div>
<div className="border-t border-primary-700 mt-8 pt-8 text-center text-primary-400">
<p>&copy; {currentYear} Confessions of Grace. All rights reserved.</p>
<p className="mt-2 text-sm">
"For by grace you have been saved through faith. And this is not your own doing; it is the gift of God." Ephesians 2:8
</p>
</div>
</div>
</footer>
</footer>
);
};
+2 -2
View File
@@ -17,7 +17,7 @@ const Layout: React.FC<LayoutProps> = ({
description = 'A blog dedicated to exploring the doctrines of grace and Reformed theology.'
}) => {
return (
<html lang="en">
<>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
@@ -33,7 +33,7 @@ const Layout: React.FC<LayoutProps> = ({
</div>
<Analytics />
<SpeedInsights/>
</html>
</>
);
};
+32 -74
View File
@@ -1,6 +1,6 @@
import React from 'react';
import Link from 'next/link';
import TagList from './TagList';
import SubscribeForm from './SubscribeForm'; // Import the reusable SubscribeForm
interface SidebarProps {
recentPosts: Array<{
@@ -16,80 +16,38 @@ interface SidebarProps {
const Sidebar: React.FC<SidebarProps> = ({ recentPosts, tags = [] }) => {
return (
<aside className="space-y-8">
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">About</h2>
<p className="text-primary-700 mb-4">
Confessions of Grace explores the riches of Reformed theology and the doctrines of grace.
</p>
<Link
href="/about"
className="text-accent-dark hover:text-accent inline-flex items-center"
>
Learn more
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 ml-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
<aside className="space-y-8">
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">About</h2>
<p className="text-primary-700 mb-4">
Confessions of Grace explores the riches of Reformed theology and the doctrines of grace.
</p>
<Link
href="/about"
className="text-accent-dark hover:text-accent inline-flex items-center"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</Link>
</div>
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">Recent Posts</h2>
<ul className="space-y-3">
{recentPosts.map(post => (
<li key={post.id}>
<Link
href={`/posts/${post.id}`}
className="hover:text-accent-dark block"
>
<h3 className="font-medium">{post.title}</h3>
<p className="text-sm text-primary-500">
{new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</p>
</Link>
</li>
))}
</ul>
</div>
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">Popular Tags</h2>
<TagList limit={15} tags={tags} />
</div>
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">Subscribe</h2>
<p className="text-primary-700 mb-4">
Stay updated with the latest posts and resources.
</p>
<form className="space-y-3">
<div>
<input
type="email"
placeholder="Your email"
className="w-full px-3 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
required
/>
</div>
<button
type="submit"
className="button w-full"
>
Subscribe
</button>
</form>
</div>
</aside>
Learn more
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 ml-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</Link>
</div>
{/* Subscribe section */}
<div className="bg-white rounded-md shadow-sm p-6 border border-primary-200">
<h2 className="text-xl font-bold mb-4 border-b border-primary-200 pb-2">Subscribe</h2>
<p className="text-primary-700 mb-4">
Stay updated with the latest posts and resources.
</p>
<SubscribeForm /> {/* Use the common SubscribeForm */}
</div>
</aside>
);
};
+75
View File
@@ -0,0 +1,75 @@
import React, { useState } from 'react';
interface SubscribeFormProps {
placeholder?: string;
buttonLabel?: string;
className?: string;
}
const SubscribeForm: React.FC<SubscribeFormProps> = ({
placeholder = 'Your email',
buttonLabel = 'Subscribe',
className = '',
}) => {
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);
const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
if (!email) {
setMessage('Please enter your email.');
return;
}
setLoading(true);
setMessage('');
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
if (response.ok) {
setMessage('You have successfully subscribed!');
setEmail('');
} else {
const errorData = await response.json();
setMessage(`Error: ${errorData.message}`);
}
} catch (error) {
setMessage('Something went wrong. Please try again.');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubscribe} className={`space-y-3 ${className}`}>
<div>
<input
type="email"
placeholder={placeholder}
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 border border-primary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
</div>
<button
type="submit"
className="button w-full"
disabled={loading}
>
{loading ? 'Subscribing...' : buttonLabel}
</button>
{message && <p className="text-sm text-primary-700">{message}</p>}
</form>
);
};
export default SubscribeForm;
@@ -0,0 +1,27 @@
import { MongoClient } from 'mongodb';
const uri = process.env.MONGODB_URI || ''; // Load URI from environment variables
const options = {};
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
if (!process.env.MONGODB_URI) {
throw new Error('Please add your MongoDB URI to .env.local');
}
// Reuse the client instance for serverless deployments
if (process.env.NODE_ENV === 'development') {
// Create a global instance to preserve the same client during HMR in development
if (!(global as typeof globalThis & { _mongoClientPromise?: Promise<MongoClient> })._mongoClientPromise) {
client = new MongoClient(uri, options);
(global as typeof globalThis & { _mongoClientPromise?: Promise<MongoClient> })._mongoClientPromise = client.connect();
}
clientPromise = (global as typeof globalThis & { _mongoClientPromise?: Promise<MongoClient> })._mongoClientPromise!;
} else {
// In production, create a new client connection
client = new MongoClient(uri, options);
clientPromise = client.connect();
}
export default clientPromise;
+59
View File
@@ -0,0 +1,59 @@
import { MongoClient } from 'mongodb';
import { NextApiRequest, NextApiResponse } from 'next';
// MongoDB connection URI and database constants
const MONGODB_URI = process.env.MONGODB_URI as string; // Add this in your .env.local file
const MONGODB_DB = process.env.MONGODB_DB as string; // Add this in your .env.local file
// Create a MongoClient instance
let cachedClient: MongoClient | null = null;
async function connectToDatabase() {
if (!cachedClient) {
cachedClient = new MongoClient(MONGODB_URI);
await cachedClient.connect();
}
return cachedClient.db(MONGODB_DB);
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Only allow POST requests
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
}
const { email } = req.body;
// Check if email was provided
if (!email) {
return res.status(400).json({ message: 'Email is required.' });
}
// Validate email format
const emailRegex = /\S+@\S+\.\S+/;
if (!emailRegex.test(email)) {
return res.status(400).json({ message: 'Invalid email address.' });
}
try {
// Connect to the database
const db = await connectToDatabase();
// Check if the email already exists
const existingEmail = await db.collection('subscriptions').findOne({ email });
if (existingEmail) {
return res.status(400).json({ message: 'Email is already subscribed.' });
}
// Save email to the database
await db.collection('subscriptions').insertOne({ email, subscribedAt: new Date() });
// Respond with success
return res.status(200).json({ message: 'Successfully subscribed!' });
} catch (error) {
// Log the error and respond with a server error status
console.error('Failed to save subscription:', error);
return res.status(500).json({ message: 'An unexpected error occurred.' });
}
}