Skip to content

Commit 1b253b2

Browse files
Kai-rosjonathanyeong
authored andcommitted
feat: implemented BlogPostListCard component for the list page
1 parent 81868e0 commit 1b253b2

11 files changed

Lines changed: 282 additions & 12 deletions

File tree

TODOs.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# TODOs
2+
3+
<!-- TODO: A todo within the TODOs!!! This is a temporary file to track work since there are so many of us mucking about on this! REMOVE BEFORE FINALIZING THE PR!!!! -->
4+
5+
---
6+
7+
## Current
8+
9+
- Blog list UI - fix posts width
10+
- Blog post UI
11+
- OAuth
12+
- Standard site push - Mock PDS push for now
13+
- anthony's alternative markdown solution for now and Nuxt content integration for later
14+
- automatic pulls or pushes from an action runner
15+
- constellation - bsky API
16+
- records walkers
17+
- bsky posts stuff - Bluesky comments
18+
- How does i18n deal with dynamic values? $t('blog.post.title'),
19+
- TBD
20+
21+
---
22+
23+
## Completed
24+
25+
- [x] Lexicons
26+
- [x] x
27+
- [x] x
28+
29+
---

app/components/BlogPost.server.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const props = defineProps<{ title: string; htmlContent: string; date: string }>(
66
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-2">
77
{{ title }}
88
</h1>
9-
<!-- <time class="text-sm text-gray-500 dark:text-gray-400"> -->
109

1110
<DateTime
1211
:datetime="date"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
/** First and last name - Potentially Multiple? i.e. co-authors */
4+
author: string
5+
/** Blog Title */
6+
title: string
7+
/** Tags such as OpenSource, Architecture, Community, etc. */
8+
topics: string[]
9+
/** Brief line from the text. */
10+
excerpt: string
11+
/** The datetime value (ISO string or Date) */
12+
published: string
13+
/** Path/Slug of the post */
14+
path: string
15+
/** For keyboard nav scaffold */
16+
index: number
17+
}>()
18+
19+
const emit = defineEmits<{
20+
focus: [index: number]
21+
}>()
22+
</script>
23+
24+
<template>
25+
<!-- TODO: Width is currently being constrained -->
26+
<article
27+
class="group card-interactive relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50"
28+
>
29+
<NuxtLink
30+
:to="`/blog/${path}`"
31+
:data-suggestion-index="index"
32+
class="flex items-center gap-4 focus-visible:outline-none after:content-[''] after:absolute after:inset-0"
33+
@focus="index != null && emit('focus', index)"
34+
@mouseenter="index != null && emit('focus', index)"
35+
>
36+
<!-- Avatar placeholder -->
37+
<div
38+
class="w-10 h-10 shrink-0 flex items-center justify-center border border-border rounded-full bg-bg-muted"
39+
aria-hidden="true"
40+
>
41+
<span class="text-lg text-fg-subtle font-mono">
42+
{{
43+
author
44+
.split(' ')
45+
.map(n => n[0])
46+
.join('')
47+
.toUpperCase()
48+
}}
49+
</span>
50+
</div>
51+
52+
<!-- Text Content -->
53+
<div class="flex-1 min-w-0 text-left">
54+
<h2
55+
class="font-mono text-base font-medium text-fg group-hover:text-primary transition-colors hover:underline"
56+
>
57+
{{ title }}
58+
</h2>
59+
60+
<div class="flex items-center gap-2 text-xs text-fg-muted font-mono">
61+
<span>{{ author }}</span>
62+
<span>•</span>
63+
<span>{{ published }}</span>
64+
</div>
65+
66+
<p v-if="excerpt" class="text-sm text-muted-foreground mt-2 line-clamp-2 no-underline">
67+
{{ excerpt }}
68+
</p>
69+
</div>
70+
71+
<span
72+
class="i-carbon-arrow-right w-4 h-4 text-fg-subtle group-hover:text-fg transition-colors shrink-0"
73+
aria-hidden="true"
74+
/>
75+
</NuxtLink>
76+
</article>
77+
</template>
78+
<!-- :class="{
79+
'bg-bg-muted border-border-hover': selected,
80+
'border-accent/30 bg-accent/5': isExactMatch,
81+
}" -->

app/pages/blog/index.vue

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,90 @@ const { data: posts } = await useAsyncData('blog-posts', () =>
33
queryCollection('blog').where('draft', '<>', true).order('date', 'DESC').all(),
44
)
55
6+
const placeHolder = ['atproto', 'atproto']
7+
8+
// TODO: This should be extracted into a reusable form so search and blog post can both use it
9+
// function scrollToSelectedItem() {
10+
// const pkgIndex = toPackageIndex(unifiedSelectedIndex.value)
11+
// if (pkgIndex !== null) {
12+
// packageListRef.value?.scrollToIndex(pkgIndex)
13+
// }
14+
// }
15+
16+
// function focusSelectedItem() {
17+
// const suggIdx = toSuggestionIndex(unifiedSelectedIndex.value)
18+
// const pkgIdx = toPackageIndex(unifiedSelectedIndex.value)
19+
20+
// nextTick(() => {
21+
// if (suggIdx !== null) {
22+
// const el = document.querySelector<HTMLElement>(`[data-suggestion-index="${suggIdx}"]`)
23+
// el?.focus()
24+
// } else if (pkgIdx !== null) {
25+
// scrollToSelectedItem()
26+
// nextTick(() => {
27+
// const el = document.querySelector<HTMLElement>(`[data-result-index="${pkgIdx}"]`)
28+
// el?.focus()
29+
// })
30+
// }
31+
// })
32+
// }
33+
34+
// function handleResultsKeydown(e: KeyboardEvent) {
35+
// if (totalSelectableCount.value <= 0) return
36+
37+
// const isFromInput = (e.target as HTMLElement).tagName === 'INPUT'
38+
39+
// if (e.key === 'ArrowDown') {
40+
// e.preventDefault()
41+
// userHasNavigated.value = true
42+
// unifiedSelectedIndex.value = clampUnifiedIndex(unifiedSelectedIndex.value + 1)
43+
// if (isFromInput) {
44+
// scrollToSelectedItem()
45+
// } else {
46+
// focusSelectedItem()
47+
// }
48+
// return
49+
// }
50+
51+
// if (e.key === 'ArrowUp') {
52+
// e.preventDefault()
53+
// userHasNavigated.value = true
54+
// unifiedSelectedIndex.value = clampUnifiedIndex(unifiedSelectedIndex.value - 1)
55+
// if (isFromInput) {
56+
// scrollToSelectedItem()
57+
// } else {
58+
// focusSelectedItem()
59+
// }
60+
// return
61+
// }
62+
63+
// if (e.key === 'Enter') {
64+
// if (!resultsMatchQuery.value) return
65+
66+
// const suggIdx = toSuggestionIndex(unifiedSelectedIndex.value)
67+
// const pkgIdx = toPackageIndex(unifiedSelectedIndex.value)
68+
69+
// if (suggIdx !== null) {
70+
// const el = document.querySelector<HTMLElement>(`[data-suggestion-index="${suggIdx}"]`)
71+
// if (el) {
72+
// e.preventDefault()
73+
// el.click()
74+
// }
75+
// } else if (pkgIdx !== null) {
76+
// const el = document.querySelector<HTMLElement>(`[data-result-index="${pkgIdx}"]`)
77+
// if (el) {
78+
// e.preventDefault()
79+
// el.click()
80+
// }
81+
// }
82+
// }
83+
// }
84+
85+
// function handleBlogPostSelect(index: number) {
86+
// // Convert suggestion index to unified index
87+
// unifiedSelectedIndex.value = -(suggestionCount.value - index)
88+
// }
89+
690
definePageMeta({
791
name: 'blog',
892
// alias: ['/:path(.*)*'],
@@ -16,18 +100,30 @@ useSeoMeta({
16100

17101
<template>
18102
<main class="container py-8 sm:py-12 w-full">
19-
<header class="mb-8 pb-8 border-b border-border">
20-
<div class="">I AM A MIGHTY HEADER</div>
103+
<header class="mb-8 pb-8 border-b border-border flex place-content-center text-3xl">
104+
<div class="">blog...</div>
21105
</header>
22106

23107
<article class="flex flex-col gap-6">
24-
<div v-for="post in posts" :key="post.slug" class="p-4 border border-border rounded-lg">
25-
<h2 class="text-xl font-semibold">
26-
<NuxtLink :to="`/blog/${post.slug}`" class="text-primary hover:underline">
27-
{{ post.title }}
28-
</NuxtLink>
29-
</h2>
30-
<p class="text-muted-foreground">{{ post.excerpt }}</p>
108+
<div v-if="posts && posts.length > 0" class="max-w-3xl mx-auto mb-6 space-y-3">
109+
<BlogPostListCard
110+
v-for="(post, idx) in posts"
111+
:key="`${post.author}-${post.title}`"
112+
:author="post.author || 'Roe'"
113+
:title="post.title"
114+
:path="post.slug"
115+
:excerpt="post.excerpt || post.description || 'No Excerpt Available'"
116+
:topics="Array.isArray(post.tags) ? post.tags : placeHolder"
117+
:published="post.date"
118+
:index="idx"
119+
@focus="i => console.log('Hovered:', i)"
120+
/>
121+
<!-- :selected="toSuggestionIndex(unifiedSelectedIndex) === idx" -->
122+
<!-- :is-exact-match="
123+
(exactMatchType === 'org' && suggestion.type === 'org') ||
124+
(exactMatchType === 'user' && suggestion.type === 'user')
125+
" -->
126+
<!-- @focus="handleBlogPostSelect" -->
31127
</div>
32128
</article>
33129
</main>

content/blog/atproto.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
author: 'Daniel Roe'
3+
title: 'ATProto'
4+
tags: ['OpenSource', 'Nuxt']
5+
excerpt: 'ATProto is very cool'
6+
date: '2026-01-28'
7+
slug: 'atproto'
8+
description: 'ATProto Adjacency Agenda'
9+
draft: false
10+
---
11+
12+
# Atmosphere Apps
13+
14+
All the cool kids are doing Software Decentralization.

content/blog/first-post.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
---
2+
author: 'Daniel Roe'
23
title: 'Hello World'
4+
tags: ['OpenSource', 'Nuxt']
5+
excerpt: 'My first post'
36
date: '2026-01-28'
47
slug: 'first-post'
58
description: 'My first post on the blog'
6-
excerpt: 'My first post'
79
draft: false
810
---
911

content/blog/nuxt.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
author: 'Daniel Roe'
3+
title: 'Nuxted'
4+
tags: ['OpenSource', 'Nuxt']
5+
excerpt: 'Nuxting'
6+
date: '2026-01-28'
7+
slug: 'nuxt'
8+
description: 'Nuxter'
9+
draft: false
10+
---
11+
12+
# Nuxt
13+
14+
What a great meta-framework!!

content/blog/open-source.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
author: 'Daniel Roe'
3+
title: 'OSS'
4+
tags: ['OpenSource', 'Nuxt']
5+
excerpt: 'OSS Things'
6+
date: '2026-01-28'
7+
slug: 'open-source'
8+
description: 'Talking about Open Source Software'
9+
draft: false
10+
---
11+
12+
# OSS
13+
14+
This is about Open Source Software.

content/blog/package-registries.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
author: 'Daniel Roe'
3+
title: 'Package Registries'
4+
tags: ['OpenSource', 'Nuxt']
5+
excerpt: 'Package Registries need fixing'
6+
date: '2026-01-28'
7+
slug: 'package-registries'
8+
description: 'Package Registries Reimagined'
9+
draft: false
10+
---
11+
12+
# Package Registries
13+
14+
Shortest explanation: Production grade JavaScript is weird.

shared/schemas/blog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { object, string, boolean, array, optional } from 'valibot'
22
import type { InferOutput } from 'valibot'
33

44
export const BlogPostSchema = object({
5+
author: string(),
56
title: string(),
67
date: string(),
78
description: string(),
89
slug: string(),
910
excerpt: optional(string()),
10-
author: optional(string()),
1111
tags: optional(array(string())),
1212
draft: optional(boolean()),
1313
})

0 commit comments

Comments
 (0)