This commit is contained in:
parent
3ced32a4d0
commit
81c890471c
4 changed files with 250 additions and 89 deletions
|
|
@ -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 = ({
|
const BlogPost = ({
|
||||||
post,
|
post,
|
||||||
children,
|
children,
|
||||||
|
|
@ -11,77 +5,15 @@ const BlogPost = ({
|
||||||
post: any;
|
post: any;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const { title, authorName, image, pubDate, description, authorImage } =
|
const { title } = post.data;
|
||||||
post.data;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className="container px-6 py-2 md:py-6">
|
||||||
{/* Hero section with gradient background and post info */}
|
<article className="poetry-post mx-auto">
|
||||||
<section className="pb-8 pt-4">
|
<h1 className="poetry-post-title">{title}</h1>
|
||||||
<div className="container max-w-4xl">
|
<div className="poetry-post-body">{children}</div>
|
||||||
<div className="space-y-2 text-center">
|
</article>
|
||||||
<h1 className="blog-post-title">
|
</section>
|
||||||
{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}
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import { getCollection, render } from 'astro:content';
|
import { getCollection, render } from 'astro:content';
|
||||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||||
import { BlogPost } from '@/components/sections/blog-post';
|
import { BlogPost } from '@/components/sections/blog-post';
|
||||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
|
||||||
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
|
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
|
||||||
import { SITE_TITLE } from '@/consts';
|
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}>
|
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
|
||||||
<div class="py-16 md:py-28 lg:py-32">
|
<div class="py-10 md:py-16 lg:py-20">
|
||||||
<BlogPost post={post} client:only="react">
|
<BlogPost post={post}>
|
||||||
<Content />
|
<Content />
|
||||||
</BlogPost>
|
</BlogPost>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NewsletterSignup client:load />
|
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,116 @@ import { getCollection } from 'astro:content';
|
||||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||||
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
|
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
|
||||||
import { BlogPosts } from '@/components/sections/blog-posts';
|
|
||||||
|
|
||||||
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
|
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
|
||||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
(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}>
|
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||||
<div class="py-12 md:py-20 lg:py-24">
|
<section class="poetry-home">
|
||||||
<div class="container max-w-5xl">
|
<div class="poetry-home-panel">
|
||||||
<div class="mb-10">
|
<div class="poetry-home-chrome" aria-hidden="true">
|
||||||
<h1 class="blog-page-title mb-3">{ACTIVE_BLOG_SITE.title}</h1>
|
<span></span>
|
||||||
<p class="text-lg text-muted-foreground">{ACTIVE_BLOG_SITE.description}</p>
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
<BlogPosts posts={posts} client:only="react" />
|
|
||||||
</div>
|
</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>
|
</DefaultLayout>
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,143 @@
|
||||||
@apply text-2xl font-semibold md:text-3xl lg:text-4xl;
|
@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 {
|
.blog-card-title-featured {
|
||||||
@apply text-xl font-semibold md:text-2xl;
|
@apply text-xl font-semibold md:text-2xl;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue