Make poetry blog minimal
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-23 22:39:44 +05:30
parent 3ced32a4d0
commit 81c890471c
4 changed files with 250 additions and 89 deletions

View file

@ -1,9 +1,3 @@
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { format } from "date-fns";
import { Calendar, Clock, ArrowLeft, User } from "lucide-react";
const BlogPost = ({
post,
children,
@ -11,77 +5,15 @@ const BlogPost = ({
post: any;
children: React.ReactNode;
}) => {
const { title, authorName, image, pubDate, description, authorImage } =
post.data;
const { title } = post.data;
return (
<>
{/* Hero section with gradient background and post info */}
<section className="pb-8 pt-4">
<div className="container max-w-4xl">
<div className="space-y-2 text-center">
<h1 className="blog-post-title">
{title}
</h1>
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">
{description}
</p>
</div>
<div className="mx-auto mt-6 flex max-w-md flex-wrap items-center justify-center gap-6">
{/* Author info */}
{authorImage ? (
<div className="flex items-center gap-3">
</div>
) : (
<div className="flex items-center gap-3">
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full shadow-sm">
<User className="h-5 w-5" />
</div>
<div className="flex flex-col text-sm">
<span className="font-medium">
{authorName || "Anonymous"}
</span>
<span className="text-muted-foreground">Author</span>
</div>
</div>
)}
{/* Date info */}
<div className="flex items-center gap-4">
<div className="text-muted-foreground flex items-center gap-1 text-sm">
<Calendar className="h-4 w-4" />
<span>{format(pubDate, "MMMM d, yyyy")}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1 text-sm">
<Clock className="h-4 w-4" />
<span>5 min read</span>
</div>
</div>
</div>
</div>
</section>
{/* Featured image */}
{image && (
<section className="container max-w-5xl py-4">
<div className="aspect-video w-full overflow-hidden rounded-xl border">
<img
src={image}
alt={title}
className="h-full w-full object-cover"
/>
</div>
</section>
)}
{/* Article content */}
<section className="container my-10 max-w-3xl">
<article className="prose prose-lg dark:prose-invert prose-headings:font-semibold prose-a:text-primary mx-auto">
{children}
<section className="container px-6 py-2 md:py-6">
<article className="poetry-post mx-auto">
<h1 className="poetry-post-title">{title}</h1>
<div className="poetry-post-body">{children}</div>
</article>
</section>
</>
);
};

View file

@ -2,7 +2,6 @@
import { getCollection, render } from 'astro:content';
import DefaultLayout from '@/layouts/DefaultLayout.astro';
import { BlogPost } from '@/components/sections/blog-post';
import NewsletterSignup from '@/components/sections/newsletter-signup';
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
import { SITE_TITLE } from '@/consts';
@ -19,11 +18,9 @@ const { Content } = await render(post);
---
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
<div class="py-16 md:py-28 lg:py-32">
<BlogPost post={post} client:only="react">
<div class="py-10 md:py-16 lg:py-20">
<BlogPost post={post}>
<Content />
</BlogPost>
</div>
<NewsletterSignup client:load />
</DefaultLayout>

View file

@ -3,21 +3,116 @@ import { getCollection } from 'astro:content';
import DefaultLayout from '@/layouts/DefaultLayout.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
import { BlogPosts } from '@/components/sections/blog-posts';
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
const formatDate = new Intl.DateTimeFormat('en', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
const poems = posts.map((post) => ({
title: post.data.title,
description: post.data.description,
href: `/${post.id.replace(/\/index$/, '')}/`,
dateLabel: formatDate.format(post.data.pubDate),
searchText: `${post.data.title} ${post.data.description}`.toLowerCase(),
}));
const recentPoems = poems.slice(0, 3);
---
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
<div class="py-12 md:py-20 lg:py-24">
<div class="container max-w-5xl">
<div class="mb-10">
<h1 class="blog-page-title mb-3">{ACTIVE_BLOG_SITE.title}</h1>
<p class="text-lg text-muted-foreground">{ACTIVE_BLOG_SITE.description}</p>
<section class="poetry-home">
<div class="poetry-home-panel">
<div class="poetry-home-chrome" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<BlogPosts posts={posts} client:only="react" />
<header class="poetry-home-header">
<div>
<p class="poetry-home-kicker">{ACTIVE_BLOG_SITE.description}</p>
<h1>{ACTIVE_BLOG_SITE.title}</h1>
</div>
<form class="poetry-search" data-poem-search-form role="search">
<label class="sr-only" for="poem-search">Search poems</label>
<input
id="poem-search"
data-poem-search-input
type="search"
autocomplete="off"
placeholder="Search poems"
/>
<button type="submit" aria-label="Search poems">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="6.5"></circle>
<path d="m16 16 4 4"></path>
</svg>
</button>
</form>
</header>
<div class="poetry-home-grid">
<section class="poetry-recent" aria-labelledby="recent-poems">
<h2 id="recent-poems">Recent</h2>
<ol>
{recentPoems.map((poem) => (
<li>
<time>{poem.dateLabel}</time>
<a href={poem.href}>{poem.title}</a>
</li>
))}
</ol>
</section>
<section class="poetry-archive" aria-labelledby="all-poems">
<h2 id="all-poems">All Poems</h2>
<ol data-poem-list>
{poems.map((poem) => (
<li data-poem-row data-search={poem.searchText}>
<a href={poem.href}>{poem.title}</a>
<time>{poem.dateLabel}</time>
</li>
))}
</ol>
<p class="poetry-empty" data-poem-empty hidden>No poems found.</p>
</section>
</div>
</div>
</section>
<script is:inline>
const form = document.querySelector("[data-poem-search-form]");
const input = document.querySelector("[data-poem-search-input]");
const rows = [...document.querySelectorAll("[data-poem-row]")];
const empty = document.querySelector("[data-poem-empty]");
function filterPoems() {
const query = input.value.trim().toLowerCase();
let visibleCount = 0;
for (const row of rows) {
const isVisible = !query || row.dataset.search.includes(query);
row.hidden = !isVisible;
if (isVisible) visibleCount += 1;
}
empty.hidden = visibleCount !== 0;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
input.focus();
filterPoems();
});
input.addEventListener("input", filterPoems);
</script>
</DefaultLayout>

View file

@ -293,6 +293,143 @@
@apply text-2xl font-semibold md:text-3xl lg:text-4xl;
}
.poetry-post {
@apply max-w-2xl text-center;
}
.poetry-post-title {
@apply mb-12 text-3xl font-semibold md:mb-16 md:text-4xl;
}
.poetry-post-body {
@apply mx-auto max-w-xl text-center text-xl leading-loose text-foreground/80 md:text-2xl;
}
.poetry-post-body :where(p) {
margin: 0 0 1.75em;
}
.poetry-post-body :where(p:last-child) {
margin-bottom: 0;
}
.poetry-post-body :where(a) {
text-decoration-line: underline;
text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
text-underline-offset: 0.2em;
}
.poetry-post-body :where(ul, ol) {
display: inline-block;
margin: 1.5em auto;
padding-left: 1.4em;
text-align: left;
}
.poetry-post-body :where(blockquote) {
margin: 2em auto;
max-width: 32rem;
color: hsl(var(--muted-foreground));
}
.poetry-post-body :where(hr) {
display: none;
}
.poetry-post-body :where(pre) {
margin-inline: auto;
text-align: left;
}
.poetry-home {
@apply px-5 py-10 md:px-8 md:py-16 lg:py-20;
}
.poetry-home-panel {
@apply mx-auto max-w-5xl rounded-2xl border bg-card px-7 py-8 shadow-sm md:px-10 md:py-10;
}
.poetry-home-chrome {
@apply mb-12 flex gap-3;
}
.poetry-home-chrome span {
@apply block h-3 w-3 rounded-full bg-muted;
}
.poetry-home-header {
@apply flex flex-col gap-6 border-b pb-8 md:flex-row md:items-end md:justify-between;
}
.poetry-home-header h1 {
@apply mt-2 text-3xl font-semibold md:text-4xl;
}
.poetry-home-kicker {
@apply max-w-xl text-base text-muted-foreground;
}
.poetry-search {
@apply flex w-full max-w-sm items-center gap-2 rounded-full border bg-background/80 px-4 py-2 md:w-72;
}
.poetry-search input {
@apply min-w-0 flex-1 bg-transparent text-base outline-none placeholder:text-muted-foreground/70;
}
.poetry-search button {
@apply flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground;
}
.poetry-search svg {
@apply h-5 w-5;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.poetry-home-grid {
@apply grid gap-12 pt-10 md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.3fr)] md:gap-16;
}
.poetry-recent h2,
.poetry-archive h2 {
@apply mb-6 text-sm font-semibold uppercase tracking-wider text-muted-foreground;
}
.poetry-recent ol,
.poetry-archive ol {
@apply m-0 list-none p-0;
}
.poetry-recent li {
@apply grid grid-cols-[5.75rem_minmax(0,1fr)] gap-4 py-3;
}
.poetry-recent time,
.poetry-archive time {
@apply text-muted-foreground/75;
}
.poetry-recent a,
.poetry-archive a {
@apply font-semibold text-foreground decoration-muted-foreground/30 underline-offset-4 hover:underline;
}
.poetry-archive li {
@apply flex items-baseline justify-between gap-8 border-t py-4;
}
.poetry-archive li[hidden] {
display: none;
}
.poetry-empty {
@apply border-t py-4 text-muted-foreground;
}
.blog-card-title-featured {
@apply text-xl font-semibold md:text-2xl;
}