Skip to main content

logicaffeine_web/ui/components/
achievement_toast.rs

1//! Achievement unlock celebration overlay.
2//!
3//! Displays a full-screen modal when the user unlocks an achievement,
4//! with particle effects and sound feedback.
5//!
6//! # Props
7//!
8//! - `achievement` - The achievement that was unlocked
9//! - `on_dismiss` - Callback when the user dismisses the overlay
10
11use dioxus::prelude::*;
12use crate::achievements::Achievement;
13use crate::audio::{SoundEffect, play_sound};
14use crate::ui::components::icon::{Icon, IconVariant, IconSize};
15
16const ACHIEVEMENT_STYLE: &str = r#"
17.achievement-overlay {
18    position: fixed;
19    top: 0;
20    left: 0;
21    right: 0;
22    bottom: 0;
23    background: rgba(0, 0, 0, 0.8);
24    display: flex;
25    align-items: center;
26    justify-content: center;
27    z-index: 2000;
28    animation: overlay-fade 0.3s ease-out;
29}
30
31@keyframes overlay-fade {
32    from { opacity: 0; }
33    to { opacity: 1; }
34}
35
36.achievement-card {
37    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
38    border: 2px solid #fbbf24;
39    border-radius: 20px;
40    padding: 40px 60px;
41    text-align: center;
42    box-shadow: 0 0 60px rgba(251, 191, 36, 0.4);
43    animation: card-appear 0.5s ease-out;
44}
45
46@keyframes card-appear {
47    0% { transform: scale(0.5) translateY(50px); opacity: 0; }
48    70% { transform: scale(1.05) translateY(0); }
49    100% { transform: scale(1) translateY(0); opacity: 1; }
50}
51
52.achievement-icon {
53    font-size: 64px;
54    margin-bottom: 16px;
55    animation: icon-bounce 0.5s ease-out 0.3s both;
56}
57
58@keyframes icon-bounce {
59    0% { transform: scale(0); }
60    50% { transform: scale(1.3); }
61    100% { transform: scale(1); }
62}
63
64.achievement-label {
65    font-size: 14px;
66    color: #fbbf24;
67    text-transform: uppercase;
68    letter-spacing: 2px;
69    margin-bottom: 8px;
70}
71
72.achievement-title {
73    font-size: 32px;
74    font-weight: 700;
75    color: #fff;
76    margin-bottom: 12px;
77}
78
79.achievement-description {
80    font-size: 16px;
81    color: #888;
82    margin-bottom: 24px;
83}
84
85.achievement-reward {
86    font-size: 24px;
87    color: #4ade80;
88    font-weight: 600;
89    margin-bottom: 16px;
90}
91
92.achievement-title-unlock {
93    background: linear-gradient(90deg, #fbbf24, #f59e0b);
94    -webkit-background-clip: text;
95    -webkit-text-fill-color: transparent;
96    background-clip: text;
97    font-size: 18px;
98    font-weight: 600;
99    margin-bottom: 24px;
100}
101
102.achievement-dismiss {
103    padding: 12px 32px;
104    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
105    border: none;
106    border-radius: 8px;
107    color: white;
108    font-size: 16px;
109    font-weight: 600;
110    cursor: pointer;
111    transition: transform 0.2s ease;
112}
113
114.achievement-dismiss:hover {
115    transform: scale(1.05);
116}
117
118.particles {
119    position: absolute;
120    top: 0;
121    left: 0;
122    right: 0;
123    bottom: 0;
124    pointer-events: none;
125    overflow: hidden;
126}
127
128.particle {
129    position: absolute;
130    font-size: 24px;
131    animation: particle-fall 2s ease-out forwards;
132}
133
134@keyframes particle-fall {
135    0% { transform: translateY(-50px) rotate(0deg); opacity: 1; }
136    100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
137}
138"#;
139
140#[component]
141pub fn AchievementToast(achievement: &'static Achievement, on_dismiss: EventHandler<()>) -> Element {
142    use_effect(move || {
143        play_sound(SoundEffect::Achievement);
144    });
145
146    rsx! {
147        style { "{ACHIEVEMENT_STYLE}" }
148        div {
149            class: "achievement-overlay",
150            onclick: move |_| on_dismiss.call(()),
151            div { class: "particles",
152                for i in 0..20 {
153                    span {
154                        class: "particle",
155                        style: "left: {(i * 5) % 100}%; animation-delay: {i as f32 * 0.1}s;",
156                        if i % 2 == 0 {
157                            Icon { variant: IconVariant::Star, size: IconSize::Large, color: "#fbbf24" }
158                        } else {
159                            Icon { variant: IconVariant::Sparkles, size: IconSize::Large, color: "#a78bfa" }
160                        }
161                    }
162                }
163            }
164            div { class: "achievement-card",
165                div { class: "achievement-icon",
166                    Icon { variant: IconVariant::Trophy, size: IconSize::XXLarge, color: "#fbbf24" }
167                }
168                div { class: "achievement-label", "Achievement Unlocked" }
169                div { class: "achievement-title", "{achievement.title}" }
170                div { class: "achievement-description", "{achievement.description}" }
171                div { class: "achievement-reward", "+{achievement.xp_reward} XP" }
172                if let Some(title) = achievement.unlocks_title {
173                    div { class: "achievement-title-unlock",
174                        "Title Unlocked: {title}"
175                    }
176                }
177                if achievement.grants_freeze {
178                    div { class: "achievement-title-unlock",
179                        style: "display: flex; align-items: center; gap: 8px; justify-content: center;",
180                        Icon { variant: IconVariant::Shield, size: IconSize::Medium, color: "#38bdf8" }
181                        "+1 Streak Freeze!"
182                    }
183                }
184                button {
185                    class: "achievement-dismiss",
186                    onclick: move |e| {
187                        e.stop_propagation();
188                        on_dismiss.call(());
189                    },
190                    "Continue"
191                }
192            }
193        }
194    }
195}