Skip to content

Commit 81c8b5e

Browse files
committed
feat: enhance documentation app
1 parent 42c5c4f commit 81c8b5e

22 files changed

Lines changed: 629 additions & 146 deletions

File tree

.github/workflows/deploy-blog.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ on:
77
- 'apps/blog/**'
88
- 'packages/**'
99
- 'pnpm-lock.yaml'
10+
schedule:
11+
- cron: '0 6 * * *' # Every day at 6:00 UTC
1012
workflow_dispatch:
1113

12-
env:
13-
DEPLOY_TARGET: '' # Set to 'vercel', 'cloudflare', 'github-pages', or leave empty to skip
14-
1514
jobs:
1615
build:
1716
runs-on: ubuntu-latest
@@ -36,7 +35,7 @@ jobs:
3635

3736
deploy-vercel:
3837
needs: build
39-
if: env.DEPLOY_TARGET == 'vercel'
38+
if: vars.DEPLOY_TARGET == 'vercel'
4039
runs-on: ubuntu-latest
4140
steps:
4241
- uses: actions/checkout@v4
@@ -57,7 +56,7 @@ jobs:
5756

5857
deploy-cloudflare:
5958
needs: build
60-
if: env.DEPLOY_TARGET == 'cloudflare'
59+
if: vars.DEPLOY_TARGET == 'cloudflare'
6160
runs-on: ubuntu-latest
6261
permissions:
6362
contents: read
@@ -75,7 +74,7 @@ jobs:
7574

7675
deploy-github-pages:
7776
needs: build
78-
if: env.DEPLOY_TARGET == 'github-pages'
77+
if: vars.DEPLOY_TARGET == 'github-pages'
7978
runs-on: ubuntu-latest
8079
permissions:
8180
pages: write

.github/workflows/deploy-docs.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ on:
99
- 'pnpm-lock.yaml'
1010
workflow_dispatch:
1111

12-
env:
13-
DEPLOY_TARGET: '' # Set to 'vercel', 'cloudflare', 'github-pages', or leave empty to skip
14-
1512
jobs:
1613
build:
1714
runs-on: ubuntu-latest
@@ -36,7 +33,7 @@ jobs:
3633

3734
deploy-vercel:
3835
needs: build
39-
if: env.DEPLOY_TARGET == 'vercel'
36+
if: vars.DEPLOY_TARGET == 'vercel'
4037
runs-on: ubuntu-latest
4138
steps:
4239
- uses: actions/checkout@v4
@@ -57,7 +54,7 @@ jobs:
5754

5855
deploy-cloudflare:
5956
needs: build
60-
if: env.DEPLOY_TARGET == 'cloudflare'
57+
if: vars.DEPLOY_TARGET == 'cloudflare'
6158
runs-on: ubuntu-latest
6259
permissions:
6360
contents: read
@@ -75,7 +72,7 @@ jobs:
7572

7673
deploy-github-pages:
7774
needs: build
78-
if: env.DEPLOY_TARGET == 'github-pages'
75+
if: vars.DEPLOY_TARGET == 'github-pages'
7976
runs-on: ubuntu-latest
8077
permissions:
8178
pages: write

.github/workflows/deploy-website.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ on:
99
- 'pnpm-lock.yaml'
1010
workflow_dispatch:
1111

12-
env:
13-
DEPLOY_TARGET: '' # Set to 'vercel', 'cloudflare', 'github-pages', or leave empty to skip
14-
1512
jobs:
1613
build:
1714
runs-on: ubuntu-latest
@@ -36,7 +33,7 @@ jobs:
3633

3734
deploy-vercel:
3835
needs: build
39-
if: env.DEPLOY_TARGET == 'vercel'
36+
if: vars.DEPLOY_TARGET == 'vercel'
4037
runs-on: ubuntu-latest
4138
steps:
4239
- uses: actions/checkout@v4
@@ -57,7 +54,7 @@ jobs:
5754

5855
deploy-cloudflare:
5956
needs: build
60-
if: env.DEPLOY_TARGET == 'cloudflare'
57+
if: vars.DEPLOY_TARGET == 'cloudflare'
6158
runs-on: ubuntu-latest
6259
permissions:
6360
contents: read
@@ -75,7 +72,7 @@ jobs:
7572

7673
deploy-github-pages:
7774
needs: build
78-
if: env.DEPLOY_TARGET == 'github-pages'
75+
if: vars.DEPLOY_TARGET == 'github-pages'
7976
runs-on: ubuntu-latest
8077
permissions:
8178
pages: write
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
import type { Post } from '../lib/posts'
3+
import { formatDate, getPostSlug, getReadingTime } from '../lib/posts'
4+
5+
interface Props {
6+
posts: Post[]
7+
}
8+
9+
const { posts } = Astro.props
10+
const [first, second, third] = posts
11+
---
12+
13+
{posts.length > 0 && (
14+
<section class="mb-12">
15+
{posts.length === 1 ? (
16+
<article class="group rounded-xl border bg-card overflow-hidden">
17+
<a href={`/${getPostSlug(first)}`} class="block">
18+
{first.data.cover && (
19+
<div class="overflow-hidden">
20+
<img
21+
src={first.data.cover}
22+
alt={first.data.title}
23+
class="w-full aspect-[16/10] object-cover transition-transform duration-300 group-hover:scale-[1.03]"
24+
/>
25+
</div>
26+
)}
27+
<div class="p-6">
28+
{first.data.tags.length > 0 && (
29+
<div class="flex flex-wrap gap-2 mb-3">
30+
{first.data.tags.map((tag) => (
31+
<span class="text-xs font-medium text-primary">{tag}</span>
32+
))}
33+
</div>
34+
)}
35+
<h2 class="text-2xl font-bold group-hover:text-primary transition-colors">{first.data.title}</h2>
36+
<p class="text-muted-foreground mt-2">{first.data.description}</p>
37+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground mt-3">
38+
<time datetime={first.data.date.toISOString()}>{formatDate(first.data.date)}</time>
39+
<span>·</span>
40+
<span>{getReadingTime(first.body ?? '')} min read</span>
41+
</div>
42+
</div>
43+
</a>
44+
</article>
45+
) : posts.length === 2 ? (
46+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
47+
{[first, second].map((post) => (
48+
<article class="group rounded-xl border bg-card overflow-hidden">
49+
<a href={`/${getPostSlug(post)}`} class="block">
50+
{post.data.cover && (
51+
<div class="overflow-hidden">
52+
<img
53+
src={post.data.cover}
54+
alt={post.data.title}
55+
class="w-full aspect-[16/10] object-cover transition-transform duration-300 group-hover:scale-[1.03]"
56+
/>
57+
</div>
58+
)}
59+
<div class="p-6">
60+
{post.data.tags.length > 0 && (
61+
<div class="flex flex-wrap gap-2 mb-3">
62+
{post.data.tags.map((tag) => (
63+
<span class="text-xs font-medium text-primary">{tag}</span>
64+
))}
65+
</div>
66+
)}
67+
<h2 class="text-2xl font-bold group-hover:text-primary transition-colors">{post.data.title}</h2>
68+
<p class="text-muted-foreground mt-2">{post.data.description}</p>
69+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground mt-3">
70+
<time datetime={post.data.date.toISOString()}>{formatDate(post.data.date)}</time>
71+
<span>·</span>
72+
<span>{getReadingTime(post.body ?? '')} min read</span>
73+
</div>
74+
</div>
75+
</a>
76+
</article>
77+
))}
78+
</div>
79+
) : (
80+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
81+
<article class="group rounded-xl border bg-card overflow-hidden lg:row-span-2">
82+
<a href={`/${getPostSlug(first)}`} class="block h-full">
83+
{first.data.cover && (
84+
<div class="overflow-hidden">
85+
<img
86+
src={first.data.cover}
87+
alt={first.data.title}
88+
class="w-full aspect-[16/10] object-cover transition-transform duration-300 group-hover:scale-[1.03]"
89+
/>
90+
</div>
91+
)}
92+
<div class="p-6">
93+
{first.data.tags.length > 0 && (
94+
<div class="flex flex-wrap gap-2 mb-3">
95+
{first.data.tags.map((tag) => (
96+
<span class="text-xs font-medium text-primary">{tag}</span>
97+
))}
98+
</div>
99+
)}
100+
<h2 class="text-2xl font-bold group-hover:text-primary transition-colors">{first.data.title}</h2>
101+
<p class="text-muted-foreground mt-2">{first.data.description}</p>
102+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground mt-3">
103+
<time datetime={first.data.date.toISOString()}>{formatDate(first.data.date)}</time>
104+
<span>·</span>
105+
<span>{getReadingTime(first.body ?? '')} min read</span>
106+
</div>
107+
</div>
108+
</a>
109+
</article>
110+
{[second, third].filter(Boolean).map((post) => (
111+
<article class="group rounded-xl border bg-card overflow-hidden">
112+
<a href={`/${getPostSlug(post)}`} class="block p-6">
113+
{post.data.tags.length > 0 && (
114+
<div class="flex flex-wrap gap-2 mb-3">
115+
{post.data.tags.map((tag) => (
116+
<span class="text-xs font-medium text-primary">{tag}</span>
117+
))}
118+
</div>
119+
)}
120+
<h2 class="text-xl font-bold group-hover:text-primary transition-colors">{post.data.title}</h2>
121+
<p class="text-muted-foreground text-sm mt-2">{post.data.description}</p>
122+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground mt-3">
123+
<time datetime={post.data.date.toISOString()}>{formatDate(post.data.date)}</time>
124+
<span>·</span>
125+
<span>{getReadingTime(post.body ?? '')} min read</span>
126+
</div>
127+
</a>
128+
</article>
129+
))}
130+
</div>
131+
)}
132+
</section>
133+
)}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
import { formatDate } from '../lib/posts'
3+
import type { Author } from '../lib/authors'
4+
5+
interface Props {
6+
title: string
7+
description?: string
8+
date: Date
9+
tags: string[]
10+
cover?: string
11+
slug: string
12+
readingTime: number
13+
author?: Author
14+
class?: string
15+
}
16+
17+
const { title, description, date, tags, cover, slug, readingTime, author, class: className } = Astro.props
18+
---
19+
20+
<article class:list={['post-card group', className]}>
21+
<a href={`/${slug}`} class="post-card-link block rounded-xl overflow-hidden relative border border-transparent hover:border-primary/20 transition-colors duration-400">
22+
<div class="pointer-events-none absolute inset-0 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-linear-to-b from-primary/20 via-primary/10 to-transparent" />
23+
<div class="post-card-inner transition-transform duration-300 ease-out">
24+
<!-- Image -->
25+
<div class="post-card-img rounded-t-xl bg-muted aspect-16/10 shrink-0">
26+
{cover ? (
27+
<img src={cover} alt={title} class="w-full h-full rounded-xl object-cover" />
28+
) : (
29+
<div class="w-full h-full bg-linear-to-br from-primary/20 to-primary/5" />
30+
)}
31+
</div>
32+
<!-- Body -->
33+
<div class="post-card-body relative z-30 p-3 flex flex-col">
34+
<div class="flex flex-wrap items-center gap-2 mb-2">
35+
{tags.map((tag) => (
36+
<span class="rounded-full border border-primary/30 bg-primary/10 text-primary group-hover:border-border group-hover:bg-background group-hover:text-foreground px-2 py-px text-[9px] font-semibold uppercase tracking-wider transition-colors duration-300">
37+
{tag}
38+
</span>
39+
))}
40+
41+
{readingTime > 0 && (
42+
<span class="flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 text-primary group-hover:border-border group-hover:bg-background group-hover:text-foreground px-2.5 py-px text-[9px] font-semibold uppercase tracking-wider transition-colors duration-300">
43+
<svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
44+
{readingTime} minutes
45+
</span>
46+
)}
47+
</div>
48+
<time datetime={date.toISOString()} class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
49+
{formatDate(date)}
50+
</time>
51+
<h2 class="font-bold text-lg leading-snug mt-1.5 line-clamp-2">
52+
{title}
53+
</h2>
54+
{description && <p class="post-card-desc text-sm text-muted-foreground mt-3 line-clamp-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 delay-100">{description}</p>}
55+
{author && (
56+
<div class="post-card-author flex items-center gap-3 mt-auto pt-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 delay-150">
57+
<img src={author.avatar} alt={author.name} class="size-9 rounded-full object-cover" />
58+
<div>
59+
<p class="text-sm font-semibold leading-tight">{author.name}</p>
60+
<p class="text-xs text-muted-foreground">{author.title}</p>
61+
</div>
62+
</div>
63+
)}
64+
</div>
65+
</div>
66+
</a>
67+
</article>
68+
69+
<style>
70+
.post-card:hover .post-card-inner {
71+
transform: translateY(var(--slide));
72+
}
73+
</style>
74+
75+
<script>
76+
function initPostCards() {
77+
const cards = document.querySelectorAll('.post-card')
78+
const measurements: { card: HTMLElement; link: HTMLElement; body: HTMLElement; imgHeight: number; bodyNoHidden: number }[] = []
79+
80+
// First pass: reset and measure
81+
cards.forEach((card) => {
82+
const link = card.querySelector('.post-card-link') as HTMLElement | null
83+
const img = card.querySelector('.post-card-img') as HTMLElement | null
84+
const body = card.querySelector('.post-card-body') as HTMLElement | null
85+
if (!link || !img || !body) return
86+
87+
link.style.height = ''
88+
body.style.minHeight = ''
89+
90+
const imgHeight = img.offsetHeight
91+
const desc = card.querySelector('.post-card-desc') as HTMLElement | null
92+
const author = card.querySelector('.post-card-author') as HTMLElement | null
93+
94+
if (desc) desc.style.display = 'none'
95+
if (author) author.style.display = 'none'
96+
const bodyNoHidden = body.offsetHeight
97+
if (desc) desc.style.display = ''
98+
if (author) author.style.display = ''
99+
100+
measurements.push({ card: card as HTMLElement, link, body, imgHeight, bodyNoHidden })
101+
})
102+
103+
// Find max card height
104+
const maxCardHeight = Math.max(...measurements.map((m) => m.imgHeight + m.bodyNoHidden))
105+
106+
// Second pass: apply uniform height
107+
measurements.forEach(({ card, link, body, imgHeight }) => {
108+
link.style.height = `${maxCardHeight}px`
109+
body.style.minHeight = `${maxCardHeight}px`
110+
card.style.setProperty('--slide', `-${imgHeight}px`)
111+
})
112+
}
113+
114+
initPostCards()
115+
window.addEventListener('resize', initPostCards)
116+
document.addEventListener('astro:after-swap', initPostCards)
117+
</script>

0 commit comments

Comments
 (0)