logicaffeine_web/ui/pages/
profile.rs1use 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(); let engine = ContentEngine::new();
224
225 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 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 div { class: "profile-content",
271 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 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 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 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}