Improved posts UX
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m32s

This commit is contained in:
2026-02-03 11:00:13 -07:00
parent 63282cf34d
commit ba1193896f
2 changed files with 83 additions and 247 deletions

View File

@@ -22,140 +22,55 @@ function formatDate(date: Date): string {
---
<Layout>
<div class="w-full p-4 sm:p-8">
<div class="w-full max-w-3xl mx-auto p-4 sm:p-8">
<h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
>
Posts
</h1>
{/* Mobile: One-sided compact timeline */}
<ul
class="timeline timeline-vertical timeline-compact timeline-snap-icon max-w-3xl mx-auto px-4 md:hidden"
>
{
sortedPosts.map((post, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon
name="mdi:circle"
class="w-4 h-4 text-primary"
/>
</div>
<div class="timeline-end mb-8 ml-4">
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
<time class="font-mono text-sm opacity-60">
{formatDate(new Date(post.data.pubDate))}
</time>
<a
href={`/post/${post.id}`}
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{post.data.title}
</h3>
<p class="text-sm opacity-80 mt-1">
{post.data.description ||
"No description available."}
</p>
</a>
{post.data.tags &&
post.data.tags.length > 0 && (
<div class="flex gap-1 flex-wrap mt-2">
{post.data.tags
.slice(0, 3)
.map((tag: string) => (
<span class="badge badge-sm badge-outline">
{tag}
</span>
))}
</div>
)}
</div>
</div>
{index < sortedPosts.length - 1 && (
<hr class="bg-primary" />
)}
</li>
))
}
</ul>
{/* Desktop: Dual-sided alternating timeline */}
<ul
class="timeline timeline-vertical timeline-snap-icon max-w-3xl mx-auto px-4 hidden md:block"
>
{
sortedPosts.map((post, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon
name="mdi:circle"
class="w-4 h-4 text-primary"
/>
</div>
<div
class={`timeline-${index % 2 === 0 ? "start" : "end"} text-${index % 2 === 0 ? "end" : "start"} mb-8 mx-4`}
>
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
<time class="font-mono text-sm opacity-60">
{formatDate(new Date(post.data.pubDate))}
</time>
<a
href={`/post/${post.id}`}
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{post.data.title}
</h3>
<p class="text-sm opacity-80 mt-1">
{post.data.description ||
"No description available."}
</p>
</a>
{post.data.tags &&
post.data.tags.length > 0 && (
<div
class={`flex gap-1 flex-wrap mt-2 ${index % 2 === 0 ? "justify-end" : "justify-start"}`}
>
{post.data.tags
.slice(0, 3)
.map((tag: string) => (
<span class="badge badge-sm badge-outline">
{tag}
</span>
))}
</div>
)}
</div>
</div>
{index < sortedPosts.length - 1 && (
<hr class="bg-primary" />
)}
</li>
))
}
</ul>
{
sortedPosts.length === 0 && (
sortedPosts.length === 0 ? (
<p class="text-center text-gray-500 mt-12">
No posts available yet. Check back soon!
</p>
) : (
<ul class="flex flex-col bg-base-100 rounded-box shadow-md border border-base-content/20 divide-y divide-base-content/20">
{sortedPosts.map((post) => (
<li class="flex items-center hover:bg-base-200/50 transition-colors p-4 group relative rounded-none first:rounded-t-box last:rounded-b-box">
<a href={`/post/${post.id}`} class="absolute inset-0 z-0" aria-label={`Read ${post.data.title}`}></a>
<div class="w-24 sm:w-32 flex-none opacity-80 flex flex-col justify-center text-right pr-4 border-r border-base-content/20 z-10 pointer-events-none">
<span class="font-mono text-sm sm:text-base font-bold">
{post.data.pubDate.toLocaleDateString("en-us", { month: "short", day: "numeric" })}
</span>
<span class="text-xs opacity-70">
{post.data.pubDate.getFullYear()}
</span>
</div>
<div class="flex-grow flex flex-col gap-1 justify-center pl-4 z-10 pointer-events-none">
<h2
class="font-bold text-lg sm:text-xl text-primary group-hover:text-accent transition-colors"
>
{post.data.title}
</h2>
<p class="text-sm opacity-80 line-clamp-2 leading-relaxed">
{post.data.description || "No description available."}
</p>
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex gap-2 mt-1">
{post.data.tags.slice(0, 3).map((tag: string) => (
<span class="badge badge-xs badge-outline opacity-70">
{tag}
</span>
))}
</div>
)}
</div>
</li>
))}
</ul>
)
}
</div>

View File

@@ -19,139 +19,60 @@ function formatDate(dateStr: string): string {
---
<Layout>
<div class="w-full p-4 sm:p-8">
<div class="w-full max-w-3xl mx-auto p-4 sm:p-8">
<h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
>
Talks
</h1>
{/* Single talk: Simple centered card without timeline */}
{sortedTalks.length === 1 && (
<div class="max-w-xl mx-auto px-4">
<div class="border border-base-content/20 rounded-box p-6 bg-base-200 hover:border-primary transition-colors">
{sortedTalks[0].date && (
<time class="font-mono text-sm opacity-60">
{formatDate(sortedTalks[0].date)}
</time>
)}
<a
href={sortedTalks[0].link}
target="_blank"
rel="noopener noreferrer"
class="block group"
>
<h3 class="text-xl font-bold text-primary group-hover:text-accent transition-colors">
{sortedTalks[0].name}
</h3>
<p class="opacity-80 mt-2">
{sortedTalks[0].description}
</p>
<span class="inline-flex items-center gap-1 text-sm text-primary mt-3 group-hover:text-accent transition-colors">
<Icon name="mdi:open-in-new" class="w-4 h-4" />
View talk
</span>
</a>
</div>
</div>
)}
{/* Multiple talks: Mobile one-sided compact timeline */}
{sortedTalks.length > 1 && (
<ul class="timeline timeline-vertical timeline-compact timeline-snap-icon max-w-3xl mx-auto px-4 md:hidden">
{
sortedTalks.map((talk, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon name="mdi:circle" class="w-4 h-4 text-primary" />
</div>
<div class="timeline-end mb-8 ml-4">
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
{talk.date && (
<time class="font-mono text-sm opacity-60">
{formatDate(talk.date)}
</time>
)}
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{talk.name}
</h3>
<p class="text-sm opacity-80 mt-1">
{talk.description}
</p>
</a>
</div>
</div>
{index < sortedTalks.length - 1 && <hr class="bg-primary" />}
</li>
))
}
</ul>
)}
{/* Multiple talks: Desktop dual-sided alternating timeline */}
{sortedTalks.length > 1 && (
<ul class="timeline timeline-vertical timeline-snap-icon max-w-3xl mx-auto px-4 hidden md:block">
{
sortedTalks.map((talk, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon name="mdi:circle" class="w-4 h-4 text-primary" />
</div>
<div class={`timeline-${index % 2 === 0 ? 'start' : 'end'} text-${index % 2 === 0 ? 'end' : 'start'} mb-8 mx-4`}>
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
{talk.date && (
<time class="font-mono text-sm opacity-60">
{formatDate(talk.date)}
</time>
)}
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{talk.name}
</h3>
<p class="text-sm opacity-80 mt-1">
{talk.description}
</p>
</a>
</div>
</div>
{index < sortedTalks.length - 1 && <hr class="bg-primary" />}
</li>
))
}
</ul>
)}
{
sortedTalks.length === 0 && (
sortedTalks.length === 0 ? (
<p class="text-center text-gray-500 mt-12">
No talks available yet. Check back soon!
</p>
) : (
<ul class="flex flex-col bg-base-100 rounded-box shadow-md border border-base-content/20 divide-y divide-base-content/20">
{sortedTalks.map((talk) => {
const talkDate = talk.date ? new Date(talk.date) : null;
return (
<li class="flex items-center hover:bg-base-200/50 transition-colors p-4 group relative rounded-none first:rounded-t-box last:rounded-b-box">
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="absolute inset-0 z-0"
aria-label={`View ${talk.name}`}
></a>
<div class="w-24 sm:w-32 flex-none opacity-80 flex flex-col justify-center text-right pr-4 border-r border-base-content/20 z-10 pointer-events-none">
{talkDate ? (
<>
<span class="font-mono text-sm sm:text-base font-bold">
{talkDate.toLocaleDateString("en-us", { month: "short", day: "numeric" })}
</span>
<span class="text-xs opacity-70">
{talkDate.getFullYear()}
</span>
</>
) : (
<span class="text-xs opacity-50 italic">Undated</span>
)}
</div>
<div class="flex-grow flex flex-col gap-1 justify-center pl-4 z-10 pointer-events-none">
<h2 class="font-bold text-lg sm:text-xl text-primary group-hover:text-accent transition-colors flex items-center gap-2">
{talk.name}
<Icon name="mdi:open-in-new" class="w-4 h-4 opacity-50 group-hover:opacity-100 transition-opacity" />
</h2>
<p class="text-sm opacity-80 line-clamp-2 leading-relaxed">
{talk.description}
</p>
</div>
</li>
);
})}
</ul>
)
}
</div>