diff --git a/src/app/api/subscribe/route.js b/src/app/api/subscribe/route.js new file mode 100644 index 0000000..2770a3a --- /dev/null +++ b/src/app/api/subscribe/route.js @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server'; + +export async function POST(request) { + const { email } = await request.json(); + + if (!email) { + return NextResponse.json({ error: 'Email is required' }, { status: 400 }); + } + + try { + const response = await fetch('https://api.brevo.com/v3/contacts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': process.env.BREVO_API_KEY, + }, + body: JSON.stringify({ + email, + listIds: [2], // Replace with your Brevo list ID + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + return NextResponse.json({ error: errorData.message || 'Failed to subscribe' }, { status: response.status }); + } + + const data = await response.json(); + return NextResponse.json(data, { status: 201 }); + } catch (error) { + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/src/app/components/NewsletterForm.js b/src/app/components/NewsletterForm.js new file mode 100644 index 0000000..c5b0f6b --- /dev/null +++ b/src/app/components/NewsletterForm.js @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; +import styles from '../page.module.css'; + +/** + * NewsletterForm Component + * A reusable newsletter subscription form. + * Props: + * - onSubmit: function(email) => void | Promise + * - placeholder: string (optional) + * - buttonText: string (optional) + * - successMessage: string (optional) + * - errorMessage: string (optional) + */ +export default function NewsletterForm({ + onSubmit, + placeholder = 'Enter your email', + buttonText = 'Subscribe', + successMessage = 'Thank you for subscribing!', + errorMessage = 'Please enter a valid email address.' +}) { + const [email, setEmail] = useState(''); + const [status, setStatus] = useState('idle'); // idle | loading | success | error + const [error, setError] = useState(''); + + const validateEmail = (email) => { + return /\S+@\S+\.\S+/.test(email); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + if (!validateEmail(email)) { + setError(errorMessage); + setStatus('error'); + return; + } + setStatus('loading'); + try { + const response = await fetch('/api/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Subscription failed.'); + } + + setStatus('success'); + setEmail(''); + } catch (err) { + setError(err.message || 'Subscription failed.'); + setStatus('error'); + } + }; + + return ( +
+

Subscribe to our Newsletter

+

+ Get the latest updates on new projects and hidden gems, delivered straight to your inbox. +

+
+ setEmail(e.target.value)} + placeholder={placeholder} + required + disabled={status === 'loading' || status === 'success'} + className={styles['newsletter-input']} + /> + +
+ {status === 'success' && ( +
{successMessage}
+ )} + {status === 'error' && error && ( +
{error}
+ )} +
+ ); +} diff --git a/src/app/newsletter/page.js b/src/app/newsletter/page.js new file mode 100644 index 0000000..8a5d88b --- /dev/null +++ b/src/app/newsletter/page.js @@ -0,0 +1,54 @@ +'use client'; + +import Link from 'next/link'; +import NewsletterForm from '../components/NewsletterForm'; +import styles from '../page.module.css'; +import '../globals.css'; + +export default function NewsletterPage() { + return ( + <> +
+ +
+
+
+
+

+ Stay Updated +

+

+ Subscribe to our newsletter to get the latest updates on new projects and hidden gems. +

+
+ +
+
+ + ); +} diff --git a/src/app/page.js b/src/app/page.js index a869be7..90886d5 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,6 +1,9 @@ 'use client'; import { useState, useEffect, Suspense } from 'react'; +import dynamic from 'next/dynamic'; +// Dynamically import NewsletterForm to avoid SSR issues with useState +const NewsletterForm = dynamic(() => import('./components/NewsletterForm'), { ssr: false }); import Link from 'next/link'; import { useSearchParams, useRouter } from 'next/navigation'; @@ -269,6 +272,10 @@ function HomePageContent() { Home + + + Newsletter + Featured @@ -305,6 +312,21 @@ function HomePageContent() { Open Source + {/* Newsletter Form Section */} +
+
+ { + // TODO: Replace with your newsletter API integration + await new Promise((resolve) => setTimeout(resolve, 1000)); + // throw new Error('Demo error'); // Uncomment to test error state + }} + placeholder="Your email address" + buttonText="Join Newsletter" + successMessage="You're subscribed! 🎉" + /> +
+
diff --git a/src/app/page.module.css b/src/app/page.module.css index a11c8f3..1864718 100644 --- a/src/app/page.module.css +++ b/src/app/page.module.css @@ -166,3 +166,95 @@ a.secondary { filter: invert(); } } + +.newsletter-form { + width: 100%; + max-width: 500px; /* Increased max-width */ + margin: 40px auto; /* Centered with more margin */ + text-align: center; /* Center align the content */ +} + +.newsletter-title { + font-size: 1.5rem; /* Larger font size */ + font-weight: 600; + color: var(--text-primary); + margin-bottom: 1rem; +} + +.newsletter-description { + color: var(--text-secondary); + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.newsletter-input-wrapper { + display: flex; + background: var(--bg-secondary); + border-radius: 8px; /* Rounded corners */ + border: 1px solid var(--border); + padding: 6px; /* Padding around the input and button */ + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.newsletter-input-wrapper:focus-within { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3); /* Glow effect */ +} + +.newsletter-input { + flex-grow: 1; + padding: 12px 15px; /* Increased padding */ + border: none; + background: transparent; /* Transparent background */ + color: var(--text-primary); + font-size: 1rem; + outline: none; /* Remove default outline */ +} + +.newsletter-input::placeholder { + color: var(--text-muted); +} + +.newsletter-button { + padding: 10px 20px; /* Adjusted padding */ + border: none; + background-color: var(--primary); + color: white; + border-radius: 6px; /* Slightly rounded corners */ + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +.newsletter-button:hover { + background-color: var(--primary-dark); + transform: translateY(-1px); +} + +.newsletter-button:disabled { + background-color: var(--bg-tertiary); + color: var(--text-muted); + cursor: not-allowed; + transform: none; +} + +.newsletter-message { + margin-top: 15px; + padding: 12px; + border-radius: 6px; + text-align: center; + font-weight: 500; +} + +.newsletter-message.success { + background-color: rgba(16, 185, 129, 0.1); /* Secondary color with alpha */ + color: var(--secondary); + border: 1px solid var(--secondary); +} + +.newsletter-message.error { + background-color: rgba(239, 68, 68, 0.1); /* Red color with alpha */ + color: #ef4444; + border: 1px solid #ef4444; +} \ No newline at end of file diff --git a/src/app/post/[id]/page.js b/src/app/post/[id]/page.js index f7e4e5c..c2c1578 100644 --- a/src/app/post/[id]/page.js +++ b/src/app/post/[id]/page.js @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; +import NewsletterForm from '../../components/NewsletterForm'; const fallbackImage = '/images/open-source-logo-830x460.jpg'; @@ -569,6 +570,10 @@ export default function PostPage() { Home + + + Newsletter + Featured @@ -699,8 +704,10 @@ export default function PostPage() {
))}
-
+
+ +

@@ -784,6 +791,14 @@ export default function PostPage() {