Updated UX to remove the weird looking cards. No more cards!
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m47s

This commit is contained in:
2025-12-15 10:28:48 -07:00
parent 4ab28078e8
commit 203f83bfcb
8 changed files with 324 additions and 353 deletions

View File

@@ -47,6 +47,9 @@ export default defineConfig({
"clock-outline", "clock-outline",
"apple", "apple",
"google-play", "google-play",
"code-braces",
"circle",
"open-in-new",
], ],
"simple-icons": [ "simple-icons": [
"gitea", "gitea",

View File

@@ -1,65 +0,0 @@
---
import type { CollectionEntry } from "astro:content";
import { Icon } from "astro-icon/components";
export interface Props {
post: CollectionEntry<"posts">;
}
const { post } = Astro.props;
const { title, description: blurb, pubDate } = post.data;
const { slug } = post;
---
<div
class="card bg-accent text-accent-content w-full shadow-lg hover:shadow-xl transition-shadow mb-4 sm:mb-6"
>
<div class="card-body wrap-break-word">
<h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center wrap-break-word"
>
{title}
</h2>
<p class="text-center wrap-break-word my-4">
{blurb || "No description available."}
</p>
<div
class="flex flex-wrap items-center justify-center opacity-75 gap-2 text-sm mb-4"
>
<Icon name="mdi:clock" class="text-xl" />
<span>
{
new Date(pubDate).toLocaleDateString("en-us", {
month: "long",
day: "numeric",
year: "numeric",
})
}
</span>
</div>
{
post.data.tags && post.data.tags.length > 0 && (
<div class="flex gap-2 flex-wrap mb-4 justify-center">
{post.data.tags.map((tag: string) => (
<div class="badge badge-primary font-bold">
<Icon name="mdi:tag" class="text-lg" />
{tag}
</div>
))}
</div>
)
}
<div class="card-actions justify-end">
<a
href={`/post/${slug}`}
class="btn btn-circle"
aria-label={`Read more about ${title}`}
>
<Icon name="mdi:arrow-right" class="text-lg" />
</a>
</div>
</div>
</div>

View File

@@ -1,137 +0,0 @@
---
import { Icon } from "astro-icon/components";
import type { Project } from "../types";
import { formatRelativeTime } from "../utils/gitea";
interface Props {
project: Project;
}
const { project } = Astro.props;
---
<div
class="card bg-accent text-accent-content w-full shadow-lg hover:shadow-xl transition-shadow mb-4 sm:mb-6"
>
<div class="card-body">
<h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center wrap-break-word"
>
{project.name}
</h2>
<p class="text-center wrap-break-word">
{project.description}
</p>
{
project.giteaInfo &&
(project.giteaInfo.languages.length > 0 ||
project.giteaInfo.updatedAt) && (
<>
<div class="divider my-2 before:bg-accent-content/30 after:bg-accent-content/30" />
<div class="flex flex-wrap gap-2 justify-center">
{project.giteaInfo.languages.map(
(language: string) => (
<div class="badge badge-sm gap-1 bg-accent-content/20 border-accent-content/30">
<Icon
name="mdi:code-tags"
class="w-3 h-3"
/>
{language}
</div>
),
)}
{project.giteaInfo.updatedAt && (
<div class="badge badge-sm gap-1 bg-accent-content/20 border-accent-content/30">
<Icon
name="mdi:clock-outline"
class="w-3 h-3"
/>
{formatRelativeTime(
project.giteaInfo.updatedAt,
)}
</div>
)}
</div>
</>
)
}
{
project.giteaInfo?.topics &&
project.giteaInfo.topics.length > 0 && (
<>
<div class="divider my-2 before:bg-accent-content/30 after:bg-accent-content/30" />
<div class="flex gap-2 flex-wrap justify-center">
{project.giteaInfo.topics.map((tag: string) => (
<div class="badge badge-sm badge-outline gap-1">
<Icon name="mdi:tag" class="w-3 h-3" />
{tag}
</div>
))}
</div>
</>
)
}
<div class="card-actions justify-center gap-2 mt-4">
{
project.webLink && (
<a
href={project.webLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm gap-2 bg-accent-content text-accent hover:bg-accent-content/90"
aria-label={`Visit ${project.name} website`}
>
<Icon name="mdi:web" class="w-4 h-4" />
Website
</a>
)
}
{
project.gitLink && (
<a
href={project.gitLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm gap-2 bg-accent-content text-accent hover:bg-accent-content/90"
aria-label={`View ${project.name} source code`}
>
<Icon name="simple-icons:gitea" class="w-4 h-4" />
Source
</a>
)
}
{
project.iosLink && (
<a
href={project.iosLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm gap-2 bg-accent-content text-accent hover:bg-accent-content/90"
aria-label={`Download ${project.name} on iOS App Store`}
>
<Icon name="mdi:apple" class="w-4 h-4" />
iOS
</a>
)
}
{
project.androidLink && (
<a
href={project.androidLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm gap-2 bg-accent-content text-accent hover:bg-accent-content/90"
aria-label={`Download ${project.name} on Google Play Store`}
>
<Icon name="mdi:google-play" class="w-4 h-4" />
Android
</a>
)
}
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
--- ---
import ProjectCard from "./ProjectCard.astro"; import { Icon } from "astro-icon/components";
import { config } from "../config"; import { config } from "../config";
import { fetchGiteaInfoFromUrl } from "../utils/gitea"; import { fetchGiteaInfoFromUrl, formatRelativeTime } from "../utils/gitea";
import type { Project } from "../types"; import type { Project } from "../types";
Astro.response.headers.set( Astro.response.headers.set(
@@ -48,27 +48,92 @@ const sortedProjects = projectsWithGiteaInfo.sort((a, b) => {
}); });
--- ---
<div <ul class="list bg-base-200 rounded-box max-w-4xl mx-auto">
class:list={[
sortedProjects.length <= 2
? "flex flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto px-4"
: "columns-1 sm:columns-2 lg:columns-3 gap-4 sm:gap-6 max-w-6xl mx-auto px-4",
]}
>
{ {
sortedProjects.map((project) => ( sortedProjects.map((project) => (
<div <li class="list-row hover:bg-base-300 transition-colors">
class:list={[ {/* Project icon/avatar */}
sortedProjects.length <= 2 <div class="list-col-grow-0">
? "w-full sm:w-96" <div class="w-12 h-12 rounded-lg bg-accent flex items-center justify-center">
: "inline-block break-inside-avoid w-full", <Icon name="mdi:code-braces" class="w-6 h-6 text-accent-content" />
]} </div>
> </div>
<ProjectCard project={project} />
</div> {/* Main content */}
<div class="list-col-grow">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
<h3 class="font-bold text-lg">{project.name}</h3>
{project.giteaInfo?.updatedAt && (
<span class="text-xs opacity-60">
{formatRelativeTime(project.giteaInfo.updatedAt)}
</span>
)}
</div>
<p class="text-sm opacity-80 mt-1">{project.description}</p>
{/* Languages & Topics */}
<div class="flex flex-wrap gap-1 mt-2">
{project.giteaInfo?.languages?.slice(0, 3).map((lang: string) => (
<span class="badge badge-sm badge-primary">{lang}</span>
))}
{project.giteaInfo?.topics?.slice(0, 4).map((topic: string) => (
<span class="badge badge-sm badge-outline">{topic}</span>
))}
</div>
</div>
{/* Action buttons */}
<div class="list-col-grow-0 flex flex-wrap gap-1 justify-end">
{project.webLink && (
<a
href={project.webLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-primary hover:bg-primary hover:text-primary-content transition-all"
aria-label={`Visit ${project.name} website`}
>
<Icon name="mdi:web" class="w-5 h-5" />
</a>
)}
{project.gitLink && (
<a
href={project.gitLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-secondary hover:bg-secondary hover:text-secondary-content transition-all"
aria-label={`View ${project.name} source`}
>
<Icon name="simple-icons:gitea" class="w-5 h-5" />
</a>
)}
{project.iosLink && (
<a
href={project.iosLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-accent hover:bg-accent hover:text-accent-content transition-all"
aria-label={`${project.name} on iOS`}
>
<Icon name="mdi:apple" class="w-5 h-5" />
</a>
)}
{project.androidLink && (
<a
href={project.androidLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-success hover:bg-success hover:text-success-content transition-all"
aria-label={`${project.name} on Android`}
>
<Icon name="mdi:google-play" class="w-5 h-5" />
</a>
)}
</div>
</li>
)) ))
} }
</div> </ul>
{ {
sortedProjects.length === 0 && ( sortedProjects.length === 0 && (

View File

@@ -1,46 +1,7 @@
--- ---
const skeletonCount = 6;
---
<div ---
class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto"
>
{
Array.from({ length: skeletonCount }).map((_, i) => (
<div class="card bg-accent text-accent-content w-full max-w-sm shrink shadow-lg">
<div class="card-body">
<div class="skeleton h-8 w-3/4 mx-auto mb-4" />
<div class="space-y-2 mb-4"> <div class="flex justify-center items-center py-12">
<div class="skeleton h-4 w-full" /> <span class="loading loading-spinner loading-lg text-primary"></span>
<div class="skeleton h-4 w-5/6 mx-auto" />
</div>
<div class="divider my-2 before:bg-accent-content/30 after:bg-accent-content/30" />
<div class="flex flex-wrap gap-3 justify-center">
<div class="skeleton h-5 w-24" />
<div class="skeleton h-5 w-24" />
</div>
<div class="flex flex-wrap gap-2 justify-center mt-2">
<div class="skeleton h-5 w-16 rounded-full" />
<div class="skeleton h-5 w-20 rounded-full" />
</div>
<div class="divider my-2 before:bg-accent-content/30 after:bg-accent-content/30" />
<div class="flex gap-2 flex-wrap justify-center">
<div class="skeleton h-5 w-16 rounded-full" />
<div class="skeleton h-5 w-20 rounded-full" />
<div class="skeleton h-5 w-14 rounded-full" />
</div>
<div class="card-actions justify-center gap-2 mt-4">
<div class="skeleton h-8 w-20" />
</div>
</div>
</div>
))
}
</div> </div>

View File

@@ -1,49 +0,0 @@
---
import { Icon } from "astro-icon/components";
import type { Talk } from "../types";
interface Props {
talk: Talk;
}
const { talk } = Astro.props;
---
<div
class="card bg-accent text-accent-content w-full shadow-lg hover:shadow-xl transition-shadow mb-4 sm:mb-6"
>
<div class="card-body p-6 wrap-break-word">
<h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center wrap-break-word"
>
{talk.name}
</h2>
<p class="text-center wrap-break-word my-4">
{talk.description}
</p>
<div class="flex flex-col gap-2 mb-4 text-sm">
{
talk.date && (
<div class="flex items-center gap-2">
<span class="font-semibold">Date:</span>
<span>{talk.date}</span>
</div>
)
}
</div>
<div class="card-actions justify-end mt-4">
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle"
aria-label={`Visit ${talk.name}`}
>
<Icon name="mdi:link" class="text-lg" />
</a>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
--- ---
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import { getCollection, type CollectionEntry } from "astro:content"; import { getCollection, type CollectionEntry } from "astro:content";
import PostCard from "../components/PostCard.astro"; import { Icon } from "astro-icon/components";
// Get all posts from the content collection // Get all posts from the content collection
const posts = await getCollection("posts"); const posts = await getCollection("posts");
@@ -11,6 +11,14 @@ const sortedPosts = posts.sort(
(a: CollectionEntry<"posts">, b: CollectionEntry<"posts">) => (a: CollectionEntry<"posts">, b: CollectionEntry<"posts">) =>
new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf(), new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf(),
); );
function formatDate(date: Date): string {
return date.toLocaleDateString("en-us", {
month: "short",
day: "numeric",
year: "numeric",
});
}
--- ---
<Layout> <Layout>
@@ -20,27 +28,98 @@ const sortedPosts = posts.sort(
> >
Posts Posts
</h1> </h1>
<div
class:list={[ {/* Mobile: One-sided compact timeline */}
sortedPosts.length <= 2 <ul class="timeline timeline-vertical timeline-compact timeline-snap-icon max-w-3xl mx-auto px-4 md:hidden">
? "flex flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto px-4"
: "columns-1 sm:columns-2 lg:columns-3 gap-4 sm:gap-6 max-w-6xl mx-auto px-4",
]}
>
{ {
sortedPosts.map((post) => ( sortedPosts.map((post, index) => (
<div <li>
class:list={[ {index > 0 && <hr class="bg-primary" />}
sortedPosts.length <= 2
? "w-full sm:w-96" <div class="timeline-middle">
: "inline-block break-inside-avoid w-full", <Icon name="mdi:circle" class="w-4 h-4 text-primary" />
]} </div>
>
<PostCard post={post} /> <div class="timeline-end mb-8 ml-4">
</div> <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.slug}`}
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>
)) ))
} }
</div> </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.slug}`}
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 && (

View File

@@ -1,7 +1,21 @@
--- ---
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import TalkCard from "../components/TalkCard.astro"; import { Icon } from "astro-icon/components";
import { config } from "../config"; import { config } from "../config";
// Sort talks by date, newest first
const sortedTalks = [...config.talks].sort((a, b) => {
if (!a.date || !b.date) return 0;
return new Date(b.date).valueOf() - new Date(a.date).valueOf();
});
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString("en-us", {
month: "short",
day: "numeric",
year: "numeric",
});
}
--- ---
<Layout> <Layout>
@@ -11,30 +25,130 @@ import { config } from "../config";
> >
Talks Talks
</h1> </h1>
<div
class:list={[ {/* Single talk: Simple centered card without timeline */}
config.talks.length <= 2 {sortedTalks.length === 1 && (
? "flex flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto px-4" <div class="max-w-xl mx-auto px-4">
: "columns-1 sm:columns-2 lg:columns-3 gap-4 sm:gap-6 max-w-6xl 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)}
config.talks.map((talk) => ( </time>
<div )}
class:list={[
config.talks.length <= 2 <a
? "w-full sm:w-96" href={sortedTalks[0].link}
: "inline-block break-inside-avoid w-full", target="_blank"
]} rel="noopener noreferrer"
class="block group"
> >
<TalkCard talk={talk} /> <h3 class="text-xl font-bold text-primary group-hover:text-accent transition-colors">
</div> {sortedTalks[0].name}
)) </h3>
}
</div> <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>
)}
{ {
config.talks.length === 0 && ( sortedTalks.length === 0 && (
<p class="text-center text-gray-500 mt-12"> <p class="text-center text-gray-500 mt-12">
No talks available yet. Check back soon! No talks available yet. Check back soon!
</p> </p>