Skip to main content

logicaffeine_web/ui/components/
streak_display.rs

1//! Daily streak display component.
2//!
3//! Shows the user's current daily streak with status indicator and freeze tokens.
4//! Different visual states for active, at-risk, frozen, and lost streaks.
5//!
6//! # Props
7//!
8//! - `streak` - Current streak day count
9//! - `status` - Streak status (Active, AtRisk, Frozen, Lost)
10//! - `freezes` - Number of available streak freeze tokens (0-3)
11
12use dioxus::prelude::*;
13use crate::game::StreakStatus;
14use crate::ui::components::icon::{Icon, IconVariant, IconSize};
15
16const STREAK_STYLE: &str = r#"
17.streak-display {
18    display: flex;
19    align-items: center;
20    gap: 8px;
21    padding: 6px 12px;
22    border-radius: 20px;
23    font-size: 14px;
24    font-weight: 500;
25}
26
27.streak-active {
28    background: rgba(249, 115, 22, 0.15);
29    border: 1px solid rgba(249, 115, 22, 0.3);
30    color: #f97316;
31}
32
33.streak-at-risk {
34    background: rgba(248, 113, 113, 0.15);
35    border: 1px solid rgba(248, 113, 113, 0.3);
36    color: #f87171;
37    animation: risk-pulse 1s ease-in-out infinite;
38}
39
40@keyframes risk-pulse {
41    0%, 100% { opacity: 1; }
42    50% { opacity: 0.7; }
43}
44
45.streak-frozen {
46    background: rgba(56, 189, 248, 0.15);
47    border: 1px solid rgba(56, 189, 248, 0.3);
48    color: #38bdf8;
49}
50
51.streak-lost {
52    background: rgba(107, 114, 128, 0.15);
53    border: 1px solid rgba(107, 114, 128, 0.3);
54    color: #9ca3af;
55}
56
57.streak-icon {
58    font-size: 16px;
59}
60
61.streak-count {
62    font-weight: 600;
63}
64
65.streak-label {
66    color: #888;
67    font-size: 12px;
68}
69
70.freeze-tokens {
71    display: flex;
72    gap: 4px;
73    margin-left: 8px;
74    padding-left: 8px;
75    border-left: 1px solid rgba(255, 255, 255, 0.1);
76}
77
78.freeze-token {
79    font-size: 12px;
80    opacity: 0.8;
81}
82
83.freeze-token.empty {
84    opacity: 0.3;
85}
86"#;
87
88#[component]
89pub fn StreakDisplay(streak: u32, status: StreakStatus, freezes: u8) -> Element {
90    let (class, icon_variant, icon_color, text) = match status {
91        StreakStatus::Active { days } => {
92            ("streak-display streak-active", IconVariant::Fire, "#f97316", format!("{} day streak", days))
93        }
94        StreakStatus::AtRisk => {
95            ("streak-display streak-at-risk", IconVariant::Warning, "#f87171", "Streak at risk!".to_string())
96        }
97        StreakStatus::Frozen => {
98            ("streak-display streak-frozen", IconVariant::Shield, "#38bdf8", format!("{} days (frozen)", streak))
99        }
100        StreakStatus::Lost { was } => {
101            ("streak-display streak-lost", IconVariant::HeartBroken, "#9ca3af", format!("Lost {} day streak", was))
102        }
103    };
104
105    rsx! {
106        style { "{STREAK_STYLE}" }
107        div { class: "{class}",
108            span { class: "streak-icon",
109                Icon { variant: icon_variant, size: IconSize::Medium, color: icon_color }
110            }
111            span { class: "streak-count", "{text}" }
112            div { class: "freeze-tokens",
113                for i in 0..3u8 {
114                    span {
115                        class: if i < freezes { "freeze-token" } else { "freeze-token empty" },
116                        Icon { variant: IconVariant::Shield, size: IconSize::Small, color: "#38bdf8" }
117                    }
118                }
119            }
120        }
121    }
122}