Add featured-post flag, align tool-page connector arrows, misc blog polish

- Posts collection: new "featured" checkbox (Payload) — /blog now prefers
  a manually-featured post over just the most recent one. Query sorts by
  "-featured,-publishedAt" so multiple accidentally-featured posts
  resolve deterministically (most recent among them wins) instead of
  erroring or being ambiguous.
- challenge/page.tsx: step-connector arrow now uses the same
  /icon-arrow-connector.svg asset as todo-cards/newsletter's HowItWorks,
  replacing its own hand-drawn inline SVG.
- todo-cards & newsletter HowItWorks: connector arrows now vertically
  centered on their icon (md:mt-[1.625rem]), matching Challenge's
  margin-based centering technique instead of sitting flush at the row's
  top edge.
- Blog overview: featured card overlaps the header slightly (-mt-8) per
  Figma-inspired refinement, and now also shows the publish date in its
  meta row (previously only the list items did).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-19 21:24:04 +00:00
parent ed58a65b8f
commit 8bfbf74693
5 changed files with 38 additions and 14 deletions
+6 -1
View File
@@ -43,7 +43,10 @@ export default async function BlogOverviewPage() {
</Reveal>
{featured && (
<Reveal delay={0.1} className="w-full px-[var(--layout-padding-x)] pt-10 pb-4">
// -mt-8, not pt-10 — pulls the card up to slightly overlap the
// hero section's bottom edge instead of sitting flush below it.
// relative z-10 keeps it stacked above the hero's own photo.
<Reveal delay={0.1} className="relative z-10 w-full px-[var(--layout-padding-x)] -mt-8 pb-4">
<Link
href={`/blog/${featured.slug}`}
className="group grid grid-cols-1 md:grid-cols-2 w-full max-w-[80rem] mx-auto rounded-md overflow-hidden bg-bg-muted transition-transform duration-300 hover:-translate-y-1"
@@ -58,6 +61,8 @@ export default async function BlogOverviewPage() {
<span>{featured.category}</span>
<span></span>
<span>{featured.readTime} Min</span>
<span></span>
<span className="uppercase">{formatDate(featured.publishedAt)}</span>
</div>
<p
className="font-semibold text-h-section text-text-primary"
+12 -10
View File
@@ -86,14 +86,6 @@ function LockIcon() {
);
}
function ArrowRight() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className="text-[#ccc] shrink-0">
<path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
const steps = [
{
icon: <IconEnvelope />,
@@ -294,8 +286,18 @@ export default function ChallengePage() {
</div>
</RevealItem>,
i < steps.length - 1 ? (
<div key={`arrow-${i}`} className="hidden lg:flex items-center shrink-0 mt-5">
<ArrowRight />
// Same /icon-arrow-connector.svg asset and rotate-on-stack
// pattern as todo-cards/newsletter's HowItWorks — this
// used to be its own hand-drawn SVG arrow, inconsistent
// with those two. Always visible (rotated 90° while
// stacked below lg, this page's own structural
// breakpoint) rather than hidden below lg like before.
<div key={`arrow-${i}`} className="flex items-center justify-center shrink-0 lg:mt-5">
<img
alt=""
src="/icon-arrow-connector.svg"
className="w-6 h-6 rotate-90 lg:w-10 lg:h-3 lg:rotate-0"
/>
</div>
) : null,
])}
+10 -1
View File
@@ -10,6 +10,7 @@ export type BlogPost = {
excerpt: string;
thumbnail: string | null;
publishedAt: string;
featured: boolean;
};
type PayloadPost = {
@@ -21,12 +22,18 @@ type PayloadPost = {
excerpt: string;
thumbnail: { url: string } | number | null;
publishedAt: string;
featured: boolean;
};
// sort: "-featured,-publishedAt" — Payload's multi-field sort puts any
// featured post(s) first, most-recently-published first among those, then
// the rest by date. Callers that just want posts[0] as "the" featured post
// (e.g. /blog) get a deterministic pick even if more than one post was
// accidentally marked featured — no special-casing needed here.
export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
const params = new URLSearchParams({
"where[tenant.slug][equals]": TENANT_SLUG,
sort: "-publishedAt",
sort: "-featured,-publishedAt",
depth: "2",
limit: String(limit),
});
@@ -56,6 +63,7 @@ export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
? post.thumbnail.url
: null,
publishedAt: post.publishedAt,
featured: post.featured,
}));
}
@@ -96,6 +104,7 @@ export async function getPostBySlug(slug: string): Promise<PostDetail | null> {
typeof doc.thumbnail === "object" && doc.thumbnail ? doc.thumbnail.url : null,
content: doc.content,
publishedAt: doc.publishedAt,
featured: doc.featured,
};
}
+5 -1
View File
@@ -57,7 +57,11 @@ export function HowItWorks() {
<p className="text-body-sm text-text-primary text-center">{step.desc}</p>
</RevealItem>
{i < steps.length - 1 && (
<div className="flex items-center justify-center shrink-0">
// md:mt-[1.625rem] (26px) centers the arrow on the h-16
// (64px) icon above it — same margin-based centering
// technique as Challenge's step connector and todo-cards'
// identical HowItWorks, not just the same icon asset.
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
<img
alt=""
src="/icon-arrow-connector.svg"
+5 -1
View File
@@ -68,7 +68,11 @@ export function HowItWorks() {
<p className="text-body-sm text-text-primary text-center">{step.desc}</p>
</RevealItem>
{i < steps.length - 1 && (
<div className="flex items-center justify-center shrink-0">
// md:mt-[1.625rem] (26px) centers the arrow on the h-16
// (64px) icon above it — (64 - arrow's own 12px height) / 2
// — same margin-based centering technique as Challenge's
// step connector, not just the same icon asset.
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
<img
alt=""
src="/icon-arrow-connector.svg"