Skip to main content

logicaffeine_web/ui/pages/
profile.rs

1//! User profile and progress page.
2//!
3//! Displays user statistics, curriculum progress, and achievements. Sections:
4//!
5//! - **Stats**: Total XP, level, streak days, modules completed
6//! - **Era Progress**: Progress bars for each curriculum era
7//! - **Achievements**: Earned and locked achievement badges
8//!
9//! # Route
10//!
11//! Accessed via [`Route::Profile`](crate::ui::router::Route::Profile).
12
13use dioxus::prelude::*;
14#[cfg(all(feature = "split", target_arch = "wasm32"))]
15use dioxus::wasm_split;
16use crate::ui::components::main_nav::{MainNav, ActivePage};
17use crate::ui::components::footer::Footer;
18use crate::ui::components::icon::{Icon, IconVariant, IconSize};
19use crate::ui::seo::{JsonLdMultiple, PageHead, organization_schema, profile_page_schema, breadcrumb_schema, BreadcrumbItem, pages as seo_pages};
20use crate::progress::UserProgress;
21use crate::content::ContentEngine;
22
23const PROFILE_STYLE: &str = r#"
24.profile-page {
25    min-height: 100vh;
26    color: var(--text-primary);
27    background:
28        radial-gradient(1200px 600px at 50% -120px, rgba(167,139,250,0.14), transparent 60%),
29        radial-gradient(900px 500px at 15% 30%, rgba(96,165,250,0.14), transparent 60%),
30        radial-gradient(800px 450px at 90% 45%, rgba(34,197,94,0.08), transparent 62%),
31        linear-gradient(180deg, #070a12, #0b1022 55%, #070a12);
32    font-family: var(--font-sans);
33}
34
35.profile-hero {
36    max-width: 1000px;
37    margin: 0 auto;
38    padding: 60px var(--spacing-xl) 40px;
39    text-align: center;
40}
41
42.profile-avatar {
43    width: 100px;
44    height: 100px;
45    border-radius: 50%;
46    background: linear-gradient(135deg, var(--color-accent-blue), var(--color-accent-purple));
47    display: flex;
48    align-items: center;
49    justify-content: center;
50    font-size: 48px;
51    margin: 0 auto var(--spacing-lg);
52    box-shadow: 0 8px 32px rgba(96, 165, 250, 0.3);
53}
54
55.profile-name {
56    font-size: var(--font-heading-lg);
57    font-weight: 900;
58    margin-bottom: var(--spacing-sm);
59    background: linear-gradient(180deg, #ffffff 0%, rgba(229,231,235,0.78) 65%);
60    -webkit-background-clip: text;
61    -webkit-text-fill-color: transparent;
62}
63
64.profile-title {
65    font-size: var(--font-body-lg);
66    color: var(--color-accent-purple);
67    font-weight: 600;
68}
69
70.profile-content {
71    max-width: 1000px;
72    margin: 0 auto;
73    padding: 0 var(--spacing-xl) 80px;
74}
75
76.profile-stats {
77    display: grid;
78    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
79    gap: var(--spacing-lg);
80    margin-bottom: var(--spacing-xxl);
81}
82
83.stat-card {
84    background: rgba(255, 255, 255, 0.04);
85    border: 1px solid rgba(255, 255, 255, 0.08);
86    border-radius: var(--radius-xl);
87    padding: var(--spacing-xl);
88    text-align: center;
89    transition: all 0.2s ease;
90}
91
92.stat-card:hover {
93    background: rgba(255, 255, 255, 0.06);
94    transform: translateY(-2px);
95}
96
97.stat-value {
98    font-size: var(--font-display-md);
99    font-weight: 900;
100    background: linear-gradient(135deg, var(--color-accent-blue), var(--color-accent-purple));
101    -webkit-background-clip: text;
102    -webkit-text-fill-color: transparent;
103    margin-bottom: var(--spacing-xs);
104}
105
106.stat-value.xp {
107    background: linear-gradient(135deg, #fbbf24, #f59e0b);
108    -webkit-background-clip: text;
109    -webkit-text-fill-color: transparent;
110}
111
112.stat-value.streak {
113    background: linear-gradient(135deg, #4ade80, #22c55e);
114    -webkit-background-clip: text;
115    -webkit-text-fill-color: transparent;
116}
117
118.stat-value.level {
119    background: linear-gradient(135deg, var(--color-accent-purple), #c084fc);
120    -webkit-background-clip: text;
121    -webkit-text-fill-color: transparent;
122}
123
124.stat-label {
125    font-size: var(--font-body-md);
126    color: var(--text-secondary);
127    font-weight: 500;
128}
129
130.profile-section {
131    margin-bottom: var(--spacing-xxl);
132}
133
134.profile-section-title {
135    font-size: var(--font-heading-sm);
136    font-weight: 700;
137    margin-bottom: var(--spacing-lg);
138    display: flex;
139    align-items: center;
140    gap: var(--spacing-sm);
141}
142
143.progress-card {
144    background: rgba(255, 255, 255, 0.04);
145    border: 1px solid rgba(255, 255, 255, 0.08);
146    border-radius: var(--radius-lg);
147    padding: var(--spacing-lg);
148    margin-bottom: var(--spacing-md);
149}
150
151.progress-era-name {
152    font-weight: 600;
153    color: var(--text-primary);
154    margin-bottom: var(--spacing-sm);
155}
156
157.progress-bar-container {
158    height: 8px;
159    background: rgba(255, 255, 255, 0.08);
160    border-radius: var(--radius-full);
161    overflow: hidden;
162    margin-bottom: var(--spacing-xs);
163}
164
165.progress-bar {
166    height: 100%;
167    background: linear-gradient(90deg, var(--color-accent-blue), var(--color-accent-purple));
168    border-radius: var(--radius-full);
169    transition: width 0.3s ease;
170}
171
172.progress-text {
173    font-size: var(--font-caption-md);
174    color: var(--text-tertiary);
175}
176
177.achievements-grid {
178    display: grid;
179    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
180    gap: var(--spacing-md);
181}
182
183.achievement-badge {
184    background: rgba(255, 255, 255, 0.04);
185    border: 1px solid rgba(255, 255, 255, 0.08);
186    border-radius: var(--radius-lg);
187    padding: var(--spacing-lg);
188    text-align: center;
189    transition: all 0.2s ease;
190}
191
192.achievement-badge:hover {
193    background: rgba(255, 255, 255, 0.06);
194    transform: scale(1.02);
195}
196
197.achievement-badge.locked {
198    opacity: 0.4;
199    filter: grayscale(100%);
200}
201
202.achievement-icon {
203    font-size: 32px;
204    margin-bottom: var(--spacing-sm);
205}
206
207.achievement-name {
208    font-size: var(--font-caption-md);
209    font-weight: 600;
210    color: var(--text-primary);
211}
212
213.empty-state {
214    text-align: center;
215    padding: var(--spacing-xxl);
216    color: var(--text-tertiary);
217}
218"#;
219
220#[component(lazy)]
221pub fn Profile() -> Element {
222    let progress = UserProgress::new(); // TODO: Load from storage
223    let engine = ContentEngine::new();
224
225    // Calculate totals
226    let _total_exercises: usize = engine.eras()
227        .iter()
228        .flat_map(|e| e.modules.iter())
229        .map(|m| m.exercises.len())
230        .sum();
231
232    let completed_modules = progress.modules.values().filter(|m| m.completed).count();
233    let total_modules: usize = engine.eras().iter().map(|e| e.modules.len()).sum();
234
235    let breadcrumbs = vec![
236        BreadcrumbItem { name: "Home", path: "/" },
237        BreadcrumbItem { name: "Profile", path: "/profile" },
238    ];
239    let schemas = vec![
240        organization_schema(),
241        profile_page_schema(),
242        breadcrumb_schema(&breadcrumbs),
243    ];
244
245    rsx! {
246        PageHead {
247            title: seo_pages::PROFILE.title,
248            description: seo_pages::PROFILE.description,
249            canonical_path: seo_pages::PROFILE.canonical_path,
250        }
251        style { "{PROFILE_STYLE}" }
252        JsonLdMultiple { schemas }
253        div { class: "profile-page",
254            MainNav { active: ActivePage::Profile, subtitle: Some("Your logic journey") }
255
256            // Hero section
257            div { class: "profile-hero",
258                div { class: "profile-avatar", "L" }
259                h1 { class: "profile-name", "Logic Learner" }
260                p { class: "profile-title",
261                    if progress.title.is_some() {
262                        "{progress.title.as_ref().unwrap()}"
263                    } else {
264                        "Apprentice Logician"
265                    }
266                }
267            }
268
269            // Content
270            div { class: "profile-content",
271                // Stats grid
272                div { class: "profile-stats",
273                    div { class: "stat-card",
274                        div { class: "stat-value xp", "{progress.xp}" }
275                        div { class: "stat-label", "Total XP" }
276                    }
277                    div { class: "stat-card",
278                        div { class: "stat-value level", "Level {progress.level}" }
279                        div { class: "stat-label", "Current Level" }
280                    }
281                    div { class: "stat-card",
282                        div { class: "stat-value streak", "{progress.streak_days}" }
283                        div { class: "stat-label", "Day Streak" }
284                    }
285                    div { class: "stat-card",
286                        div { class: "stat-value", "{completed_modules}/{total_modules}" }
287                        div { class: "stat-label", "Modules Completed" }
288                    }
289                }
290
291                // Era Progress
292                div { class: "profile-section",
293                    h2 { class: "profile-section-title", "Progress by Era" }
294
295                    for era in engine.eras() {
296                        {
297                            let era_modules = era.modules.len();
298                            let era_completed = era.modules.iter()
299                                .filter(|m| progress.modules.get(&m.meta.id).map_or(false, |p| p.completed))
300                                .count();
301                            let percent = if era_modules > 0 { (era_completed * 100) / era_modules } else { 0 };
302
303                            rsx! {
304                                div { class: "progress-card",
305                                    div { class: "progress-era-name", "{era.meta.title}" }
306                                    div { class: "progress-bar-container",
307                                        div {
308                                            class: "progress-bar",
309                                            style: "width: {percent}%;",
310                                        }
311                                    }
312                                    div { class: "progress-text", "{era_completed} of {era_modules} modules completed" }
313                                }
314                            }
315                        }
316                    }
317                }
318
319                // Achievements
320                div { class: "profile-section",
321                    h2 { class: "profile-section-title", "Achievements" }
322
323                    if progress.achievements.is_empty() {
324                        div { class: "empty-state",
325                            p { "Complete modules and practice exercises to earn achievements!" }
326                        }
327                    } else {
328                        div { class: "achievements-grid",
329                            for achievement in progress.achievements.iter() {
330                                div { class: "achievement-badge",
331                                    div { class: "achievement-icon",
332                                        Icon { variant: IconVariant::Trophy, size: IconSize::Large, color: "#fbbf24" }
333                                    }
334                                    div { class: "achievement-name", "{achievement}" }
335                                }
336                            }
337                        }
338                    }
339
340                    // Show locked achievements
341                    div { class: "achievements-grid", style: "margin-top: var(--spacing-lg);",
342                        div { class: "achievement-badge locked",
343                            div { class: "achievement-icon",
344                                Icon { variant: IconVariant::Target, size: IconSize::Large, color: "#60a5fa" }
345                            }
346                            div { class: "achievement-name", "First Blood" }
347                        }
348                        div { class: "achievement-badge locked",
349                            div { class: "achievement-icon",
350                                Icon { variant: IconVariant::Fire, size: IconSize::Large, color: "#f97316" }
351                            }
352                            div { class: "achievement-name", "7-Day Streak" }
353                        }
354                        div { class: "achievement-badge locked",
355                            div { class: "achievement-icon",
356                                Icon { variant: IconVariant::Star, size: IconSize::Large, color: "#a78bfa" }
357                            }
358                            div { class: "achievement-name", "Perfect Score" }
359                        }
360                        div { class: "achievement-badge locked",
361                            div { class: "achievement-icon",
362                                Icon { variant: IconVariant::Brain, size: IconSize::Large, color: "#f472b6" }
363                            }
364                            div { class: "achievement-name", "Logic Master" }
365                        }
366                    }
367                }
368            }
369
370            Footer {}
371        }
372    }
373}