From bac215299692625f1319510de45460b7b66b68d3 Mon Sep 17 00:00:00 2001 From: m shehzen Date: Sat, 5 Jul 2025 12:21:17 +0530 Subject: [PATCH 1/4] feat: Add Twitter engagement metrics component and integrate with PostPage --- next.config.js | 8 + src/app/components/TwitterMetrics.js | 418 +++++++++++++++++++++++++++ src/app/post/[id]/page.js | 13 + 3 files changed, 439 insertions(+) create mode 100644 src/app/components/TwitterMetrics.js diff --git a/next.config.js b/next.config.js index 5aa9db2..3d65cf1 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,14 @@ const nextConfig = { async rewrites() { return [ + { + source: '/api/tweet-metrics', + destination: 'https://twitter-api.opensourceprojects.dev/tweet-metrics', + }, + { + source: '/api/multiple-tweet-metrics', + destination: 'https://twitter-api.opensourceprojects.dev/multiple-tweet-metrics', + }, { source: '/api/:path*', destination: 'https://twitter-api.opensourceprojects.dev/:path*', diff --git a/src/app/components/TwitterMetrics.js b/src/app/components/TwitterMetrics.js new file mode 100644 index 0000000..2bd9c63 --- /dev/null +++ b/src/app/components/TwitterMetrics.js @@ -0,0 +1,418 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +const TwitterMetrics = ({ postId, tweetUrl }) => { + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchMetrics = async () => { + if (!tweetUrl) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + // Use the backend API to fetch Twitter metrics + const response = await fetch('/api/tweet-metrics', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url: tweetUrl }), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch metrics: ${response.status}`); + } + + const data = await response.json(); + setMetrics(data); + } catch (err) { + console.warn('Failed to fetch Twitter metrics:', err.message); + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchMetrics(); + }, [tweetUrl]); + + // Don't render anything if there's no tweet URL + if (!tweetUrl) { + return null; + } + + if (loading) { + return ( +
+
+ + X Engagement +
+
+
+ Loading metrics... +
+
+ ); + } + + if (error || !metrics) { + return ( +
+
+ + X Engagement +
+
+ + Metrics unavailable +
+
+ ); + } + + const formatNumber = (num) => { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } + if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } + return num.toString(); + }; + + return ( + <> +
+
+ + X Engagement + + + +
+ +
+
+
+ +
+
+ {formatNumber(metrics.likes)} + Likes +
+
+ +
+
+ +
+
+ {formatNumber(metrics.retweets)} + Retweets +
+
+ +
+
+ +
+
+ {formatNumber(metrics.shares)} + Quotes +
+
+ +
+
+ +
+
+ {formatNumber(metrics.views)} + Views +
+
+
+
+ + + + ); +}; + +export default TwitterMetrics; \ No newline at end of file diff --git a/src/app/post/[id]/page.js b/src/app/post/[id]/page.js index f7e4e5c..96e0dda 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 TwitterMetrics from '../../components/TwitterMetrics'; const fallbackImage = '/images/open-source-logo-830x460.jpg'; @@ -26,6 +27,12 @@ const extractTags = (content) => { return hashtags.map(tag => tag.substring(1)); // Remove the # symbol }; +const getTwitterUrl = (postId, username = 'GithubProjects') => { + // Construct Twitter URL from post ID + // Since the post data comes from @githubprojects tweets, we can construct the URL + return `https://x.com/${username}/status/${postId}`; +}; + const formatDate = (dateString) => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', @@ -732,6 +739,12 @@ export default function PostPage() {
Created
+ + {/* Twitter Engagement Metrics */} + From 1e9c9b2e4d0d17ad8722cdb5c57afcb08423944b Mon Sep 17 00:00:00 2001 From: m shehzen Date: Sun, 6 Jul 2025 10:00:50 +0530 Subject: [PATCH 2/4] refactor: Remove TwitterMetrics component and related functionality from PostPage --- next.config.js | 8 - src/app/components/TwitterMetrics.js | 418 --------------------------- src/app/post/[id]/page.js | 13 - 3 files changed, 439 deletions(-) delete mode 100644 src/app/components/TwitterMetrics.js diff --git a/next.config.js b/next.config.js index 3d65cf1..5aa9db2 100644 --- a/next.config.js +++ b/next.config.js @@ -2,14 +2,6 @@ const nextConfig = { async rewrites() { return [ - { - source: '/api/tweet-metrics', - destination: 'https://twitter-api.opensourceprojects.dev/tweet-metrics', - }, - { - source: '/api/multiple-tweet-metrics', - destination: 'https://twitter-api.opensourceprojects.dev/multiple-tweet-metrics', - }, { source: '/api/:path*', destination: 'https://twitter-api.opensourceprojects.dev/:path*', diff --git a/src/app/components/TwitterMetrics.js b/src/app/components/TwitterMetrics.js deleted file mode 100644 index 2bd9c63..0000000 --- a/src/app/components/TwitterMetrics.js +++ /dev/null @@ -1,418 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; - -const TwitterMetrics = ({ postId, tweetUrl }) => { - const [metrics, setMetrics] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchMetrics = async () => { - if (!tweetUrl) { - setLoading(false); - return; - } - - try { - setLoading(true); - setError(null); - - // Use the backend API to fetch Twitter metrics - const response = await fetch('/api/tweet-metrics', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ url: tweetUrl }), - }); - - if (!response.ok) { - throw new Error(`Failed to fetch metrics: ${response.status}`); - } - - const data = await response.json(); - setMetrics(data); - } catch (err) { - console.warn('Failed to fetch Twitter metrics:', err.message); - setError(err.message); - } finally { - setLoading(false); - } - }; - - fetchMetrics(); - }, [tweetUrl]); - - // Don't render anything if there's no tweet URL - if (!tweetUrl) { - return null; - } - - if (loading) { - return ( -
-
- - X Engagement -
-
-
- Loading metrics... -
-
- ); - } - - if (error || !metrics) { - return ( -
-
- - X Engagement -
-
- - Metrics unavailable -
-
- ); - } - - const formatNumber = (num) => { - if (num >= 1000000) { - return (num / 1000000).toFixed(1) + 'M'; - } - if (num >= 1000) { - return (num / 1000).toFixed(1) + 'K'; - } - return num.toString(); - }; - - return ( - <> -
-
- - X Engagement - - - -
- -
-
-
- -
-
- {formatNumber(metrics.likes)} - Likes -
-
- -
-
- -
-
- {formatNumber(metrics.retweets)} - Retweets -
-
- -
-
- -
-
- {formatNumber(metrics.shares)} - Quotes -
-
- -
-
- -
-
- {formatNumber(metrics.views)} - Views -
-
-
-
- - - - ); -}; - -export default TwitterMetrics; \ No newline at end of file diff --git a/src/app/post/[id]/page.js b/src/app/post/[id]/page.js index 96e0dda..f7e4e5c 100644 --- a/src/app/post/[id]/page.js +++ b/src/app/post/[id]/page.js @@ -3,7 +3,6 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; -import TwitterMetrics from '../../components/TwitterMetrics'; const fallbackImage = '/images/open-source-logo-830x460.jpg'; @@ -27,12 +26,6 @@ const extractTags = (content) => { return hashtags.map(tag => tag.substring(1)); // Remove the # symbol }; -const getTwitterUrl = (postId, username = 'GithubProjects') => { - // Construct Twitter URL from post ID - // Since the post data comes from @githubprojects tweets, we can construct the URL - return `https://x.com/${username}/status/${postId}`; -}; - const formatDate = (dateString) => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', @@ -739,12 +732,6 @@ export default function PostPage() {
Created
- - {/* Twitter Engagement Metrics */} - From 3bc6bc2db3be8603b70fa1006b5c8a18e08d7aae Mon Sep 17 00:00:00 2001 From: m shehzen Date: Fri, 11 Jul 2025 21:42:22 +0530 Subject: [PATCH 3/4] feat: Add newsletter subscription functionality with dynamic form and API integration --- src/app/api/subscribe/route.js | 33 ++++++++++ src/app/components/NewsletterForm.js | 92 ++++++++++++++++++++++++++++ src/app/newsletter/page.js | 54 ++++++++++++++++ src/app/page.js | 22 +++++++ src/app/page.module.css | 92 ++++++++++++++++++++++++++++ src/app/post/[id]/page.js | 17 ++++- 6 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 src/app/api/subscribe/route.js create mode 100644 src/app/components/NewsletterForm.js create mode 100644 src/app/newsletter/page.js 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() {