Skip to main content

dioxus_docs_kit/components/blog/
blog_card.rs

1use dioxus::prelude::*;
2use dioxus_free_icons::Icon;
3use dioxus_free_icons::icons::ld_icons::LdClock;
4
5use crate::BlogContext;
6use crate::blog::registry::BlogRegistry;
7use crate::blog::types::BlogPost;
8
9/// Individual blog post card for the listing page.
10#[component]
11pub fn BlogCard(post: BlogPost) -> Element {
12    let ctx = use_context::<BlogContext>();
13    let registry = use_context::<&'static BlogRegistry>();
14    let slug = post.slug.clone();
15    let href = format!("{}/{}", ctx.base_path, slug);
16    let author = registry.get_author(&post.frontmatter.author);
17    let date_display = registry.format_date(&post.frontmatter.date);
18
19    rsx! {
20        article {
21            class: "group flex flex-col rounded-xl border border-base-300 bg-base-200/30 hover:border-primary/30 hover:shadow-lg transition-all duration-200 overflow-hidden",
22
23            if let Some(ref cover) = post.frontmatter.cover_image {
24                Link { to: NavigationTarget::Internal(href.clone()), class: "aspect-video overflow-hidden bg-base-300",
25                    img {
26                        src: "{cover}",
27                        alt: "{post.frontmatter.title}",
28                        class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300",
29                    }
30                }
31            }
32
33            div { class: "flex flex-col flex-1 p-5 gap-3",
34                if !post.frontmatter.tags.is_empty() {
35                    div { class: "flex flex-wrap gap-1.5",
36                        for tag in post.frontmatter.tags.iter().take(3) {
37                            if let Some(url) = registry.category_url_for_tag(tag) {
38                                Link { to: NavigationTarget::Internal(url), class: "badge badge-sm badge-outline hover:badge-primary", "{tag}" }
39                            } else {
40                                span { class: "badge badge-sm badge-outline", "{tag}" }
41                            }
42                        }
43                        if post.frontmatter.tags.len() > 3 {
44                            span { class: "badge badge-sm badge-ghost",
45                                "+{post.frontmatter.tags.len() - 3}"
46                            }
47                        }
48                    }
49                }
50
51                h2 { class: "text-lg font-semibold leading-snug group-hover:text-primary transition-colors line-clamp-2",
52                    Link { to: NavigationTarget::Internal(href.clone()), "{post.frontmatter.title}" }
53                }
54
55                if let Some(ref desc) = post.frontmatter.description {
56                    p { class: "text-sm text-base-content/60 leading-relaxed line-clamp-3",
57                        "{desc}"
58                    }
59                }
60
61                div { class: "mt-auto pt-3 border-t border-base-300/60 flex items-center gap-3 text-xs text-base-content/50",
62                    if let Some(author) = author {
63                        div { class: "flex items-center gap-1.5",
64                            if let Some(ref avatar) = author.avatar {
65                                img {
66                                    src: "{avatar}",
67                                    alt: "{author.name}",
68                                    class: "size-5 rounded-full",
69                                }
70                            }
71                            span { "{author.name}" }
72                        }
73                    }
74                    span { "{date_display}" }
75                    div { class: "flex items-center gap-1 ml-auto",
76                        Icon { class: "size-3", icon: LdClock }
77                        span { "{post.reading_time_minutes} min read" }
78                    }
79                }
80            }
81        }
82    }
83}