Skip to content

Commit 0084d95

Browse files
authored
Merge pull request #272 from OlufunbiIK/feat/feat-blog-structure-and-templates
Feat/feat blog structure and templates
2 parents f2eea59 + d2639f3 commit 0084d95

File tree

9 files changed

+918
-3
lines changed

9 files changed

+918
-3
lines changed

frontend/components/layout/Footer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// components/layout/Footer.tsx
12
const FOOTER_LINKS = [
23
{
34
title: "Product",
@@ -10,6 +11,7 @@ const FOOTER_LINKS = [
1011
{
1112
title: "Resources",
1213
links: [
14+
{ label: "Blog", href: "/blog" },
1315
{
1416
label: "Documentation",
1517
href: "https://github.com/0xVida/stellar-suite#readme",

frontend/components/layout/Navbar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const NAV_LINKS = [
55
{ label: "Features", href: "/#features" },
66
{ label: "Templates", href: "/#templates" },
77
{ label: "Compare", href: "/#compare" },
8+
{ label: "Blog", href: "/blog" },
89
{ label: "Docs", href: "https://github.com/0xVida/stellar-suite#readme" },
910
];
1011

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// src/app/blog/[slug]/page.tsx
2+
import { notFound } from "next/navigation";
3+
import Link from "next/link";
4+
import type { Metadata } from "next";
5+
import { Navbar } from "../../../../components/layout/Navbar";
6+
import { Footer } from "../../../../components/layout/Footer";
7+
import { formatDate, getAllPosts, getPostBySlug } from "@/lib/post";
8+
import { PostBody } from "@/components/PostBody";
9+
import { PostCard } from "@/components/ PostCard";
10+
11+
// Static generation: pre-render all slugs at build time
12+
export async function generateStaticParams() {
13+
return getAllPosts().map((p) => ({ slug: p.slug }));
14+
}
15+
16+
// Per-post metadata
17+
export async function generateMetadata({
18+
params,
19+
}: {
20+
params: { slug: string };
21+
}): Promise<Metadata> {
22+
const post = getPostBySlug(params.slug);
23+
if (!post) return {};
24+
return {
25+
title: `${post.title} — Stellar Suite Blog`,
26+
description: post.excerpt,
27+
openGraph: {
28+
title: post.title,
29+
description: post.excerpt,
30+
type: "article",
31+
publishedTime: post.date,
32+
},
33+
};
34+
}
35+
36+
export default function PostPage({ params }: { params: { slug: string } }) {
37+
const post = getPostBySlug(params.slug);
38+
if (!post) notFound();
39+
40+
const allPosts = getAllPosts();
41+
const currentIndex = allPosts.findIndex((p) => p.slug === post.slug);
42+
const related = allPosts
43+
.filter(
44+
(p) =>
45+
p.slug !== post.slug &&
46+
(p.category === post.category ||
47+
p.tags.some((t) => post.tags.includes(t))),
48+
)
49+
.slice(0, 2);
50+
51+
return (
52+
<div className="min-h-screen bg-background">
53+
<Navbar />
54+
55+
<main id="main-content" className="pt-28 pb-20 px-6">
56+
<div className="mx-auto max-w-3xl">
57+
{/* Breadcrumb */}
58+
<nav className="flex items-center gap-2 mb-8 text-sm font-body text-muted-foreground">
59+
<Link href="/" className="hover:text-foreground transition-colors">
60+
Home
61+
</Link>
62+
<span className="text-muted-foreground/40">/</span>
63+
<Link
64+
href="/blog"
65+
className="hover:text-foreground transition-colors"
66+
>
67+
Blog
68+
</Link>
69+
<span className="text-muted-foreground/40">/</span>
70+
<span className="text-foreground truncate max-w-[200px]">
71+
{post.title}
72+
</span>
73+
</nav>
74+
75+
{/* Category badge */}
76+
<div className="mb-5">
77+
<Link
78+
href={`/blog?category=${post.category}`}
79+
className="inline-flex items-center rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold text-primary font-display tracking-wide hover:bg-primary/20 transition-colors"
80+
>
81+
{post.category}
82+
</Link>
83+
</div>
84+
85+
{/* Title */}
86+
<h1 className="text-3xl md:text-4xl lg:text-5xl font-display font-extrabold text-foreground leading-tight tracking-tight mb-6">
87+
{post.title}
88+
</h1>
89+
90+
{/* Meta row */}
91+
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 mb-3 pb-8 border-b border-border">
92+
<div className="flex items-center gap-2">
93+
<div className="h-7 w-7 rounded-full bg-primary/20 flex items-center justify-center">
94+
<span className="text-xs font-bold text-primary font-display">
95+
{post.author.name[0]}
96+
</span>
97+
</div>
98+
<span className="text-sm font-semibold text-foreground font-body">
99+
{post.author.name}
100+
</span>
101+
</div>
102+
<span className="text-muted-foreground text-sm font-body">
103+
{formatDate(post.date)}
104+
</span>
105+
<span className="text-muted-foreground text-sm font-body">
106+
{post.readingTime}
107+
</span>
108+
</div>
109+
110+
{/* Tags */}
111+
<div className="flex flex-wrap gap-2 mb-10">
112+
{post.tags.map((tag) => (
113+
<Link
114+
key={tag}
115+
href={`/blog?tag=${tag}`}
116+
className="text-xs font-mono text-muted-foreground border border-border rounded px-2 py-1 hover:border-primary/40 hover:text-primary transition-colors"
117+
>
118+
#{tag}
119+
</Link>
120+
))}
121+
</div>
122+
123+
{/* Excerpt (lead) */}
124+
<p className="text-lg font-body text-muted-foreground leading-relaxed mb-10 pb-10 border-b border-border">
125+
{post.excerpt}
126+
</p>
127+
128+
{/* Post content */}
129+
<PostBody content={post.content} />
130+
131+
{/* Post nav: prev/next */}
132+
<div className="mt-16 pt-8 border-t border-border flex items-center justify-between gap-4">
133+
{currentIndex < allPosts.length - 1 ? (
134+
<Link
135+
href={`/blog/${allPosts[currentIndex + 1].slug}`}
136+
className="group flex items-center gap-2 text-sm font-body text-muted-foreground hover:text-foreground transition-colors"
137+
>
138+
<svg
139+
width="16"
140+
height="16"
141+
fill="none"
142+
viewBox="0 0 24 24"
143+
stroke="currentColor"
144+
strokeWidth={2}
145+
className="group-hover:-translate-x-0.5 transition-transform"
146+
>
147+
<path
148+
strokeLinecap="round"
149+
strokeLinejoin="round"
150+
d="M7 16l-4-4m0 0l4-4m-4 4h18"
151+
/>
152+
</svg>
153+
<span className="truncate max-w-[200px]">
154+
{allPosts[currentIndex + 1].title}
155+
</span>
156+
</Link>
157+
) : (
158+
<div />
159+
)}
160+
{currentIndex > 0 ? (
161+
<Link
162+
href={`/blog/${allPosts[currentIndex - 1].slug}`}
163+
className="group flex items-center gap-2 text-sm font-body text-muted-foreground hover:text-foreground transition-colors text-right"
164+
>
165+
<span className="truncate max-w-[200px]">
166+
{allPosts[currentIndex - 1].title}
167+
</span>
168+
<svg
169+
width="16"
170+
height="16"
171+
fill="none"
172+
viewBox="0 0 24 24"
173+
stroke="currentColor"
174+
strokeWidth={2}
175+
className="group-hover:translate-x-0.5 transition-transform"
176+
>
177+
<path
178+
strokeLinecap="round"
179+
strokeLinejoin="round"
180+
d="M17 8l4 4m0 0l-4 4m4-4H3"
181+
/>
182+
</svg>
183+
</Link>
184+
) : (
185+
<div />
186+
)}
187+
</div>
188+
189+
{/* Back to blog */}
190+
<div className="mt-8 text-center">
191+
<Link href="/blog" className="btn-outline text-sm">
192+
← Back to Blog
193+
</Link>
194+
</div>
195+
</div>
196+
197+
{/* Related posts */}
198+
{related.length > 0 && (
199+
<div className="mx-auto max-w-6xl mt-20 pt-12 border-t border-border">
200+
<h2 className="text-xl font-display font-bold text-foreground mb-8">
201+
Related Posts
202+
</h2>
203+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
204+
{related.map((p) => (
205+
<PostCard key={p.slug} post={p} />
206+
))}
207+
</div>
208+
</div>
209+
)}
210+
</main>
211+
212+
<Footer />
213+
</div>
214+
);
215+
}

frontend/src/app/blog/page.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// src/app/blog/page.tsx
2+
"use client";
3+
4+
import { useState, useMemo } from "react";
5+
import Link from "next/link";
6+
import { Navbar } from "../../../components/layout/Navbar";
7+
import { Footer } from "../../../components/layout/Footer";
8+
import { getAllCategories, getAllPosts, getAllTags } from "@/lib/post";
9+
import { CategoryFilter } from "@/components/CategoryFilter";
10+
import { PostCard } from "@/components/ PostCard";
11+
12+
const allPosts = getAllPosts();
13+
const allCategories = getAllCategories();
14+
const allTags = getAllTags();
15+
16+
export default function BlogIndex() {
17+
const [activeCategory, setActiveCategory] = useState("");
18+
const [activeTag, setActiveTag] = useState("");
19+
20+
const filtered = useMemo(() => {
21+
return allPosts.filter((p) => {
22+
const matchCat = activeCategory === "" || p.category === activeCategory;
23+
const matchTag = activeTag === "" || p.tags.includes(activeTag);
24+
return matchCat && matchTag;
25+
});
26+
}, [activeCategory, activeTag]);
27+
28+
const featured = filtered[0];
29+
const rest = filtered.slice(1);
30+
31+
return (
32+
<div className="min-h-screen bg-background">
33+
<Navbar />
34+
35+
{/* Hero */}
36+
<section className="pt-32 pb-14 px-6 border-b border-border">
37+
<div className="mx-auto max-w-6xl">
38+
<div className="flex items-center gap-2 mb-5">
39+
<Link
40+
href="/"
41+
className="text-sm text-muted-foreground hover:text-foreground transition-colors font-body"
42+
>
43+
Home
44+
</Link>
45+
<span className="text-muted-foreground/40">/</span>
46+
<span className="text-sm text-foreground font-body">Blog</span>
47+
</div>
48+
<h1 className="text-4xl md:text-6xl font-display font-extrabold tracking-tight text-foreground leading-tight">
49+
Blog
50+
</h1>
51+
<p className="mt-4 text-lg font-body text-muted-foreground max-w-xl">
52+
Release notes, tutorials, and updates from the Stellar Suite team.
53+
</p>
54+
55+
{/* Filters */}
56+
<div className="mt-10">
57+
<CategoryFilter
58+
categories={allCategories}
59+
tags={allTags}
60+
activeCategory={activeCategory}
61+
activeTag={activeTag}
62+
onCategoryChange={(c) => {
63+
setActiveCategory(c);
64+
setActiveTag("");
65+
}}
66+
onTagChange={(t) => {
67+
setActiveTag(t);
68+
setActiveCategory("");
69+
}}
70+
/>
71+
</div>
72+
</div>
73+
</section>
74+
75+
{/* Posts */}
76+
<main id="main-content" className="py-16 px-6">
77+
<div className="mx-auto max-w-6xl">
78+
{filtered.length === 0 ? (
79+
<div className="text-center py-24">
80+
<p className="text-muted-foreground font-body text-lg">
81+
No posts match your filter.
82+
</p>
83+
<button
84+
onClick={() => {
85+
setActiveCategory("");
86+
setActiveTag("");
87+
}}
88+
className="mt-4 text-primary underline underline-offset-4 text-sm font-body hover:opacity-80 transition-opacity"
89+
>
90+
Clear filters
91+
</button>
92+
</div>
93+
) : (
94+
<div className="flex flex-col gap-6">
95+
{/* Featured (first) post — full width */}
96+
{featured && (
97+
<div className="grid grid-cols-1 lg:grid-cols-2">
98+
<PostCard post={featured} featured />
99+
</div>
100+
)}
101+
102+
{/* Remaining posts — 2-col grid */}
103+
{rest.length > 0 && (
104+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
105+
{rest.map((post) => (
106+
<PostCard key={post.slug} post={post} />
107+
))}
108+
</div>
109+
)}
110+
</div>
111+
)}
112+
</div>
113+
</main>
114+
115+
<Footer />
116+
</div>
117+
);
118+
}

frontend/src/app/globals.css

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
1+
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap");
22

33
@tailwind base;
44
@tailwind components;
@@ -120,7 +120,12 @@
120120
@apply bg-background text-foreground font-body antialiased;
121121
}
122122

123-
h1, h2, h3, h4, h5, h6 {
123+
h1,
124+
h2,
125+
h3,
126+
h4,
127+
h5,
128+
h6 {
124129
@apply font-display;
125130
}
126131

@@ -134,7 +139,11 @@
134139
@layer utilities {
135140
.text-gradient-blue {
136141
@apply bg-clip-text text-transparent;
137-
background-image: linear-gradient(135deg, hsl(228 76% 52%), hsl(262 68% 56%));
142+
background-image: linear-gradient(
143+
135deg,
144+
hsl(228 76% 52%),
145+
hsl(262 68% 56%)
146+
);
138147
}
139148
}
140149

0 commit comments

Comments
 (0)