Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/app/api/subscribe/route.js
Original file line number Diff line number Diff line change
@@ -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 });
}
}
92 changes: 92 additions & 0 deletions src/app/components/NewsletterForm.js
Original file line number Diff line number Diff line change
@@ -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<void>
* - 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 (
<form onSubmit={handleSubmit} className={styles['newsletter-form']}>
<h3 className={styles['newsletter-title']}>Subscribe to our Newsletter</h3>
<p className={styles['newsletter-description']}>
Get the latest updates on new projects and hidden gems, delivered straight to your inbox.
</p>
<div className={styles['newsletter-input-wrapper']}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder={placeholder}
required
disabled={status === 'loading' || status === 'success'}
className={styles['newsletter-input']}
/>
<button
type="submit"
disabled={status === 'loading' || status === 'success'}
className={styles['newsletter-button']}
>
{status === 'loading' ? 'Submitting...' : buttonText}
</button>
</div>
{status === 'success' && (
<div className={`${styles['newsletter-message']} ${styles.success}`}>{successMessage}</div>
)}
{status === 'error' && error && (
<div className={`${styles['newsletter-message']} ${styles.error}`}>{error}</div>
)}
</form>
);
}
54 changes: 54 additions & 0 deletions src/app/newsletter/page.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<header className="header">
<nav className="nav">
<div className="nav-brand">
<Link href="/" className="brand-link">
<i className="fab fa-github brand-icon"></i>
<span className="brand-text">Open-source Projects</span>
</Link>
</div>
<div className="nav-links">
<Link href="/" className="nav-link">
<i className="fas fa-home"></i>
<span>Home</span>
</Link>
<Link href="/newsletter" className="nav-link">
<i className="fas fa-envelope"></i>
<span>Newsletter</span>
</Link>
<Link href="#" className="nav-link">
<i className="fas fa-star"></i>
<span>Featured</span>
</Link>
<Link href="#" className="nav-link">
<i className="fas fa-gem"></i>
<span>Hidden Gems</span>
</Link>
</div>
</nav>
</header>
<div className={styles.page}>
<main className={styles.main}>
<div style={{ textAlign: 'center' }}>
<h1 className="hero-title">
<span className="gradient-text">Stay Updated</span>
</h1>
<p className="hero-description">
Subscribe to our newsletter to get the latest updates on new projects and hidden gems.
</p>
</div>
<NewsletterForm />
</main>
</div>
</>
);
}
22 changes: 22 additions & 0 deletions src/app/page.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -269,6 +272,10 @@ function HomePageContent() {
<i className="fas fa-home"></i>
<span>Home</span>
</Link>
<Link href="/newsletter" className="nav-link">
<i className="fas fa-envelope"></i>
<span>Newsletter</span>
</Link>
<Link href="#" className="nav-link">
<i className="fas fa-star"></i>
<span>Featured</span>
Expand Down Expand Up @@ -305,6 +312,21 @@ function HomePageContent() {
<span className="stat-label">Open Source</span>
</div>
</div>
{/* Newsletter Form Section */}
<section className="newsletter-section" style={{ display: 'flex', justifyContent: 'center', margin: '2rem 0' }}>
<div style={{ maxWidth: 400, width: '100%' }}>
<NewsletterForm
onSubmit={async (email) => {
// 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! 🎉"
/>
</div>
</section>
</div>
<div className="hero-visual">
<div className="code-window">
Expand Down
92 changes: 92 additions & 0 deletions src/app/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
17 changes: 16 additions & 1 deletion src/app/post/[id]/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -569,6 +570,10 @@ export default function PostPage() {
<i className="fas fa-home"></i>
<span>Home</span>
</Link>
<Link href="/newsletter" className="nav-link">
<i className="fas fa-envelope"></i>
<span>Newsletter</span>
</Link>
<Link href="#" className="nav-link">
<i className="fas fa-star"></i>
<span>Featured</span>
Expand Down Expand Up @@ -699,8 +704,10 @@ export default function PostPage() {
</div>
))}
</div>

<div className="article-footer">
<div className="newsletter-section-container">
<NewsletterForm />
</div>
<div className="contributors">
<h3>
<i className="fas fa-users"></i>
Expand Down Expand Up @@ -784,6 +791,14 @@ export default function PostPage() {
</footer>

<style jsx global>{`
.newsletter-section-container {
margin-top: 4rem;
padding: 3rem;
background: var(--bg-secondary);
border-radius: 1rem;
border: 1px solid var(--border);
}

/* Hero Image Styles */
.hero-image-container {
position: relative;
Expand Down
Empty file.