Skip to content

Commit 0569509

Browse files
committed
feat(navigation): implement blog post navigation
1 parent 82fc7c8 commit 0569509

File tree

6 files changed

+169
-16
lines changed

6 files changed

+169
-16
lines changed

__posts/2025-01-22-rinha-de-backend-2025-copy-2.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ image: "/assets/blog/posts/placeholder.svg?height=400&width=800"
1010
featured: false
1111
---
1212

13-
# Lessons from Rinha de Backend 2025: LLMs as Power Tools and the Art of Problem Solving
14-
1513
The third edition of Rinha de Backend presented another fascinating opportunity to dive deep into backend architecture and performance optimization. This time around, I chose Rust as my weapon of choice, partly inspired by recent conversations about the language and partly driven by curiosity about how modern AI tools could assist in the development process.
1614

1715
## Why Rust?

__posts/2025-01-22-rinha-de-backend-2025-copy.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ image: "/assets/blog/posts/placeholder.svg?height=400&width=800"
1010
featured: true
1111
---
1212

13-
# Lessons from Rinha de Backend 2025: LLMs as Power Tools and the Art of Problem Solving
14-
1513
The third edition of Rinha de Backend presented another fascinating opportunity to dive deep into backend architecture and performance optimization. This time around, I chose Rust as my weapon of choice, partly inspired by recent conversations about the language and partly driven by curiosity about how modern AI tools could assist in the development process.
1614

1715
## Why Rust?
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2025 Josimar Silva
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import { render, screen } from "@testing-library/react";
26+
27+
import { BlogPostNavigation } from "./blog-post-navigation";
28+
29+
describe("BlogPostNavigation", () => {
30+
it("should render both next and previous links", () => {
31+
const previousPost = { slug: "prev-slug", title: "Previous Post" };
32+
const nextPost = { slug: "next-slug", title: "Next Post" };
33+
render(
34+
<BlogPostNavigation previousPost={previousPost} nextPost={nextPost} />,
35+
);
36+
expect(screen.getByText("Previous")).toBeInTheDocument();
37+
expect(screen.getByText("Next")).toBeInTheDocument();
38+
});
39+
40+
it("should render only the next link", () => {
41+
const nextPost = { slug: "next-slug", title: "Next Post" };
42+
render(<BlogPostNavigation previousPost={null} nextPost={nextPost} />);
43+
expect(screen.queryByText("Previous")).toBeInTheDocument();
44+
expect(screen.getByText("Next")).toBeInTheDocument();
45+
});
46+
47+
it("should render only the previous link", () => {
48+
const previousPost = { slug: "prev-slug", title: "Previous Post" };
49+
render(<BlogPostNavigation previousPost={previousPost} nextPost={null} />);
50+
expect(screen.getByText("Next")).toBeInTheDocument();
51+
expect(screen.queryByText("Next")).toBeInTheDocument();
52+
});
53+
54+
it("should render disabled buttons when no links are provided", () => {
55+
render(<BlogPostNavigation previousPost={null} nextPost={null} />);
56+
expect(screen.getByText("Previous")).toBeInTheDocument();
57+
expect(screen.getByText("Next")).toBeInTheDocument();
58+
expect(screen.getByText("Previous").closest("button")).toBeDisabled();
59+
expect(screen.getByText("Next").closest("button")).toBeDisabled();
60+
});
61+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2025 Josimar Silva
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import { ChevronLeft, ChevronRight } from "lucide-react";
26+
import Link from "next/link";
27+
28+
import { Button } from "@/app/__components/ui/button";
29+
30+
interface PostNavigationProps {
31+
slug: string;
32+
title: string;
33+
}
34+
35+
interface BlogPostNavigationProps {
36+
previousPost?: PostNavigationProps | null;
37+
nextPost?: PostNavigationProps | null;
38+
}
39+
40+
export function BlogPostNavigation({
41+
previousPost,
42+
nextPost,
43+
}: BlogPostNavigationProps) {
44+
return (
45+
<div className="flex items-center justify-between">
46+
<div className="flex items-center gap-2">
47+
{previousPost ? (
48+
<Button variant="outline" asChild>
49+
<Link href={`/blog/${previousPost.slug}`}>
50+
<ChevronLeft className="h-4 w-4 mr-2" />
51+
Previous
52+
</Link>
53+
</Button>
54+
) : (
55+
<Button variant="outline" disabled>
56+
<ChevronLeft className="h-4 w-4 mr-2" />
57+
Previous
58+
</Button>
59+
)}
60+
</div>
61+
62+
<div className="flex items-center gap-2">
63+
{nextPost ? (
64+
<Button variant="outline" asChild>
65+
<Link href={`/blog/${nextPost.slug}`}>
66+
Next
67+
<ChevronRight className="h-4 w-4 ml-2" />
68+
</Link>
69+
</Button>
70+
) : (
71+
<Button variant="outline" disabled>
72+
Next
73+
<ChevronRight className="h-4 w-4 ml-2" />
74+
</Button>
75+
)}
76+
</div>
77+
</div>
78+
);
79+
}

src/app/__components/blog/blog-post.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ import Image from "next/image";
2727
import Link from "next/link";
2828

2929
import BlogPostContent from "@/app/__components/blog/blog-post-content";
30+
import { BlogPostNavigation } from "@/app/__components/blog/blog-post-navigation";
3031
import { Badge } from "@/app/__components/ui/badge";
3132
import { Button } from "@/app/__components/ui/button";
3233

34+
interface PostNavigationProps {
35+
slug: string;
36+
title: string;
37+
}
38+
3339
interface BlogPostProps {
3440
post: {
3541
title: string;
@@ -40,6 +46,8 @@ interface BlogPostProps {
4046
author: string;
4147
image: string;
4248
tags: string[];
49+
previousPost?: PostNavigationProps | null;
50+
nextPost?: PostNavigationProps | null;
4351
};
4452
}
4553

@@ -59,17 +67,6 @@ export function BlogPost({ post }: BlogPostProps) {
5967

6068
{/* Header */}
6169
<header className="mb-8">
62-
{/* Tags at the top */}
63-
<div className="flex flex-wrap gap-2 mb-4">
64-
<Badge>{post.category}</Badge>
65-
{post.tags &&
66-
post.tags.map((tag) => (
67-
<Badge key={tag} variant="secondary">
68-
{tag}
69-
</Badge>
70-
))}
71-
</div>
72-
7370
<h1 className="text-4xl font-bold tracking-tight lg:text-5xl mb-4">
7471
{post.title}
7572
</h1>
@@ -119,8 +116,26 @@ export function BlogPost({ post }: BlogPostProps) {
119116
/>
120117
</div>
121118

119+
{/* Tags */}
120+
<div className="flex flex-wrap gap-2 mb-4">
121+
<Badge>{post.category}</Badge>
122+
{post.tags &&
123+
post.tags.map((tag) => (
124+
<Badge key={tag} variant="secondary">
125+
{tag}
126+
</Badge>
127+
))}
128+
</div>
122129
{/* Content */}
123130
<BlogPostContent content={post.content} />
131+
132+
{/* Navigation */}
133+
<div className="mt-12">
134+
<BlogPostNavigation
135+
previousPost={post.previousPost}
136+
nextPost={post.nextPost}
137+
/>
138+
</div>
124139
</div>
125140
</article>
126141
);

src/app/blog/[slug]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export default async function BlogPostPage({
4343
"content",
4444
"tags",
4545
"category",
46+
"previousPost",
47+
"nextPost",
4648
]);
4749

4850
if (!post) {
@@ -52,7 +54,7 @@ export default async function BlogPostPage({
5254
return (
5355
<div className="min-h-screen bg-background">
5456
<Header />
55-
<BlogPost post={{ ...post, content: post.content }} />
57+
<BlogPost post={post} />
5658
<Footer />
5759
</div>
5860
);

0 commit comments

Comments
 (0)