Initial commit: EmDash blog template
Fixed index.astro: escaped curly braces in code display block to prevent Astro parser misinterpreting them as expressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
279
src/components/PostCard.astro
Normal file
279
src/components/PostCard.astro
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
import type { MediaValue, ContentBylineCredit } from "emdash";
|
||||
import { Image } from "emdash/ui";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
excerpt?: string;
|
||||
featuredImage?: MediaValue | string;
|
||||
href: string;
|
||||
date?: Date;
|
||||
readingTime?: number;
|
||||
tags?: Array<{ slug: string; label: string }>;
|
||||
bylines?: ContentBylineCredit[];
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
excerpt,
|
||||
featuredImage,
|
||||
href,
|
||||
date,
|
||||
readingTime,
|
||||
tags,
|
||||
bylines,
|
||||
} = Astro.props;
|
||||
|
||||
const formattedDate = date
|
||||
? date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
: null;
|
||||
---
|
||||
|
||||
<article class="post-card">
|
||||
<a href={href} class="card-link">
|
||||
{
|
||||
featuredImage ? (
|
||||
<div class="card-image">
|
||||
<Image image={featuredImage} />
|
||||
</div>
|
||||
) : (
|
||||
<div class="card-placeholder" />
|
||||
)
|
||||
}
|
||||
<div class="card-body">
|
||||
<div class="card-meta">
|
||||
{
|
||||
bylines && bylines.length > 0 && (
|
||||
<>
|
||||
<div class="card-bylines">
|
||||
{bylines.slice(0, 1).map((credit) => (
|
||||
<span class="card-byline">
|
||||
{credit.byline.avatarMediaId && (
|
||||
<img
|
||||
src={`/_emdash/api/media/file/${credit.byline.avatarMediaId}`}
|
||||
alt={credit.byline.displayName}
|
||||
class="card-byline-avatar"
|
||||
/>
|
||||
)}
|
||||
<span class="card-byline-name">
|
||||
{credit.byline.displayName}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
{bylines.length > 1 && (
|
||||
<span
|
||||
class="byline-more"
|
||||
data-tooltip={bylines
|
||||
.slice(1)
|
||||
.map((c) => c.byline.displayName)
|
||||
.join(", ")}
|
||||
title={bylines
|
||||
.slice(1)
|
||||
.map((c) => c.byline.displayName)
|
||||
.join(", ")}
|
||||
tabindex="0"
|
||||
>
|
||||
+{bylines.length - 1}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(formattedDate || readingTime) && <span class="meta-dot" />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{formattedDate && <time>{formattedDate}</time>}
|
||||
{formattedDate && readingTime && <span class="meta-dot" />}
|
||||
{readingTime && <span>{readingTime} min</span>}
|
||||
</div>
|
||||
<h2 class="card-title">{title}</h2>
|
||||
{excerpt && <p class="card-excerpt">{excerpt}</p>}
|
||||
</div>
|
||||
</a>
|
||||
{
|
||||
tags && tags.length > 0 && (
|
||||
<div class="card-tags">
|
||||
{tags.slice(0, 2).map((tag) => (
|
||||
<a href={`/tag/${tag.slug}`} class="card-tag">
|
||||
{tag.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.post-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.card-image {
|
||||
aspect-ratio: 16 / 10;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card-link:hover .card-image img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.card-placeholder {
|
||||
aspect-ratio: 16 / 10;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--spacing-2);
|
||||
row-gap: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-muted);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.card-meta time,
|
||||
.card-meta span:not(.meta-dot) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta-dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-muted);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
line-height: var(--leading-snug);
|
||||
letter-spacing: var(--tracking-snug);
|
||||
margin-bottom: var(--spacing-2);
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.card-link:hover .card-title {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.card-excerpt {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--leading-relaxed);
|
||||
color: var(--color-text-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
display: inline-block;
|
||||
padding: var(--tag-padding-y) var(--spacing-2);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-muted);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
background var(--transition-fast);
|
||||
}
|
||||
|
||||
.card-tag:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
/* Byline styles */
|
||||
.card-bylines {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-byline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.card-byline-avatar {
|
||||
width: var(--avatar-size-xs);
|
||||
height: var(--avatar-size-xs);
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.card-byline-name {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.byline-more {
|
||||
position: relative;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-muted);
|
||||
margin-left: 2px;
|
||||
cursor: default;
|
||||
border-radius: var(--radius);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.byline-more:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.byline-more[data-tooltip]:hover::after,
|
||||
.byline-more[data-tooltip]:focus-visible::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
background: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 400;
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius);
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
45
src/components/TagList.astro
Normal file
45
src/components/TagList.astro
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
interface Props {
|
||||
tags: Array<{ slug: string; label: string }>;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { tags, class: className } = Astro.props;
|
||||
---
|
||||
|
||||
{tags.length > 0 && (
|
||||
<ul class:list={["tag-list", className]}>
|
||||
{tags.map((tag) => (
|
||||
<li>
|
||||
<a href={`/tag/${tag.slug}`} class="tag">{tag.label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: var(--tag-padding-y) var(--spacing-3);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast), background var(--transition-fast);
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-border);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user