Skip to content

Commit b2acaec

Browse files
committed
feat: implement newsletter subscription with confirmation flow and UI components
1 parent 1f28b39 commit b2acaec

22 files changed

Lines changed: 1755 additions & 33 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Hashnode Newsletter Configuration
2+
NEXT_PUBLIC_HASHNODE_PUBLICATION_ID=your_publication_id_here

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
.content-collections
77
.source
88

9+
# include environment variables
10+
.env
11+
12+
913
# test & build
1014
/coverage
1115
/.next/

app/(home)/page.tsx

Lines changed: 177 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,192 @@
1-
import { Button } from "@/components/ui/button";
1+
import { Button } from "../../components/ui/button";
2+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/ui/card";
3+
import Image from "next/image";
24
import Link from "next/link";
5+
import { NewsletterForm } from "../../components/NewsletterForm";
6+
7+
const projects = [
8+
{
9+
title: "Cloud Infrastructure",
10+
description: "Automated cloud infrastructure deployment using Terraform",
11+
image: "/projects/cloud-infra.jpg",
12+
link: "/projects/cloud-infrastructure"
13+
},
14+
{
15+
title: "CI/CD Pipeline",
16+
description: "Enterprise-grade CI/CD pipeline with GitHub Actions",
17+
image: "/projects/cicd.jpg",
18+
link: "/projects/cicd-pipeline"
19+
},
20+
{
21+
title: "Kubernetes Cluster",
22+
description: "Production-ready Kubernetes cluster setup",
23+
image: "/projects/kubernetes.jpg",
24+
link: "/projects/kubernetes-cluster"
25+
},
26+
{
27+
title: "Monitoring Stack",
28+
description: "Complete monitoring solution with Prometheus & Grafana",
29+
image: "/projects/monitoring.jpg",
30+
link: "/projects/monitoring-stack"
31+
},
32+
{
33+
title: "Security Framework",
34+
description: "DevSecOps implementation for cloud-native apps",
35+
image: "/projects/security.jpg",
36+
link: "/projects/security-framework"
37+
},
38+
{
39+
title: "Microservices",
40+
description: "Scalable microservices architecture deployment",
41+
image: "/projects/microservices.jpg",
42+
link: "/projects/microservices"
43+
}
44+
];
45+
46+
const features = [
47+
{ title: "Cloud & DevOps", description: "Master cloud platforms and DevOps practices" },
48+
{ title: "SRE", description: "Learn Site Reliability Engineering principles" },
49+
{ title: "Platform Engineering", description: "Build and maintain scalable platforms" }
50+
];
351

452
export default function HomePage() {
553
return (
6-
<main className="flex flex-1 flex-col justify-center text-center">
7-
<div className="h-[50rem] w-full dark:bg-black bg-white dark:bg-grid-white/[0.2] bg-grid-black/[0.2] relative flex items-center justify-center">
8-
{/* Radial gradient for the container to give a faded look */}
54+
<main className="flex flex-1 flex-col">
55+
{/* Hero Section */}
56+
<div className="min-h-[80vh] w-full dark:bg-black bg-gradient-to-b from-gray-50 to-white dark:from-neutral-950 dark:to-black relative flex items-center justify-center">
957
<div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]"></div>
10-
11-
{/* Container for title and description */}
12-
<div className="relative z-20 flex flex-col items-center">
13-
{/* Title */}
14-
<p className="text-4xl sm:text-7xl font-bold bg-clip-text text-transparent bg-gradient-to-b from-neutral-200 to-neutral-500 py-4">
58+
59+
<div className="relative z-20 flex flex-col items-center max-w-6xl mx-auto px-4 py-20">
60+
<h1 className="text-5xl sm:text-7xl font-bold bg-clip-text text-transparent bg-gradient-to-b from-gray-900 to-gray-600 dark:from-neutral-200 dark:to-neutral-500 py-4 text-center mb-6">
1561
Learn Cloud & DevOps
16-
</p>
17-
{/* Description */}
18-
<p className="text-lg sm:text-2xl font-semibold bg-clip-text text-transparent bg-gradient-to-b from-neutral-200 to-neutral-500">
62+
</h1>
63+
<p className="text-xl sm:text-2xl font-semibold bg-clip-text text-transparent bg-gradient-to-b from-gray-800 to-gray-600 dark:from-neutral-200 dark:to-neutral-500 text-center max-w-3xl mb-12">
1964
DevOps is the union of people, <strong>process</strong>, and{" "}
2065
<strong>products</strong> to enable continuous delivery of value to
2166
your <strong>end users</strong>.
2267
</p>
23-
<Link href="/docs">
24-
<Button className="m-5 from-neutral-200 to-neutral-500">
25-
Get Started
26-
</Button>
27-
</Link>
68+
69+
{/* Feature Cards */}
70+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full mb-12">
71+
{features.map((feature, index) => (
72+
<Card key={index} className="bg-white/50 dark:bg-neutral-900/50 border border-gray-200 dark:border-neutral-800 backdrop-blur-sm">
73+
<CardHeader>
74+
<CardTitle className="text-xl text-gray-900 dark:text-neutral-200">{feature.title}</CardTitle>
75+
<CardDescription className="text-gray-600 dark:text-neutral-400">{feature.description}</CardDescription>
76+
</CardHeader>
77+
</Card>
78+
))}
79+
</div>
80+
81+
{/* CNCF Section */}
82+
<div className="w-full mb-12">
83+
<Card className="bg-white/50 dark:bg-neutral-900/50 border border-gray-200 dark:border-neutral-800 backdrop-blur-sm">
84+
<CardContent className="flex flex-col items-center py-8">
85+
<div className="relative w-64 h-24 mb-6">
86+
<Image
87+
src="/Assets/cncf.png"
88+
alt="CNCF Logo"
89+
fill
90+
className="object-contain"
91+
/>
92+
</div>
93+
<p className="text-gray-600 dark:text-neutral-400 text-center max-w-2xl mb-6">
94+
The Cloud Native Computing Foundation (CNCF) serves as the vendor-neutral home for many of the fastest-growing open source projects, including Kubernetes, Prometheus, and Envoy.
95+
</p>
96+
<Link href="https://www.cncf.io/" target="_blank" rel="noopener noreferrer">
97+
<Button className="bg-gradient-to-r from-[#446ca9] to-[#2a4b8c] text-white hover:opacity-90 transition-opacity">
98+
Visit CNCF Website
99+
</Button>
100+
</Link>
101+
</CardContent>
102+
</Card>
103+
</div>
104+
105+
{/* CNCF Landscape Section */}
106+
<div className="w-full mb-12">
107+
<Card className="bg-white/50 dark:bg-neutral-900/50 border border-gray-200 dark:border-neutral-800 backdrop-blur-sm">
108+
<CardHeader className="text-center">
109+
<CardTitle className="text-2xl text-gray-900 dark:text-neutral-200">
110+
Explore Coud Native Computing Foundation (CNCF) Landscape
111+
</CardTitle>
112+
<CardDescription className="text-gray-600 dark:text-neutral-400">
113+
Discover the Cloud Native Computing Foundation ecosystem
114+
</CardDescription>
115+
</CardHeader>
116+
<CardContent>
117+
<Link href="https://landscape.cncf.io/" target="_blank" rel="noopener noreferrer">
118+
<Button className="w-full bg-gradient-to-r from-blue-600 to-blue-800 text-white hover:opacity-90 transition-opacity">
119+
View CNCF Landscape
120+
</Button>
121+
</Link>
122+
</CardContent>
123+
</Card>
124+
</div>
125+
126+
<div className="flex gap-4 mt-8">
127+
<Link href="/docs">
128+
<Button className="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-neutral-200 dark:to-neutral-500 text-white dark:text-black hover:opacity-90 transition-opacity">
129+
Get Started
130+
</Button>
131+
</Link>
132+
<Link href="/blogs">
133+
<Button variant="outline" className="border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-neutral-200 hover:bg-gray-100 dark:hover:bg-neutral-900">
134+
Read Blog
135+
</Button>
136+
</Link>
137+
</div>
28138
</div>
29139
</div>
140+
141+
{/* Project Section */}
142+
<section className="py-20 px-4 bg-gray-50 dark:bg-neutral-950">
143+
<div className="max-w-6xl mx-auto">
144+
<h2 className="text-4xl font-bold text-center mb-4 bg-clip-text text-transparent bg-gradient-to-b from-gray-900 to-gray-600 dark:from-neutral-200 dark:to-neutral-500">
145+
Explore Cloud / DevOps / DevSecOps Projects
146+
</h2>
147+
<p className="text-gray-600 dark:text-neutral-400 text-center mb-12 max-w-2xl mx-auto text-lg">
148+
Discover our collection of hands-on projects that will help you master cloud computing and DevOps practices.
149+
</p>
150+
151+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
152+
{projects.map((project, index) => (
153+
<Link href={project.link} key={index}>
154+
<Card className="h-full bg-white dark:bg-neutral-900 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-all duration-300 transform hover:-translate-y-2 cursor-pointer border border-gray-200 dark:border-neutral-800 overflow-hidden">
155+
<div className="relative h-48 w-full">
156+
<Image
157+
src={project.image}
158+
alt={project.title}
159+
fill
160+
className="object-cover"
161+
/>
162+
</div>
163+
<CardHeader>
164+
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-neutral-200">
165+
{project.title}
166+
</CardTitle>
167+
<CardDescription className="text-gray-600 dark:text-neutral-400">
168+
{project.description}
169+
</CardDescription>
170+
</CardHeader>
171+
</Card>
172+
</Link>
173+
))}
174+
</div>
175+
</div>
176+
</section>
177+
178+
{/* Newsletter Section */}
179+
<section className="py-20 px-4 bg-white dark:bg-neutral-900/50 backdrop-blur-sm">
180+
<div className="max-w-4xl mx-auto text-center">
181+
<h2 className="text-3xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-b from-gray-900 to-gray-600 dark:from-neutral-200 dark:to-neutral-500">
182+
Subscribe to my newsletter
183+
</h2>
184+
<p className="text-gray-600 dark:text-neutral-400 mb-8">
185+
Get the latest updates about cloud computing and DevOps directly in your inbox.
186+
</p>
187+
<NewsletterForm />
188+
</div>
189+
</section>
30190
</main>
31191
);
32192
}

app/actions/newsletter.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use server'
2+
3+
export async function subscribeToNewsletter(formData: FormData): Promise<{ success: boolean; error?: string }> {
4+
const email = formData.get('email')
5+
const publicationId = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_ID
6+
7+
if (!email || !publicationId) {
8+
return { success: false, error: 'Email and publication ID are required' }
9+
}
10+
11+
try {
12+
const response = await fetch('https://gql.hashnode.com', {
13+
method: 'POST',
14+
headers: {
15+
'Content-Type': 'application/json',
16+
},
17+
body: JSON.stringify({
18+
query: `
19+
mutation SubscribeToNewsletter($input: SubscribeToNewsletterInput!) {
20+
subscribeToNewsletter(input: $input) {
21+
status
22+
}
23+
}
24+
`,
25+
variables: {
26+
input: {
27+
publicationId,
28+
email: email.toString(),
29+
},
30+
},
31+
}),
32+
})
33+
34+
const data = await response.json()
35+
36+
if (data.errors) {
37+
return { success: false, error: data.errors[0].message }
38+
}
39+
40+
return { success: true }
41+
} catch (error) {
42+
console.error('Newsletter subscription error:', error)
43+
return {
44+
success: false,
45+
error: error instanceof Error ? error.message : 'Failed to subscribe to newsletter'
46+
}
47+
}
48+
}

app/api/blogs/route.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { NextResponse } from 'next/server';
2+
3+
const HASHNODE_API = 'https://gql.hashnode.com/';
4+
5+
const HASHNODE_QUERY = `
6+
query GetUserArticles($username: String!, $page: Int!) {
7+
user(username: $username) {
8+
posts(page: $page, pageSize: 6) {
9+
pageInfo {
10+
hasNextPage
11+
}
12+
edges {
13+
node {
14+
title
15+
brief
16+
slug
17+
publishedAt
18+
coverImage {
19+
url
20+
}
21+
tags {
22+
name
23+
}
24+
}
25+
}
26+
}
27+
}
28+
}
29+
`;
30+
31+
export async function GET(request: Request) {
32+
try {
33+
const { searchParams } = new URL(request.url);
34+
const page = parseInt(searchParams.get('page') || '1');
35+
36+
console.log('Fetching blogs for page:', page);
37+
38+
const response = await fetch(HASHNODE_API, {
39+
method: 'POST',
40+
headers: {
41+
'Content-Type': 'application/json',
42+
},
43+
body: JSON.stringify({
44+
query: HASHNODE_QUERY,
45+
variables: {
46+
username: 'surajkumar00',
47+
page,
48+
},
49+
}),
50+
});
51+
52+
const data = await response.json();
53+
console.log('Hashnode API response:', JSON.stringify(data, null, 2));
54+
55+
if (data.errors) {
56+
console.error('Hashnode API errors:', data.errors);
57+
return NextResponse.json({ error: data.errors[0].message }, { status: 400 });
58+
}
59+
60+
return NextResponse.json(data);
61+
} catch (error) {
62+
console.error('Failed to fetch blogs:', error);
63+
return NextResponse.json({ error: 'Failed to fetch blogs' }, { status: 500 });
64+
}
65+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export async function GET(request: Request) {
4+
try {
5+
const { searchParams } = new URL(request.url);
6+
const token = searchParams.get('token');
7+
8+
if (!token) {
9+
return NextResponse.json(
10+
{ error: 'Token is required' },
11+
{ status: 400 }
12+
);
13+
}
14+
15+
// Verify token and update subscriber status in database
16+
// For now, we'll just simulate it
17+
const subscriber = {
18+
// This would be fetched from your database
19+
confirmed: true,
20+
confirmedAt: new Date(),
21+
};
22+
23+
// Redirect to a success page
24+
return NextResponse.redirect(new URL('/newsletter/success', request.url));
25+
} catch (error) {
26+
console.error('Newsletter confirmation error:', error);
27+
return NextResponse.json(
28+
{ error: 'Failed to confirm subscription' },
29+
{ status: 500 }
30+
);
31+
}
32+
}

0 commit comments

Comments
 (0)