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
313 lines
12 KiB
Rust
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)
|
|
}
|