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