Add blog post and support pubDate/tags
Add new post "After the Silence". Update content schema to use pubDate and include tags (default empty). Update blog listing, post page and start page to use pubDate, render tag lists, and compute/show up to two related posts by tag overlap. Misc formatting and small display tweaks.
This commit is contained in:
+202
-12
@@ -1,16 +1,43 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getCollection, type CollectionEntry } from "astro:content";
|
||||
|
||||
type BlogPost = CollectionEntry<"blog">;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
props: {
|
||||
post,
|
||||
relatedPosts: posts
|
||||
.filter((candidate) => candidate.slug !== post.slug)
|
||||
.map((candidate) => ({
|
||||
post: candidate,
|
||||
score: candidate.data.tags.filter((tag) =>
|
||||
post.data.tags.includes(tag),
|
||||
).length,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return (
|
||||
b.post.data.pubDate.valueOf() -
|
||||
a.post.data.pubDate.valueOf()
|
||||
);
|
||||
})
|
||||
.filter((candidate) => candidate.score > 0)
|
||||
.slice(0, 2)
|
||||
.map((candidate) => candidate.post),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { post, relatedPosts = [] } = Astro.props as {
|
||||
post: BlogPost;
|
||||
relatedPosts: BlogPost[];
|
||||
};
|
||||
|
||||
const { Content } = await post.render();
|
||||
|
||||
function formatDate(date: Date) {
|
||||
@@ -19,7 +46,7 @@ function formatDate(date: Date) {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${post.data.title} — Hidden Den Cafe`}
|
||||
title={`${post.data.title} - Hidden Den Cafe`}
|
||||
description={post.data.description}
|
||||
>
|
||||
<div class="matrix-bg" aria-hidden="true"></div>
|
||||
@@ -28,14 +55,60 @@ function formatDate(date: Date) {
|
||||
<div class="container">
|
||||
<header class="header fade-in">
|
||||
<h1 class="title">{post.data.title}</h1>
|
||||
<p class="date">{formatDate(post.data.date)}</p>
|
||||
<div class="divider">══════════════════════════════</div>
|
||||
<p class="date">{formatDate(post.data.pubDate)}</p>
|
||||
{
|
||||
post.data.tags.length > 0 && (
|
||||
<div
|
||||
class="tag-list"
|
||||
aria-label={`${post.data.title} tags`}
|
||||
>
|
||||
{post.data.tags.map((tag) => (
|
||||
<span class="tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div class="divider">==============================</div>
|
||||
</header>
|
||||
|
||||
<article class="content fade-in">
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
{
|
||||
relatedPosts.length > 0 && (
|
||||
<section
|
||||
class="related fade-in"
|
||||
aria-labelledby="related-posts"
|
||||
>
|
||||
<div class="related-head">
|
||||
<h2 id="related-posts">Related Reading</h2>
|
||||
<a href="/blog" class="related-link">
|
||||
Back to blog
|
||||
</a>
|
||||
</div>
|
||||
<ul class="related-list">
|
||||
{relatedPosts.map((relatedPost) => (
|
||||
<li class="related-item">
|
||||
<a
|
||||
href={`/blog/${relatedPost.slug}`}
|
||||
class="related-card"
|
||||
>
|
||||
<span class="related-date">
|
||||
{formatDate(
|
||||
relatedPost.data.pubDate,
|
||||
)}
|
||||
</span>
|
||||
<h3>{relatedPost.data.title}</h3>
|
||||
<p>{relatedPost.data.description}</p>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<footer class="footer fade-in">
|
||||
<p>Made with love by Latte</p>
|
||||
</footer>
|
||||
@@ -60,8 +133,12 @@ function formatDate(date: Date) {
|
||||
}
|
||||
|
||||
@keyframes grid-move {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(50px, 50px); }
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -107,7 +184,21 @@ function formatDate(date: Date) {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Prose styles for markdown content */
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-accent);
|
||||
border: 1px solid var(--color-surface);
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--color-text);
|
||||
line-height: 1.8;
|
||||
@@ -208,6 +299,82 @@ function formatDate(date: Date) {
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.related {
|
||||
margin-top: var(--space-xl);
|
||||
padding-top: var(--space-lg);
|
||||
border-top: 1px solid var(--color-surface);
|
||||
}
|
||||
|
||||
.related-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.related-head h2 {
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.related-link {
|
||||
color: var(--color-blue);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.related-link:hover {
|
||||
color: var(--color-accent-bright);
|
||||
}
|
||||
|
||||
.related-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.related-card {
|
||||
display: block;
|
||||
padding: var(--space-md);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.related-card:hover {
|
||||
border-color: color-mix(in srgb, var(--color-accent) 55%, transparent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 0 1px
|
||||
color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.related-date {
|
||||
display: inline-block;
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.related-card h3 {
|
||||
font-size: 1rem;
|
||||
color: var(--color-accent-bright);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.related-card p {
|
||||
color: var(--color-text-dim);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: var(--space-xl);
|
||||
text-align: center;
|
||||
@@ -223,9 +390,18 @@ function formatDate(date: Date) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-in:nth-child(1) { animation-delay: 0.1s; }
|
||||
.fade-in:nth-child(2) { animation-delay: 0.2s; }
|
||||
.fade-in:nth-child(3) { animation-delay: 0.3s; }
|
||||
.fade-in:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.fade-in:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.fade-in:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.fade-in:nth-child(4) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
@@ -250,6 +426,11 @@ function formatDate(date: Date) {
|
||||
.divider {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.related-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -261,5 +442,14 @@ function formatDate(date: Date) {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.related-card {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.related-card:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+76
-26
@@ -2,8 +2,9 @@
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const posts = (await getCollection("blog", ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
|
||||
function formatDate(date: Date) {
|
||||
return date.toISOString().split("T")[0];
|
||||
@@ -23,25 +24,50 @@ function formatDate(date: Date) {
|
||||
<div class="divider">══════════════════════════════</div>
|
||||
</header>
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<section class="section fade-in">
|
||||
<p class="empty">No posts yet. Will get around to it.</p>
|
||||
</section>
|
||||
) : (
|
||||
<section class="section fade-in">
|
||||
<ul class="post-list">
|
||||
{posts.map((post) => (
|
||||
<li class="post-item">
|
||||
<span class="post-date">{formatDate(post.data.date)}</span>
|
||||
<div class="post-info">
|
||||
<a href={`/blog/${post.slug}`} class="post-title">{post.data.title}</a>
|
||||
<p class="post-desc">{post.data.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{
|
||||
posts.length === 0 ? (
|
||||
<section class="section fade-in">
|
||||
<p class="empty">
|
||||
No posts yet. Will get around to it.
|
||||
</p>
|
||||
</section>
|
||||
) : (
|
||||
<section class="section fade-in">
|
||||
<ul class="post-list">
|
||||
{posts.map((post) => (
|
||||
<li class="post-item">
|
||||
<span class="post-date">
|
||||
{formatDate(post.data.pubDate)}
|
||||
</span>
|
||||
<div class="post-info">
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
class="post-title"
|
||||
>
|
||||
{post.data.title}
|
||||
</a>
|
||||
<p class="post-desc">
|
||||
{post.data.description}
|
||||
</p>
|
||||
{post.data.tags.length > 0 && (
|
||||
<div
|
||||
class="tag-list"
|
||||
aria-label={`${post.data.title} tags`}
|
||||
>
|
||||
{post.data.tags.map((tag) => (
|
||||
<span class="tag">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<footer class="footer fade-in">
|
||||
<p>Made with love by Latte</p>
|
||||
@@ -67,8 +93,12 @@ function formatDate(date: Date) {
|
||||
}
|
||||
|
||||
@keyframes grid-move {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(50px, 50px); }
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -156,6 +186,20 @@ function formatDate(date: Date) {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-accent);
|
||||
border: 1px solid var(--color-surface);
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--color-text-dim);
|
||||
font-style: italic;
|
||||
@@ -176,9 +220,15 @@ function formatDate(date: Date) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-in:nth-child(1) { animation-delay: 0.1s; }
|
||||
.fade-in:nth-child(2) { animation-delay: 0.2s; }
|
||||
.fade-in:nth-child(3) { animation-delay: 0.3s; }
|
||||
.fade-in:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.fade-in:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.fade-in:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
|
||||
+142
-88
@@ -16,7 +16,7 @@ type Project = {
|
||||
};
|
||||
|
||||
const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
|
||||
(a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
|
||||
const recommendedPosts = posts.slice(0, 3);
|
||||
@@ -28,12 +28,17 @@ const highlightedProjects = highlightedProjectNames
|
||||
.map((name) => typedProjects.find((project) => project.name === name))
|
||||
.filter((project): project is Project => Boolean(project));
|
||||
|
||||
function formatDate(date: CollectionEntry<"blog">["data"]["date"]) {
|
||||
function formatDate(date: CollectionEntry<"blog">["data"]["pubDate"]) {
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function getProjectLink(project: Project) {
|
||||
return project.links.site || project.links.gitea || project.links.github || "/projects";
|
||||
return (
|
||||
project.links.site ||
|
||||
project.links.gitea ||
|
||||
project.links.github ||
|
||||
"/projects"
|
||||
);
|
||||
}
|
||||
|
||||
function getProjectLinkLabel(project: Project) {
|
||||
@@ -57,75 +62,95 @@ function getProjectLinkLabel(project: Project) {
|
||||
<h1 class="title">Start Here</h1>
|
||||
<div class="divider">==============================</div>
|
||||
<p class="lead">
|
||||
This page is a small orientation point for first-time visitors. If
|
||||
you have just found Hidden Den, this is the quickest way to get a
|
||||
feel for what kind of place it is and where you might want to wander
|
||||
next.
|
||||
This page is a small orientation point for first-time
|
||||
visitors. If you have just found Hidden Den, this is the
|
||||
quickest way to get a feel for what kind of place it is and
|
||||
where you might want to wander next.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="section fade-in" aria-labelledby="what-this-place-is">
|
||||
<section
|
||||
class="section fade-in"
|
||||
aria-labelledby="what-this-place-is"
|
||||
>
|
||||
<h2 id="what-this-place-is">What Hidden Den Is</h2>
|
||||
<p class="desc">
|
||||
Hidden Den is Latte's personal corner of the internet: part
|
||||
writing space, part workshop, part quiet place to think out loud. It
|
||||
holds projects, experiments, infrastructure-minded notes, and the
|
||||
kind of personal web presence that does not need to behave like a
|
||||
brand.
|
||||
Hidden Den is Latte's personal corner of the internet:
|
||||
part writing space, part workshop, part quiet place to think
|
||||
out loud. It holds projects, experiments,
|
||||
infrastructure-minded notes, and the kind of personal web
|
||||
presence that does not need to behave like a brand.
|
||||
</p>
|
||||
<p class="desc">
|
||||
The site leans toward privacy, ownership, and human-scale spaces
|
||||
online. It is built to feel warm and readable rather than optimized,
|
||||
noisy, or extractive. More cozy tech wizard than cyberpunk hacker.
|
||||
The site leans toward privacy, ownership, and human-scale
|
||||
spaces online. It is built to feel warm and readable rather
|
||||
than optimized, noisy, or extractive. More cozy tech wizard
|
||||
than cyberpunk hacker.
|
||||
</p>
|
||||
<div class="note">
|
||||
<p>
|
||||
If you want the short version: this is a personal site for
|
||||
writing, building, self-hosting, and keeping a small piece of the
|
||||
web genuinely personal.
|
||||
If you want the short version: this is a personal site
|
||||
for writing, building, self-hosting, and keeping a small
|
||||
piece of the web genuinely personal.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section fade-in" aria-labelledby="recommended-reading">
|
||||
<section
|
||||
class="section fade-in"
|
||||
aria-labelledby="recommended-reading"
|
||||
>
|
||||
<div class="section-head">
|
||||
<h2 id="recommended-reading">Recommended Reading</h2>
|
||||
<a class="section-link" href="/blog">See all posts</a>
|
||||
</div>
|
||||
<p class="desc section-intro">
|
||||
A few good starting points from the blog. These give a feel for the
|
||||
den so far without asking you to dig through everything first.
|
||||
A few good starting points from the blog. These give a feel
|
||||
for the den so far without asking you to dig through
|
||||
everything first.
|
||||
</p>
|
||||
|
||||
{recommendedPosts.length === 0 ? (
|
||||
<p class="empty">The den is still quiet on the writing front.</p>
|
||||
) : (
|
||||
<ul class="reading-list">
|
||||
{recommendedPosts.map((post) => (
|
||||
<li class="reading-item">
|
||||
<article class="content-card">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">posted</span>
|
||||
<time datetime={formatDate(post.data.date)}>
|
||||
{formatDate(post.data.date)}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
||||
</h3>
|
||||
<p>{post.data.description}</p>
|
||||
</article>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{
|
||||
recommendedPosts.length === 0 ? (
|
||||
<p class="empty">
|
||||
The den is still quiet on the writing front.
|
||||
</p>
|
||||
) : (
|
||||
<ul class="reading-list">
|
||||
{recommendedPosts.map((post) => (
|
||||
<li class="reading-item">
|
||||
<article class="content-card">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">
|
||||
posted
|
||||
</span>
|
||||
<time
|
||||
datetime={formatDate(
|
||||
post.data.pubDate,
|
||||
)}
|
||||
>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={`/blog/${post.slug}`}>
|
||||
{post.data.title}
|
||||
</a>
|
||||
</h3>
|
||||
<p>{post.data.description}</p>
|
||||
</article>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="section fade-in" aria-labelledby="explore-the-site">
|
||||
<h2 id="explore-the-site">Explore The Site</h2>
|
||||
<p class="desc section-intro">
|
||||
If you would rather browse by section, these are the best places to
|
||||
continue.
|
||||
If you would rather browse by section, these are the best
|
||||
places to continue.
|
||||
</p>
|
||||
|
||||
<div class="explore-grid">
|
||||
@@ -133,9 +158,9 @@ function getProjectLinkLabel(project: Project) {
|
||||
<span class="card-kicker">About</span>
|
||||
<h3>Meet the person behind the den</h3>
|
||||
<p>
|
||||
A fuller introduction to Latte, the philosophy behind the
|
||||
site, and the kind of internet this place is trying to make
|
||||
room for.
|
||||
A fuller introduction to Latte, the philosophy
|
||||
behind the site, and the kind of internet this place
|
||||
is trying to make room for.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -143,8 +168,9 @@ function getProjectLinkLabel(project: Project) {
|
||||
<span class="card-kicker">Blog</span>
|
||||
<h3>Read the writing</h3>
|
||||
<p>
|
||||
Thoughts, personal pieces, and technical reflections gathered
|
||||
in one place without feeds, tracking, or platform noise.
|
||||
Thoughts, personal pieces, and technical reflections
|
||||
gathered in one place without feeds, tracking, or
|
||||
platform noise.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -152,52 +178,76 @@ function getProjectLinkLabel(project: Project) {
|
||||
<span class="card-kicker">Projects</span>
|
||||
<h3>See what is being built</h3>
|
||||
<p>
|
||||
Bots, tools, experiments, and den-adjacent systems that show
|
||||
the practical side of the site.
|
||||
Bots, tools, experiments, and den-adjacent systems
|
||||
that show the practical side of the site.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section fade-in" aria-labelledby="project-highlights">
|
||||
<section
|
||||
class="section fade-in"
|
||||
aria-labelledby="project-highlights"
|
||||
>
|
||||
<div class="section-head">
|
||||
<h2 id="project-highlights">Projects Worth Seeing</h2>
|
||||
<a class="section-link" href="/projects">Browse all projects</a>
|
||||
<a class="section-link" href="/projects"
|
||||
>Browse all projects</a
|
||||
>
|
||||
</div>
|
||||
<p class="desc section-intro">
|
||||
A small curated subset, just enough to sketch the shape of the work
|
||||
without turning this page into a full catalog.
|
||||
A small curated subset, just enough to sketch the shape of
|
||||
the work without turning this page into a full catalog.
|
||||
</p>
|
||||
|
||||
<ul class="project-list">
|
||||
{highlightedProjects.map((project) => (
|
||||
<li class="project-item">
|
||||
<article class="content-card project-card">
|
||||
<div class="project-topline">
|
||||
<h3>{project.name}</h3>
|
||||
<span class={`status status-${project.status}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
<p>{project.description}</p>
|
||||
<div class="project-footer">
|
||||
<div class="tag-list" aria-label={`${project.name} tags`}>
|
||||
{project.tags.map((tag) => (
|
||||
<span class="tag">{tag}</span>
|
||||
))}
|
||||
{
|
||||
highlightedProjects.map((project) => (
|
||||
<li class="project-item">
|
||||
<article class="content-card project-card">
|
||||
<div class="project-topline">
|
||||
<h3>{project.name}</h3>
|
||||
<span
|
||||
class={`status status-${project.status}`}
|
||||
>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
class="project-link"
|
||||
href={getProjectLink(project)}
|
||||
target={getProjectLink(project).startsWith("http") ? "_blank" : undefined}
|
||||
rel={getProjectLink(project).startsWith("http") ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
{getProjectLinkLabel(project)}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
))}
|
||||
<p>{project.description}</p>
|
||||
<div class="project-footer">
|
||||
<div
|
||||
class="tag-list"
|
||||
aria-label={`${project.name} tags`}
|
||||
>
|
||||
{project.tags.map((tag) => (
|
||||
<span class="tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
class="project-link"
|
||||
href={getProjectLink(project)}
|
||||
target={
|
||||
getProjectLink(
|
||||
project,
|
||||
).startsWith("http")
|
||||
? "_blank"
|
||||
: undefined
|
||||
}
|
||||
rel={
|
||||
getProjectLink(
|
||||
project,
|
||||
).startsWith("http")
|
||||
? "noopener noreferrer"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{getProjectLinkLabel(project)}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -337,7 +387,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
.note {
|
||||
margin-top: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--color-accent) 30%, transparent);
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
@@ -365,7 +416,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
padding: var(--space-md);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||
}
|
||||
|
||||
.content-card h3 {
|
||||
@@ -405,7 +457,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
display: block;
|
||||
padding: var(--space-md);
|
||||
border-radius: 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--color-surface) 70%, transparent);
|
||||
background: color-mix(in srgb, var(--color-bg-light) 88%, transparent);
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
@@ -416,7 +469,8 @@ function getProjectLinkLabel(project: Project) {
|
||||
.explore-card:hover {
|
||||
border-color: color-mix(in srgb, var(--color-accent) 55%, transparent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||
box-shadow: 0 0 0 1px
|
||||
color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.card-kicker {
|
||||
|
||||
Reference in New Issue
Block a user