feat(ui): use official rustui shimmer component for torrent details
This commit is contained in:
91
frontend/src/components/demos/demo_shimmer.rs
Normal file
91
frontend/src/components/demos/demo_shimmer.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::spawn_local;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::components::ui::button::{Button, ButtonVariant};
|
||||
use crate::components::ui::card::{Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle};
|
||||
use crate::components::ui::shimmer::Shimmer;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CardData {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Simulates a database fetch with 1 second delay
|
||||
#[server]
|
||||
pub async fn fetch_card_data() -> Result<CardData, ServerFnError> {
|
||||
// Simulate network/database latency (only on server)
|
||||
#[cfg(feature = "ssr")]
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
Ok(CardData {
|
||||
title: "Fetched Title".to_string(),
|
||||
description: "This content was fetched from the server after a 1 second simulated delay. The shimmer effect automatically showed during the loading period.".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DemoShimmer() -> impl IntoView {
|
||||
// Loading state
|
||||
let loading = RwSignal::new(false);
|
||||
|
||||
// Store fetched data
|
||||
let card_data = RwSignal::new(None::<CardData>);
|
||||
|
||||
// Fetch handler using spawn_local for reliable repeated calls
|
||||
let on_fetch = move |_| {
|
||||
spawn_local(async move {
|
||||
loading.set(true);
|
||||
let result = fetch_card_data().await;
|
||||
if let Ok(data) = result {
|
||||
card_data.set(Some(data));
|
||||
}
|
||||
loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-2">
|
||||
<Button variant=ButtonVariant::Outline on:click=move |_| loading.set(!loading.get())>
|
||||
"Toggle Loading"
|
||||
</Button>
|
||||
<Button variant=ButtonVariant::Default on:click=on_fetch>
|
||||
"Fetch Data (1s)"
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Shimmer loading=Signal::from(loading)>
|
||||
<Card class="max-w-lg lg:max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{move || {
|
||||
card_data.get().map(|data| data.title).unwrap_or_else(|| "Card Title".to_string())
|
||||
}}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<CardDescription>
|
||||
{move || {
|
||||
card_data
|
||||
.get()
|
||||
.map(|data| data.description)
|
||||
.unwrap_or_else(|| {
|
||||
"Click 'Toggle Loading' for manual control, or 'Fetch Data' to simulate a real server call with 1 second delay."
|
||||
.to_string()
|
||||
})
|
||||
}}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter class="justify-end">
|
||||
<Button variant=ButtonVariant::Outline>"Cancel"</Button>
|
||||
<Button>"Confirm"</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Shimmer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -132,22 +132,24 @@ fn InfoItem(
|
||||
#[component]
|
||||
fn DetailsShimmer() -> impl IntoView {
|
||||
view! {
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{(0..8).map(|_| view! {
|
||||
<div class="flex flex-col gap-2">
|
||||
<crate::components::ui::shimmer::Shimmer class="h-3 w-16" />
|
||||
<crate::components::ui::shimmer::Shimmer class="h-5 w-24" />
|
||||
<crate::components::ui::shimmer::Shimmer loading=Signal::derive(move || true)>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 pointer-events-none">
|
||||
{(0..8).map(|_| view! {
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="h-3 w-16 bg-muted rounded-md" />
|
||||
<div class="h-5 w-24 bg-muted rounded-md" />
|
||||
</div>
|
||||
}).collect_view()}
|
||||
<div class="col-span-2 md:col-span-4 flex flex-col gap-2">
|
||||
<div class="h-3 w-20 bg-muted rounded-md" />
|
||||
<div class="h-5 w-full max-w-md bg-muted rounded-md" />
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-4 flex flex-col gap-2">
|
||||
<div class="h-3 w-12 bg-muted rounded-md" />
|
||||
<div class="h-5 w-full max-w-sm bg-muted rounded-md" />
|
||||
</div>
|
||||
}).collect_view()}
|
||||
<div class="col-span-2 md:col-span-4 flex flex-col gap-2">
|
||||
<crate::components::ui::shimmer::Shimmer class="h-3 w-20" />
|
||||
<crate::components::ui::shimmer::Shimmer class="h-5 w-full max-w-md" />
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-4 flex flex-col gap-2">
|
||||
<crate::components::ui::shimmer::Shimmer class="h-3 w-12" />
|
||||
<crate::components::ui::shimmer::Shimmer class="h-5 w-full max-w-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</crate::components::ui::shimmer::Shimmer>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,52 @@
|
||||
use leptos::prelude::*;
|
||||
use tw_merge::tw_merge;
|
||||
use tw_merge::*;
|
||||
|
||||
use crate::components::hooks::use_random::use_random_id_for;
|
||||
|
||||
#[component]
|
||||
pub fn Shimmer(
|
||||
#[prop(optional, into)] class: String,
|
||||
/// Controls shimmer visibility (works with any bool signal)
|
||||
#[prop(into)]
|
||||
loading: Signal<bool>,
|
||||
|
||||
/// Color of the shimmer wave (default: "rgba(255,255,255,0.15)")
|
||||
#[prop(into, optional)]
|
||||
shimmer_color: Option<String>,
|
||||
|
||||
/// Background color of shimmer blocks (default: "rgba(255,255,255,0.08)")
|
||||
#[prop(into, optional)]
|
||||
background_color: Option<String>,
|
||||
|
||||
/// Animation duration in seconds (default: 1.5)
|
||||
#[prop(optional)]
|
||||
duration: Option<f64>,
|
||||
|
||||
/// Fallback border-radius for text elements in px (default: 4)
|
||||
#[prop(optional)]
|
||||
fallback_border_radius: Option<f64>,
|
||||
|
||||
/// Additional classes
|
||||
#[prop(into, optional)]
|
||||
class: String,
|
||||
|
||||
/// Children to wrap
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let merged_class = tw_merge!(
|
||||
"relative overflow-hidden bg-muted before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent dark:before:via-white/5",
|
||||
class
|
||||
);
|
||||
view! { <div class=merged_class /> }
|
||||
let shimmer_id = use_random_id_for("Shimmer");
|
||||
let merged_class = tw_merge!("relative", class);
|
||||
|
||||
view! {
|
||||
<div
|
||||
id=shimmer_id
|
||||
class=merged_class
|
||||
data-name="Shimmer"
|
||||
data-shimmer-loading=move || loading.get().to_string()
|
||||
data-shimmer-color=shimmer_color
|
||||
data-shimmer-bg-color=background_color
|
||||
data-shimmer-duration=duration.map(|d| d.to_string())
|
||||
data-shimmer-fallback-radius=fallback_border_radius.map(|r| r.to_string())
|
||||
>
|
||||
{children()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user