Updated UX to remove the weird looking cards. No more cards!
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m47s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m47s
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user