Jesús Pérez d3a47108af
Some checks failed
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
CI/CD Pipeline / Cleanup (push) Has been cancelled
chore: update gitignore and fix content
2026-02-08 20:07:09 +00:00

313 lines
12 KiB
Rust

//! Enhanced Simple Content Grid Component
//!
//! Pure functional content grid with content-kinds.toml configuration support.
//! Includes essential features while avoiding complex reactive patterns and hydration issues.
use crate::content::card::UnifiedContentCard;
use crate::content::pagination::PaginationControls;
use leptos::prelude::*;
use rustelo_core_lib::{
content::UnifiedContentItem, create_content_kind_registry, fluent::load_content_index,
i18n::create_content_provider,
};
use std::collections::HashMap;
/// Enhanced Simple Content Grid with content-kinds.toml configuration support
#[component]
pub fn SimpleContentGrid(
content_type: String,
language: String,
#[prop(default = HashMap::new())] lang_content: HashMap<String, String>,
#[prop(default = "".to_string())] category_filter: String,
#[prop(default = 12)] limit: usize,
#[prop(default = 1)] current_page: u32,
#[prop(default = true)] enable_pagination: bool,
) -> impl IntoView {
let content_provider = create_content_provider(Some(lang_content.clone()));
let content_config = {
let registry = create_content_kind_registry();
registry
.kinds.get(&content_type)
.cloned()
.unwrap_or_else(|| {
tracing::error!(
"❌ CONFIG_ERROR: Unknown content_type: '{}', available configs: {:?}, using default config - WASM FILESYSTEM ISSUE!",
content_type, registry.kinds.keys().collect::<Vec<_>>()
);
rustelo_core_lib::ContentConfig::from_env()
})
};
let page = RwSignal::new(current_page);
let page_size = RwSignal::new(0u32); // Will be set from config
let set_page = page.write_only();
let set_page_size = page_size.write_only();
// Set pagination configuration from content config
let config_page_size = 12; // Default page size since ContentFeatures doesn't have this field
let config_page_size_options = vec![6, 12, 24, 48]; // Default options
let config_show_page_info = true; // Default to showing page info
// Initialize page size from config
if page_size.get_untracked() == 0 {
set_page_size.set(config_page_size);
}
// Clone strings for use in multiple closures
let content_type_clone = content_type.clone();
let content_type_for_empty = content_type.clone();
let language_clone = language.clone();
let category_filter_clone = category_filter.clone();
// Calculate effective page size and offset for pagination
let effective_page_size = if enable_pagination {
12 // Default page size
} else {
limit
};
let page_offset = if enable_pagination {
((current_page.saturating_sub(1)) as usize) * (effective_page_size as usize)
} else {
0
};
// Clone config for use in memo and later in view
let config_for_memo = content_config.clone();
let _use_feature = true; // Default feature usage
let _use_emojis = true; // Default emoji usage
let style_css = Some("default".to_string()); // Default CSS style
let style_mode = "grid".to_string(); // Default style mode
// Static content loading with configuration support - no reactive signals during render
let (content_items, total_items) = Memo::new(move |_| {
load_content_for_type_enhanced(
&content_type_clone,
&language_clone,
&category_filter_clone,
&config_for_memo,
effective_page_size as usize,
page_offset,
)
})
.get_untracked();
let update_page_in_url = move |new_page: u32, _new_page_size: u32| {
#[cfg(target_arch = "wasm32")]
{
if let Some(_set_path) = use_context::<WriteSignal<String>>() {
// For now, just update the page state without URL changes
// TODO: Implement URL query parameter support in custom navigation system
tracing::debug!("Page changed to: {}", new_page);
// Future: nav::anchor_navigate(set_path, &url_with_params);
}
}
#[cfg(not(target_arch = "wasm32"))]
{
tracing::debug!("Page changed to: {} (SSR mode)", new_page);
}
};
// Generate CSS classes from content configuration
let css_classes = format!(
"enhanced-content-grid {} content-type-{} layout-{}-layout",
style_css.unwrap_or("default".to_string()),
&content_type,
style_mode
);
// Determine grid layout based on style_mode
let grid_classes = match style_mode.as_str() {
"grid" => match content_items.len() {
0 => "grid grid-cols-1 gap-6",
1 => "grid grid-cols-1 gap-8 max-w-2xl mx-auto",
2 => "grid md:grid-cols-2 gap-8",
_ => "grid md:grid-cols-2 lg:grid-cols-3 gap-6",
},
_ => "flex flex-col gap-6",
};
// Check if content is empty before consuming the vector
let is_empty = content_items.is_empty();
view! {
<div class={css_classes}>
<div class={grid_classes}>
{content_items.into_iter().map(|item| {
view! {
<UnifiedContentCard
content_item=item.clone()
content_type=content_type.to_string()
language=language.clone()
lang_content=lang_content.clone()
_content_config=content_config.clone()
/>
}
}).collect_view()}
</div>
// Pagination controls (if enabled)
{if enable_pagination && total_items > effective_page_size {
let total_pages = (total_items + effective_page_size - 1) / effective_page_size;
// Create callbacks outside the view
let page_change_callback = {
Callback::new(move |new_page: u32| {
set_page.set(new_page);
update_page_in_url(new_page, page_size.get_untracked());
})
};
let page_size_change_callback = {
Callback::new(move |new_page_size: u32| {
set_page_size.set(new_page_size);
set_page.set(1); // Reset to first page when changing page size
update_page_in_url(1, new_page_size);
})
};
view! {
<PaginationControls
current_page=Signal::from(page)
total_pages=Signal::from(total_pages as u32)
total_items=total_items as u32
items_per_page=Signal::from(page_size)
content_type=content_type.to_string()
lang_content=std::collections::HashMap::new()
on_page_change=page_change_callback
on_page_size_change=page_size_change_callback
page_size_options=config_page_size_options
show_page_info=config_show_page_info
/>
}.into_any()
} else {
view! { <></> }.into_any()
}}
// Empty state when no content
{if is_empty {
view! {
<div class="empty-state text-center py-12">
<div class="max-w-md mx-auto">
<div class="text-4xl mb-4 opacity-50">{"📄"}</div>
<h3 class="text-lg font-medium ds-text mb-2">
{content_provider.t_with_prefixes("no-content", &[&content_type_for_empty, "content"], Some("No content available"))}
</h3>
<p class="text-sm ds-text-secondary">
{if !category_filter.is_empty() {
format!("No {} content found for category: {}", content_type_for_empty, category_filter)
} else {
format!("No {} content is currently available", content_type_for_empty)
}}
</p>
</div>
</div>
}.into_any()
} else {
view! { <></> }.into_any()
}}
</div>
}
}
// Note: Using UnifiedContentItem directly instead of custom ContentItem struct
// Enhanced content loading function with configuration support
fn load_content_for_type_enhanced(
content_type: &str,
language: &str,
category_filter: &str,
_content_config: &rustelo_core_lib::ContentConfig,
page_size: usize,
page_offset: usize,
) -> (Vec<UnifiedContentItem>, usize) {
// Load content index
let content_index = match load_content_index(content_type, language) {
Ok(index) => index,
Err(e) => {
tracing::warn!(
"Failed to load content index for {}/{}: {}",
content_type,
language,
e
);
return (vec![], 0);
}
};
// Filter content based on configuration and parameters
let mut all_items: Vec<UnifiedContentItem> = content_index
.items
.into_iter()
.filter(|item| item.published) // Only published items
.filter_map(|item| {
// Category filtering
if !category_filter.is_empty()
&& category_filter != "all"
&& !item.categories.contains(&category_filter.to_string())
{
return None;
}
Some(UnifiedContentItem {
id: item.slug.clone(),
title: item.title,
slug: item.slug,
language: language.to_string(),
content_type: content_type.to_string(),
content: String::new(), // Not loaded in grid view
excerpt: item.excerpt,
subtitle: None,
categories: item.categories,
tags: item.tags,
emoji: None,
featured: item.featured,
published: true, // Already filtered
draft: Some(false),
author: None,
read_time: item.read_time,
created_at: item.created_at,
updated_at: Some(String::new()),
translations: vec![],
localized_slug: None,
source_file: String::new(),
metadata: serde_json::json!({}),
difficulty: None,
prep_time: None,
duration: None,
prerequisites: None,
view_count: None,
image_url: None,
})
})
.collect();
let total_count = all_items.len();
// Sort content based on configuration
all_items.sort_by(|a, b| {
// Featured items first if use_feature is enabled
if true {
// Default feature usage
match (a.featured, b.featured) {
(true, false) => return std::cmp::Ordering::Less,
(false, true) => return std::cmp::Ordering::Greater,
_ => {}
}
}
// Then sort by date (newest first)
b.created_at.cmp(&a.created_at)
});
// Apply pagination
let items = all_items
.into_iter()
.skip(page_offset)
.take(page_size)
.collect();
(items, total_count)
}