Archived
added subscription functionality
This commit is contained in:
+58
-62
@@ -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>© {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>© {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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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.' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user