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