Skip to main content

dioxus_docs_kit/components/blog/
blog_list.rs

1use dioxus::prelude::*;
2use dioxus_free_icons::Icon;
3use dioxus_free_icons::icons::ld_icons::{LdChevronLeft, LdChevronRight};
4
5use crate::BlogContext;
6use crate::blog::registry::BlogRegistry;
7
8use super::blog_card::BlogCard;
9use super::blog_meta::BlogIndexMeta;
10use super::tag_filter::TagFilter;
11
12/// Blog listing page with cards grid, tag filter, and pagination.
13#[component]
14pub fn BlogList(hero: Option<Element>) -> Element {
15    let registry = use_context::<&'static BlogRegistry>();
16    let ctx = use_context::<BlogContext>();
17    let active_tag = use_context::<Signal<Option<String>>>();
18    let mut current_page = use_context::<Signal<usize>>();
19
20    let posts = use_memo(move || {
21        let tag = active_tag();
22        let page = current_page();
23        match tag.as_deref() {
24            Some(tag) => registry
25                .posts_page_by_tag(tag, page)
26                .into_iter()
27                .cloned()
28                .collect::<Vec<_>>(),
29            None => registry
30                .non_featured_posts_page(page)
31                .into_iter()
32                .cloned()
33                .collect::<Vec<_>>(),
34        }
35    });
36
37    let total_pages = use_memo(move || {
38        let tag = active_tag();
39        match tag.as_deref() {
40            Some(tag) => registry.total_pages_for_tag(tag),
41            None => registry.non_featured_total_pages(),
42        }
43    });
44
45    rsx! {
46        div { class: "max-w-6xl mx-auto px-4 py-12",
47            if let Some(ref site_url) = ctx.site_url {
48                {
49                    let active_tag = active_tag();
50                    let (title, description) = match active_tag.as_deref() {
51                        Some(tag) => (
52                            format!("Blog: {tag}"),
53                            format!("Browse blog posts tagged {tag}."),
54                        ),
55                        None => (
56                            "Blog".to_string(),
57                            "Latest blog posts and updates.".to_string(),
58                        ),
59                    };
60                    rsx! {
61                        BlogIndexMeta {
62                            title,
63                            description,
64                            site_url: site_url.clone(),
65                        }
66                    }
67                }
68            }
69            if let Some(hero) = hero {
70                {hero}
71            }
72
73            if !registry.all_tags().is_empty() {
74                div { class: "mb-8",
75                    TagFilter {}
76                }
77            }
78
79            // Featured posts section (only when no tag filter is active)
80            if active_tag().is_none() && registry.has_featured() {
81                div { class: "mb-10",
82                    h2 { class: "text-lg font-semibold mb-4 flex items-center gap-2",
83                        span { class: "badge badge-primary badge-sm", "Featured" }
84                    }
85                    div { class: "grid grid-cols-1 md:grid-cols-2 gap-6",
86                        for post in registry.featured_posts() {
87                            BlogCard { key: "{post.slug}", post: post.clone() }
88                        }
89                    }
90                }
91            }
92
93            if posts.read().is_empty() {
94                div { class: "text-center py-16 text-base-content/50",
95                    p { class: "text-lg", "No posts found." }
96                }
97            } else {
98                div { class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",
99                    for post in posts.read().iter() {
100                        BlogCard { key: "{post.slug}", post: post.clone() }
101                    }
102                }
103            }
104
105            if total_pages() > 1 {
106                nav { class: "flex items-center justify-center gap-2 mt-12",
107                    button {
108                        class: "btn btn-ghost btn-sm",
109                        disabled: current_page() == 0,
110                        onclick: move |_| {
111                            if current_page() > 0 {
112                                current_page -= 1;
113                            }
114                        },
115                        Icon { class: "size-4", icon: LdChevronLeft }
116                        "Prev"
117                    }
118                    for page in 0..total_pages() {
119                        {
120                            let is_active = page == current_page();
121                            let class = if is_active {
122                                "btn btn-sm btn-primary"
123                            } else {
124                                "btn btn-sm btn-ghost"
125                            };
126                            rsx! {
127                                button {
128                                    class: "{class}",
129                                    onclick: move |_| current_page.set(page),
130                                    "{page + 1}"
131                                }
132                            }
133                        }
134                    }
135                    button {
136                        class: "btn btn-ghost btn-sm",
137                        disabled: current_page() + 1 >= total_pages(),
138                        onclick: move |_| {
139                            if current_page() + 1 < total_pages() {
140                                current_page += 1;
141                            }
142                        },
143                        "Next"
144                        Icon { class: "size-4", icon: LdChevronRight }
145                    }
146                }
147            }
148        }
149    }
150}