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
41 changes: 41 additions & 0 deletions fullstack/fullstack-blog-wendell/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
36 changes: 36 additions & 0 deletions fullstack/fullstack-blog-wendell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
17 changes: 17 additions & 0 deletions fullstack/fullstack-blog-wendell/app/api/posts/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import { posts } from "@/data/posts";

export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }, // Await params for Next.js 16
) {
const { id } = await params;
// Convert id to number if your data uses numbers for IDs
const post = posts.find((p) => p.id === id); // String === String

if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}

return NextResponse.json(post);
}
6 changes: 6 additions & 0 deletions fullstack/fullstack-blog-wendell/app/api/posts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse } from "next/server";
import { posts } from "@/data/posts"; // Direct import here!

export async function GET() {
return NextResponse.json(posts);
}
Binary file added fullstack/fullstack-blog-wendell/app/favicon.ico
Binary file not shown.
20 changes: 20 additions & 0 deletions fullstack/fullstack-blog-wendell/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@import "tailwindcss";

@theme {
/* Register your custom colors here to generate utilities */
--color-primary: #00b894;
--color-secondary: #0984e3;
--color-background: #f9fafb; /* This creates 'bg-background' */
--color-foreground: #2d3436; /* This creates 'text-foreground' */

--font-sans: "Inter", sans-serif;
}

/* You no longer need to define them in :root separately for Tailwind utilities */

@layer base {
body {
/* Now 'bg-background' is a known utility! */
@apply bg-background text-foreground antialiased;
}
}
23 changes: 23 additions & 0 deletions fullstack/fullstack-blog-wendell/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "./globals.css";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="antialiased font-sans bg-background text-foreground">
{/* Sticky Header for that premium feel */}
<Header />

{/* This renders your page.tsx or your posts/[id]/page.tsx */}
<div className="min-h-screen">{children}</div>

<Footer />
</body>
</html>
);
}
139 changes: 139 additions & 0 deletions fullstack/fullstack-blog-wendell/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client";

import { useState, useMemo, useEffect } from "react";
import { postService } from "@/services/postService";
import { BlogCard } from "@/components/blog/BlogCard";
import { SearchBar } from "@/components/blog/SearchBar";
import { Post } from "@/models/Post";
import { Hero } from "@/components/layout/Hero";

export default function HomePage() {
// --- States ---
const [posts, setPosts] = useState<Post[]>([]);
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState("All");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

// --- Data Fetching ---
useEffect(() => {
const loadPosts = async () => {
try {
setLoading(true);
const data = await postService.getAll();

// Senior Move: Validate the array content
const validData = data.filter((p) => p.id && p.title);

if (validData.length === 0 && data.length > 0) {
throw new Error("Received malformed data from the server.");
}

setPosts(data);
setError(null);
} catch (err: any) {
console.error("Failed to load posts:", err);
setError(err.message || "We couldn't retrieve the blog posts.");
} finally {
setLoading(false);
}
};
loadPosts();
}, []);

// --- Logic & Derived State ---
// Senior Tip: Derived state doesn't need its own 'useState'
const categories = useMemo(
() => ["All", ...new Set(posts.map((p) => p.category))],
[posts],
);

const filteredPosts = useMemo(() => {
return posts.filter((post) => {
// 1. Defensive Programming: Ensure 'post' and 'post.title' actually exist
if (!post || !post.title) return false;

// 2. Use Optional Chaining (?.) for extra safety
const title = post.title.toLowerCase();
const category = post.category?.toLowerCase() || "";

const matchesSearch = title.includes(search.toLowerCase().trim());
const matchesCategory =
activeCategory === "All" || post.category === activeCategory;

return matchesSearch && matchesCategory;
});
}, [search, activeCategory, posts]);

// --- Early Returns (UX States) ---
if (error) {
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4 text-center">
<h2 className="text-2xl font-bold text-foreground mb-2">
Something went wrong
</h2>
<p className="text-foreground/60 mb-6">{error}</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-2 bg-primary text-background rounded-full font-bold hover:opacity-90 transition-opacity"
>
Retry
</button>
</div>
);
}

if (loading) {
return (
<div className="p-12 text-center text-primary font-bold animate-pulse">
Loading Tech Insights...
</div>
);
}

// --- Main Render ---
return (
<main className="min-h-screen p-4 md:p-8 lg:p-12 bg-background">
{/* <header className="mb-12 max-w-4xl mx-auto text-center md:text-left">
<h1 className="text-4xl md:text-6xl font-bold tracking-tight text-foreground mb-4">
Tech Insights <span className="text-primary">.</span>
</h1>
<p className="text-lg text-foreground/70">
Exploring the future of Full Stack development, security, and
hardware.
</p>
</header> */}
<Hero />
<SearchBar
search={search}
setSearch={setSearch}
categories={categories}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
/>

{filteredPosts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredPosts.map((post) => (
<BlogCard key={post.id} {...post} />
))}
</div>
) : (
<div className="text-center py-20 bg-foreground/5 rounded-3xl">
<p className="text-xl font-medium text-foreground/40">
No matching posts found.
</p>
<button
onClick={() => {
setSearch("");
setActiveCategory("All");
}}
className="mt-4 text-primary font-bold hover:underline"
>
Clear Filters
</button>
</div>
)}
</main>
);
}
54 changes: 54 additions & 0 deletions fullstack/fullstack-blog-wendell/app/posts/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { postService } from "@/services/postService";

// Senior Tip: In Next.js 16, params MUST be awaited
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
// 1. Unbox the params Promise first
const { id } = await params;

// 2. Pass the string ID to your service
const post = await postService.getById(id);

if (!post) {
notFound(); // Triggers if the service returns undefined
}

return (
<article className="min-h-screen bg-background p-4 md:p-12 lg:p-24 max-w-4xl mx-auto">
<Link
href="/"
className="inline-flex items-center gap-2 text-sm font-bold text-primary mb-12 hover:opacity-70 transition-opacity"
>
<span>←</span> Back to Overview
</Link>

<header className="mb-12">
<span className="text-xs font-bold uppercase tracking-widest text-primary mb-2 block">
{post.category}
</span>
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-foreground leading-tight">
{post.title}
</h1>
<div className="flex items-center gap-4 mt-6 text-foreground/40 text-sm">
<time>{post.date}</time>
<span>•</span>
<span>5 min read</span>
</div>
</header>

<section className="prose prose-lg max-w-none text-foreground/80 leading-relaxed border-t border-foreground/5 pt-12">
{/* Render content safely */}
<p className="mb-6 first-letter:text-5xl first-letter:font-bold first-letter:text-primary first-letter:mr-3 first-letter:float-left">
{post.content}
</p>
</section>

{/* ... rest of your CTA footer */}
</article>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Link from "next/link";

interface BlogCardProps {
id: string;
title: string;
category: string;
excerpt: string;
date: string;
}

export const BlogCard = ({
id,
title,
category,
excerpt,
date,
}: BlogCardProps) => (
// Changed: bg-surface -> bg-foreground/5 for a darker card background
// Changed: hover:shadow-xl -> hover:shadow-2xl for a more dramatic lift
// Added: hover:bg-foreground/10 for a subtle interaction change
<article className="group bg-foreground/5 rounded-2xl border border-foreground/10 overflow-hidden hover:shadow-2xl hover:border-primary/30 hover:bg-foreground/10 transition-all duration-300 flex flex-col">
<div className="p-6 flex-grow">
<span className="text-xs font-bold uppercase tracking-widest text-primary mb-2 block">
{category}
</span>
<h2 className="text-xl font-bold mb-3 group-hover:text-primary transition-colors">
{title}
</h2>
<p className="text-foreground/70 text-sm line-clamp-3 mb-4">{excerpt}</p>
</div>
<div className="p-6 pt-0 mt-auto border-t border-foreground/10 flex items-center justify-between">
<time className="text-xs text-foreground/50">{date}</time>
<Link
href={`/posts/${id}`}
className="text-sm font-bold text-secondary hover:underline flex items-center gap-1"
>
Read More <span>→</span>
</Link>
</div>
</article>
);
Loading