|
1 | | -<script module> |
2 | | - // Persists across navigations for the lifetime of the app session |
3 | | - let checkinShownThisSession = false; |
4 | | -</script> |
5 | | - |
6 | 1 | <script> |
7 | 2 | import { invoke } from "@tauri-apps/api/core"; |
8 | 3 | import { onMount, onDestroy } from "svelte"; |
|
12 | 7 | import ItemIcon from "$lib/ItemIcon.svelte"; |
13 | 8 | import { trackPageView } from "$lib/analytics.js"; |
14 | 9 | import MoodCheckin from "$lib/MoodCheckin.svelte"; |
| 10 | + import { pendingCheckinStore } from "$lib/checkin-store.js"; |
15 | 11 |
|
16 | 12 | let isLoading = $state(true); |
17 | 13 | let error = $state(""); |
|
29 | 25 | let recentMatches = $state([]); |
30 | 26 | let analysisData = $state(null); |
31 | 27 |
|
32 | | - // Mental health check-in |
33 | | - let pendingCheckin = $state(null); |
| 28 | + // Mental health check-in (set by matches page after finding new matches) |
| 29 | + let pendingCheckin = $derived($pendingCheckinStore); |
34 | 30 |
|
35 | 31 | const DAYS_TO_SHOW = 7; |
36 | 32 |
|
|
81 | 77 | loadDailyChallenge(), |
82 | 78 | loadWeeklyChallenge(), |
83 | 79 | loadQuickStats(), |
84 | | - loadPendingCheckin(), |
85 | 80 | ]); |
86 | 81 | updateMidnightCountdown(); |
87 | 82 | midnightTimer = setInterval(updateMidnightCountdown, 60000); |
|
120 | 115 | } |
121 | 116 | } |
122 | 117 |
|
123 | | - async function loadPendingCheckin() { |
124 | | - // Only show check-in once per session unless it's a loss-streak trigger |
125 | | - try { |
126 | | - const result = await invoke("get_pending_checkin"); |
127 | | - if (!result) return; |
128 | | - // Always show loss-streak triggers; otherwise show only once per session |
129 | | - if (result.is_loss_streak || !checkinShownThisSession) { |
130 | | - pendingCheckin = result; |
131 | | - checkinShownThisSession = true; |
132 | | - } |
133 | | - } catch (e) { |
134 | | - console.error("Failed to load pending checkin:", e); |
135 | | - } |
136 | | - } |
137 | | -
|
138 | 118 | function onCheckinComplete() { |
139 | | - pendingCheckin = null; |
| 119 | + pendingCheckinStore.set(null); |
140 | 120 | } |
141 | 121 |
|
142 | 122 | async function loadDailyChallenge() { |
|
307 | 287 | return { label: 'Stable', cls: 'stable' }; |
308 | 288 | } |
309 | 289 |
|
| 290 | + function getSparklineData(goalData) { |
| 291 | + const days = goalData.daily_progress; |
| 292 | + const n = days.length; |
| 293 | + if (n === 0) return null; |
| 294 | +
|
| 295 | + const rates = days.map(d => d.total > 0 ? d.achieved / d.total : null); |
| 296 | +
|
| 297 | + // 3-day trailing moving average |
| 298 | + const ma = rates.map((_, i) => { |
| 299 | + const win = rates.slice(Math.max(0, i - 2), i + 1).filter(r => r !== null); |
| 300 | + return win.length > 0 ? win.reduce((a, b) => a + b, 0) / win.length : null; |
| 301 | + }); |
| 302 | +
|
| 303 | + const W = 110, H = 40, px = 8, py = 6; |
| 304 | + const iW = W - px * 2, iH = H - py * 2; |
| 305 | +
|
| 306 | + const pts = ma |
| 307 | + .map((v, i) => v !== null |
| 308 | + ? { x: px + (n > 1 ? (i / (n - 1)) * iW : iW / 2), y: py + (1 - v) * iH } |
| 309 | + : null) |
| 310 | + .filter(Boolean); |
| 311 | +
|
| 312 | + if (pts.length === 0) return null; |
| 313 | +
|
| 314 | + const fmt = p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`; |
| 315 | + const bottom = H - py; |
| 316 | + const linePoints = pts.map(fmt).join(' '); |
| 317 | + const fillPath = |
| 318 | + `M${fmt(pts[0])} ` + |
| 319 | + pts.slice(1).map(p => `L${fmt(p)}`).join(' ') + |
| 320 | + ` L${pts[pts.length - 1].x.toFixed(1)},${bottom} L${pts[0].x.toFixed(1)},${bottom} Z`; |
| 321 | +
|
| 322 | + return { linePoints, fillPath, lastPt: pts[pts.length - 1] }; |
| 323 | + } |
| 324 | +
|
310 | 325 | function formatDayLabel(dateString) { |
311 | 326 | const date = new Date(dateString + "T00:00:00Z"); |
312 | 327 | const today = new Date(); |
|
457 | 472 | {#each goalCalendar as goalData} |
458 | 473 | {@const trend = getTrendLabel(goalData)} |
459 | 474 | {@const hitRate = getHitRate(goalData)} |
| 475 | + {@const spark = getSparklineData(goalData)} |
| 476 | + {@const sparkColor = hitRate >= 70 ? '#4ade80' : '#f0b429'} |
| 477 | + {@const gradId = `sg${goalData.goal.id}`} |
460 | 478 | <div class="goal-row" onclick={() => goto(`/goals/${goalData.goal.id}`)}> |
461 | 479 | <div class="hero-avatar"> |
462 | 480 | {#if goalData.goal.hero_id !== null} |
|
484 | 502 | {formatGoalDescription(goalData.goal)} |
485 | 503 | {/if} |
486 | 504 | </div> |
487 | | - <div class="goal-progress-bar"> |
488 | | - <div class="goal-fill {hitRate >= 70 ? 'success' : ''}" style="width:{hitRate}%"></div> |
489 | | - </div> |
490 | 505 | <div class="goal-meta"> |
491 | 506 | {#if trend} |
492 | 507 | <span class="trend-{trend.cls}">{trend.label}</span> |
|
495 | 510 | <span>{goalData.goal.game_mode}</span> |
496 | 511 | </div> |
497 | 512 | </div> |
| 513 | + <div class="goal-spark-col"> |
| 514 | + <svg class="goal-sparkline" viewBox="0 0 110 40" preserveAspectRatio="none"> |
| 515 | + <defs> |
| 516 | + <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1"> |
| 517 | + <stop offset="0%" stop-color={sparkColor} stop-opacity="0.22" /> |
| 518 | + <stop offset="100%" stop-color={sparkColor} stop-opacity="0" /> |
| 519 | + </linearGradient> |
| 520 | + </defs> |
| 521 | + <!-- 75% target reference line at y=13 --> |
| 522 | + <line x1="8" y1="13" x2="102" y2="13" stroke="rgba(255,255,255,0.07)" stroke-width="0.5" stroke-dasharray="2,2" /> |
| 523 | + {#if spark} |
| 524 | + <path d={spark.fillPath} fill="url(#{gradId})" /> |
| 525 | + <polyline |
| 526 | + points={spark.linePoints} |
| 527 | + fill="none" |
| 528 | + stroke={sparkColor} |
| 529 | + stroke-width="1.5" |
| 530 | + stroke-linecap="round" |
| 531 | + stroke-linejoin="round" |
| 532 | + /> |
| 533 | + <circle cx={spark.lastPt.x} cy={spark.lastPt.y} r="2.5" fill={sparkColor} /> |
| 534 | + {/if} |
| 535 | + </svg> |
| 536 | + </div> |
498 | 537 | <div class="goal-dots"> |
499 | 538 | {#each goalData.daily_progress as day} |
500 | 539 | <div class="dot {getDotClass(day)}" title="{formatDayLabel(day.date)}: {day.achieved}/{day.total}"> |
|
0 commit comments