Skip to content

Commit 9e3705a

Browse files
committed
chore: worked on the blog page
Signed-off-by: Daniel Ntege <danientege785@gmail.com>
1 parent 0da9ad8 commit 9e3705a

File tree

62 files changed

+3781
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3781
-12
lines changed

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { Metadata } from "next";
2+
import { getAllPosts, getPostBySlug, FALLBACK_IMAGE } from "@/lib/posts";
3+
import { format } from "date-fns";
4+
import Link from "next/link";
5+
import ShareButtons from "@/components/ShareButtons";
6+
import { notFound } from "next/navigation";
7+
8+
export async function generateStaticParams() {
9+
const posts = getAllPosts();
10+
return posts.map((p) => ({ slug: p.slug }));
11+
}
12+
13+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
14+
const { slug } = await params;
15+
const post = await getPostBySlug(slug);
16+
if (!post) return { title: "Post Not Found" };
17+
return { title: post.title, description: post.abstract };
18+
}
19+
20+
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
21+
const { slug } = await params;
22+
const post = await getPostBySlug(slug);
23+
if (!post) notFound();
24+
25+
const allPosts = getAllPosts();
26+
const recentPosts = allPosts.filter((p) => p.slug !== slug).slice(0, 4);
27+
28+
return (
29+
<div className="mx-auto flex">
30+
<article className="w-full break-words">
31+
{/* Hero */}
32+
<div id="hero" className="bg-gradient-to-br from-red-dark via-red to-red relative">
33+
<div className="container pt-14 pb-12 sm:py-[100px] text-white">
34+
<h1 className="font-medium text-xl sm:text-2xl leading-none relative mb-6 sm:mb-5">{post.title}</h1>
35+
<div className="flex flex-wrap gap-6">
36+
{post.authors.map((author, i) => (
37+
<AuthorBlock key={i} author={author} date={post.date} duration={post.duration} />
38+
))}
39+
</div>
40+
</div>
41+
</div>
42+
43+
{/* Content */}
44+
<div className="container py-14 sm:py-[80px] lg:py-[90px]">
45+
<main className="w-full min-w-0 max-w-[800px] mx-auto">
46+
<div
47+
className="content text-sm text-charcoal font-normal sm:text-base"
48+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
49+
/>
50+
<div className="mt-11 mx-auto w-fit">
51+
<ShareButtons />
52+
</div>
53+
</main>
54+
</div>
55+
56+
{/* Recent Posts */}
57+
{recentPosts.length > 0 && (
58+
<div className="w-full bg-gray-light">
59+
<div className="container py-[60px] sm:py-[110px] text-black">
60+
<h2 className="text-[24px] leading-none font-medium">Recent Hiero Posts</h2>
61+
<ul className="mt-6 grid grid-cols-1 xl:grid-cols-4 gap-[38px] list-none p-0">
62+
{recentPosts.map((rp) => (
63+
<li key={rp.slug}>
64+
<Link href={`/blog/${rp.slug}`} className="no-underline grid grid-cols-1 sm:grid-cols-2 sm:gap-9 xl:gap-0 xl:grid-cols-1">
65+
{/* eslint-disable-next-line @next/next/no-img-element */}
66+
<img src={rp.featuredImage} alt={rp.title} className="w-full h-[140px] object-cover" loading="lazy" />
67+
<div>
68+
<h3 className="mt-3 sm:mt-0 xl:mt-3 text-[20px] font-medium text-black line-clamp-1">{rp.title}</h3>
69+
<p className="text-charcoal text-sm font-normal mt-1 leading-none">
70+
{rp.duration}{rp.duration && <span className="mx-1"></span>}
71+
{format(new Date(rp.date), "MMMM d, yyyy")}
72+
</p>
73+
{rp.abstract && (
74+
<p className="text-charcoal text-sm sm:text-base font-normal line-clamp-4 xl:line-clamp-2 mt-2">
75+
{rp.abstract.length > 400 ? rp.abstract.slice(0, 400) + "…" : rp.abstract}
76+
</p>
77+
)}
78+
</div>
79+
</Link>
80+
</li>
81+
))}
82+
</ul>
83+
</div>
84+
</div>
85+
)}
86+
</article>
87+
</div>
88+
);
89+
}
90+
91+
function AuthorBlock({ author, date, duration }: { author: { name?: string; title?: string; organization?: string; link?: string; image?: string }; date: string; duration?: string }) {
92+
const inner = (
93+
<>
94+
{author.image && (
95+
// eslint-disable-next-line @next/next/no-img-element
96+
<img src={author.image} alt={author.name ?? ""} className="inline-block h-[72px] w-[72px] rounded-full bg-white" loading="lazy" />
97+
)}
98+
<div className="font-normal">
99+
<p className="m-0">
100+
{duration}{duration && <span className="mx-1"></span>}
101+
{format(new Date(date), "MMMM d, yyyy")}
102+
</p>
103+
<p className="m-0">by {author.name}</p>
104+
{(author.title || author.organization) && (
105+
<p className="m-0">
106+
{author.title}{author.title && author.organization ? ", " : " "}{author.organization}
107+
</p>
108+
)}
109+
</div>
110+
</>
111+
);
112+
113+
if (author.link) {
114+
return (
115+
<a href={author.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center text-sand text-sm gap-x-4 no-underline" title={author.name}>
116+
{inner}
117+
</a>
118+
);
119+
}
120+
121+
return (
122+
<span className="inline-flex items-center text-sand text-sm gap-x-4">
123+
{inner}
124+
</span>
125+
);
126+
}

nextjs/app/blog/page.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
import type { Metadata } from "next";
2+
import { getAllPosts, getBlogIndexMeta } from "@/lib/posts";
3+
import BlogPostList from "@/components/BlogPostList";
24

35
export const metadata: Metadata = {
46
title: "Blog",
5-
description: "Latest updates, announcements, and community stories from the Hiero ecosystem.",
7+
description: "Stay up to date with our latest news and announcements.",
68
};
79

810
export default function BlogPage() {
11+
const posts = getAllPosts();
12+
const blogMeta = getBlogIndexMeta();
13+
914
return (
10-
<div id="hero" className="bg-linear-to-br from-red-dark via-red to-red relative">
11-
<div className="container py-14 sm:py-25 xl:py-36 text-white text-center">
12-
<h1 className="text-[42px] sm:text-5xl leading-none relative mb-2.5">Hiero Blog</h1>
13-
<p className="text-[24px] tracking-[-0.081rem] sm:text-xl relative">
14-
Stay up to date with our latest news and announcements.
15-
</p>
15+
<>
16+
<div id="hero" className="bg-gradient-to-br from-red-dark via-red to-red relative">
17+
<div className="container py-14 sm:py-[100px] xl:py-36 text-white text-center">
18+
<h1 className="text-[42px] sm:text-5xl leading-none relative mb-2.5">{blogMeta.title}</h1>
19+
<p className="text-[24px] tracking-[-0.081rem] sm:text-xl relative">{blogMeta.subtitle}</p>
20+
</div>
1621
</div>
17-
</div>
22+
<BlogPostList posts={posts} listTitle={blogMeta.listTitle} />
23+
</>
1824
);
1925
}

nextjs/app/globals.css

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
@import "tailwindcss";
22

3-
/* ============================================
4-
Tailwind v4 Theme — ported from Hugo tailwind.config.js
5-
============================================ */
63
@theme {
74
/* Colors */
85
--color-transparent: transparent;
@@ -398,7 +395,7 @@ code {
398395
}
399396

400397
.pagination .page-item .page-link {
401-
@apply bg-sand flex justify-center items-center rounded-full leading-none no-underline text-base font-bold text-charcoal;
398+
@apply bg-sand flex justify-center items-center rounded-full leading-none no-underline text-base font-bold text-charcoal cursor-pointer;
402399
width: 2.5rem;
403400
height: 2.5rem;
404401
}

nextjs/app/hacktoberfest/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Metadata } from "next";
2+
import { getSimplePage } from "@/lib/posts";
3+
import IssueList from "@/components/IssueList";
4+
5+
export const metadata: Metadata = {
6+
title: "Hacktoberfest 2024",
7+
description: "Contribute to Hiero by working on a good first issue",
8+
};
9+
10+
export default async function HacktoberfestPage() {
11+
const page = await getSimplePage("content/hacktoberfest/index.md");
12+
const title = page?.title ?? "Hacktoberfest 2024";
13+
const description = page?.description ?? "Contribute to Hiero by working on a good first issue";
14+
const contentHtml = page?.contentHtml ?? "";
15+
16+
return (
17+
<>
18+
<div id="hero" className="bg-gradient-to-br from-red-dark via-red to-red relative">
19+
<div className="container py-14 sm:py-[100px] xl:py-36 text-white text-center">
20+
<h1 className="text-[42px] sm:text-5xl leading-none relative mb-2.5">{title}</h1>
21+
<p className="text-[24px] tracking-[-0.081rem] sm:text-xl relative">{description}</p>
22+
</div>
23+
</div>
24+
<div className="container py-14 sm:py-[80px] lg:py-[90px]">
25+
<main className="w-full min-w-0 max-w-[800px] mx-auto">
26+
<div
27+
className="content text-sm text-charcoal font-normal sm:text-base"
28+
dangerouslySetInnerHTML={{ __html: contentHtml }}
29+
/>
30+
<IssueList endpoint="https://hedera-issues.koyeb.app/api/hacktoberfest-issues" />
31+
</main>
32+
</div>
33+
</>
34+
);
35+
}

nextjs/app/heroes/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Metadata } from "next";
2+
import { getSimplePage } from "@/lib/posts";
3+
import ContributorsGrid from "@/components/ContributorsGrid";
4+
5+
export const metadata: Metadata = {
6+
title: "Hiero Heroes",
7+
description: "Meet the amazing contributors who have helped build Hiero",
8+
};
9+
10+
export default async function HeroesPage() {
11+
const page = await getSimplePage("content/heroes/index.md");
12+
const title = page?.title ?? "Hiero Heroes";
13+
const description = page?.description ?? "Meet the amazing contributors who have helped build Hiero";
14+
const contentHtml = page?.contentHtml ?? "";
15+
16+
return (
17+
<>
18+
<div id="hero" className="bg-gradient-to-br from-red-dark via-red to-red relative">
19+
<div className="container py-14 sm:py-[100px] xl:py-36 text-white text-center">
20+
<h1 className="text-[42px] sm:text-5xl leading-none relative mb-2.5">{title}</h1>
21+
<p className="text-[24px] tracking-[-0.081rem] sm:text-xl relative">{description}</p>
22+
</div>
23+
</div>
24+
<div className="container py-14 sm:py-[80px] lg:py-[90px]">
25+
<main className="w-full min-w-0 max-w-[800px] mx-auto">
26+
<div
27+
className="content text-sm text-charcoal font-normal sm:text-base"
28+
dangerouslySetInnerHTML={{ __html: contentHtml }}
29+
/>
30+
<ContributorsGrid endpoint="https://hedera-issues.koyeb.app/api/v2/contributors" />
31+
</main>
32+
</div>
33+
</>
34+
);
35+
}

nextjs/components/BlogPostList.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import Link from "next/link";
5+
import { format } from "date-fns";
6+
import type { PostMeta } from "@/lib/posts";
7+
8+
const POSTS_PER_PAGE = 3;
9+
const PAGER_SIZE = 5;
10+
11+
export default function BlogPostList({ posts, listTitle }: { posts: PostMeta[]; listTitle: string }) {
12+
const [page, setPage] = useState(1);
13+
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
14+
const pagePosts = posts.slice((page - 1) * POSTS_PER_PAGE, page * POSTS_PER_PAGE);
15+
16+
// Hugo v0.133+ windowed pagination: show at most PAGER_SIZE page numbers
17+
const half = Math.floor(PAGER_SIZE / 2);
18+
const rawStart = Math.max(1, page - half);
19+
const windowEnd = Math.min(totalPages, rawStart + PAGER_SIZE - 1);
20+
const windowStart = Math.max(1, windowEnd - PAGER_SIZE + 1);
21+
const visiblePages = Array.from({ length: windowEnd - windowStart + 1 }, (_, i) => windowStart + i);
22+
23+
function goTo(p: number) {
24+
setPage(p);
25+
window.scrollTo({ top: 0, behavior: "smooth" });
26+
}
27+
28+
return (
29+
<div id="posts" className="anchor">
30+
<div className="bg-white">
31+
<div className="container py-14 sm:py-[80px]">
32+
<div className="max-w-[800px] mx-auto">
33+
<h2 className="text-2xl mb-6 text-charcoal">{listTitle}</h2>
34+
35+
<div className="flex flex-col gap-[40px] sm:gap-y-12">
36+
{pagePosts.map((post) => (
37+
<Link
38+
key={post.slug}
39+
href={`/blog/${post.slug}`}
40+
className="grid grid-cols-[1fr] gap-0 sm:grid-cols-[280px_1fr] sm:gap-x-8 no-underline"
41+
>
42+
{/* eslint-disable-next-line @next/next/no-img-element */}
43+
<img src={post.featuredImage} alt={post.title} className="w-full h-[140px] object-cover" loading="lazy" />
44+
<div>
45+
<h3 className="mt-3 sm:mt-0 text-[20px] font-medium text-black">{post.title}</h3>
46+
<p className="text-charcoal text-sm font-normal mt-1 leading-none">
47+
{post.duration}{post.duration && <span className="mx-1"></span>}
48+
{format(new Date(post.date), "MMMM d, yyyy")}
49+
</p>
50+
{post.abstract && (
51+
<p className="text-charcoal text-sm sm:text-base font-normal line-clamp-2 sm:line-clamp-4 mt-2">
52+
{post.abstract.length > 400 ? post.abstract.slice(0, 400) + "…" : post.abstract}
53+
</p>
54+
)}
55+
</div>
56+
</Link>
57+
))}
58+
</div>
59+
60+
{totalPages > 1 && (
61+
<ul className="pagination pagination-default">
62+
<li className={`page-item ${page === 1 ? "disabled" : ""}`}>
63+
<a
64+
aria-disabled={page === 1 ? "true" : undefined}
65+
aria-label="First"
66+
className="page-link"
67+
role="button"
68+
tabIndex={page === 1 ? -1 : 0}
69+
onClick={(e) => { e.preventDefault(); if (page !== 1) goTo(1); }}
70+
>
71+
<span aria-hidden="true">&laquo;&laquo;</span>
72+
</a>
73+
</li>
74+
<li className={`page-item ${page === 1 ? "disabled" : ""}`}>
75+
<a
76+
aria-disabled={page === 1 ? "true" : undefined}
77+
aria-label="Previous"
78+
className="page-link"
79+
role="button"
80+
tabIndex={page === 1 ? -1 : 0}
81+
onClick={(e) => { e.preventDefault(); if (page > 1) goTo(page - 1); }}
82+
>
83+
<span aria-hidden="true">&laquo;</span>
84+
</a>
85+
</li>
86+
{visiblePages.map((p) => (
87+
<li key={p} className={`page-item ${p === page ? "active" : ""}`}>
88+
<a
89+
aria-current={p === page ? "page" : undefined}
90+
aria-label={`Page ${p}`}
91+
className="page-link"
92+
role="button"
93+
onClick={(e) => { e.preventDefault(); goTo(p); }}
94+
>
95+
{p}
96+
</a>
97+
</li>
98+
))}
99+
<li className={`page-item ${page === totalPages ? "disabled" : ""}`}>
100+
<a
101+
aria-disabled={page === totalPages ? "true" : undefined}
102+
aria-label="Next"
103+
className="page-link"
104+
role="button"
105+
tabIndex={page === totalPages ? -1 : 0}
106+
onClick={(e) => { e.preventDefault(); if (page < totalPages) goTo(page + 1); }}
107+
>
108+
<span aria-hidden="true">&raquo;</span>
109+
</a>
110+
</li>
111+
<li className={`page-item ${page === totalPages ? "disabled" : ""}`}>
112+
<a
113+
aria-disabled={page === totalPages ? "true" : undefined}
114+
aria-label="Last"
115+
className="page-link"
116+
role="button"
117+
tabIndex={page === totalPages ? -1 : 0}
118+
onClick={(e) => { e.preventDefault(); if (page < totalPages) goTo(totalPages); }}
119+
>
120+
<span aria-hidden="true">&raquo;&raquo;</span>
121+
</a>
122+
</li>
123+
</ul>
124+
)}
125+
</div>
126+
</div>
127+
</div>
128+
</div>
129+
);
130+
}

0 commit comments

Comments
 (0)