Skip to content

Commit ee36c0a

Browse files
committed
fix spark lines for goals
1 parent c98e22a commit ee36c0a

5 files changed

Lines changed: 109 additions & 50 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -751,9 +751,15 @@ fn get_checkin_history(limit: i32) -> Result<Vec<CheckinHistoryItem>, String> {
751751
Ok(items)
752752
}
753753

754+
#[derive(Debug, serde::Serialize)]
755+
struct RefreshResult {
756+
new_count: usize,
757+
matches: Vec<MatchWithGoals>,
758+
}
759+
754760
/// Refresh matches from OpenDota API
755761
#[tauri::command]
756-
async fn refresh_matches() -> Result<Vec<MatchWithGoals>, String> {
762+
async fn refresh_matches() -> Result<RefreshResult, String> {
757763
let settings = Settings::load();
758764
let steam_id = settings
759765
.steam_id
@@ -766,16 +772,18 @@ async fn refresh_matches() -> Result<Vec<MatchWithGoals>, String> {
766772
let conn = init_db()?;
767773

768774
// Insert matches that don't already exist
769-
let mut new_matches = Vec::new();
775+
let mut new_count = 0;
770776
for m in matches {
771777
if !match_exists(&conn, m.match_id)? {
772778
insert_match(&conn, &m)?;
773-
new_matches.push(m);
779+
new_count += 1;
774780
}
775781
}
776782

777-
// Return all matches from database
778-
get_matches_with_goals(&conn)
783+
Ok(RefreshResult {
784+
new_count,
785+
matches: get_matches_with_goals(&conn)?,
786+
})
779787
}
780788

781789
/// Get all stored matches

src/app.css

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ input, select, textarea { font-family: inherit; }
189189
border-radius: 8px;
190190
padding: 16px 20px;
191191
display: grid;
192-
grid-template-columns: 40px 1fr auto;
192+
grid-template-columns: 40px 1fr 110px auto;
193193
align-items: center;
194194
gap: 16px;
195195
cursor: pointer;
@@ -231,23 +231,18 @@ input, select, textarea { font-family: inherit; }
231231
text-overflow: ellipsis;
232232
}
233233

234-
.goal-progress-bar {
235-
height: 4px;
236-
background: rgba(255, 255, 255, 0.06);
237-
border-radius: 2px;
234+
.goal-spark-col {
235+
width: 110px;
236+
height: 40px;
237+
border-radius: 4px;
238+
background: rgba(255, 255, 255, 0.03);
238239
overflow: hidden;
239-
margin-bottom: 4px;
240240
}
241241

242-
.goal-fill {
242+
.goal-sparkline {
243+
display: block;
244+
width: 100%;
243245
height: 100%;
244-
border-radius: 2px;
245-
background: linear-gradient(90deg, var(--gold-dim), var(--gold));
246-
transition: width 0.4s ease;
247-
}
248-
249-
.goal-fill.success {
250-
background: linear-gradient(90deg, #16a34a, var(--green));
251246
}
252247

253248
.goal-meta {

src/lib/checkin-store.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { writable } from 'svelte/store';
2+
3+
// Set by the matches page after a refresh that finds new matches.
4+
// Read by the dashboard to show the post-game mood check-in modal.
5+
export const pendingCheckinStore = writable(null);

src/routes/+page.svelte

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
<script module>
2-
// Persists across navigations for the lifetime of the app session
3-
let checkinShownThisSession = false;
4-
</script>
5-
61
<script>
72
import { invoke } from "@tauri-apps/api/core";
83
import { onMount, onDestroy } from "svelte";
@@ -12,6 +7,7 @@
127
import ItemIcon from "$lib/ItemIcon.svelte";
138
import { trackPageView } from "$lib/analytics.js";
149
import MoodCheckin from "$lib/MoodCheckin.svelte";
10+
import { pendingCheckinStore } from "$lib/checkin-store.js";
1511
1612
let isLoading = $state(true);
1713
let error = $state("");
@@ -29,8 +25,8 @@
2925
let recentMatches = $state([]);
3026
let analysisData = $state(null);
3127
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);
3430
3531
const DAYS_TO_SHOW = 7;
3632
@@ -81,7 +77,6 @@
8177
loadDailyChallenge(),
8278
loadWeeklyChallenge(),
8379
loadQuickStats(),
84-
loadPendingCheckin(),
8580
]);
8681
updateMidnightCountdown();
8782
midnightTimer = setInterval(updateMidnightCountdown, 60000);
@@ -120,23 +115,8 @@
120115
}
121116
}
122117
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-
138118
function onCheckinComplete() {
139-
pendingCheckin = null;
119+
pendingCheckinStore.set(null);
140120
}
141121
142122
async function loadDailyChallenge() {
@@ -307,6 +287,41 @@
307287
return { label: 'Stable', cls: 'stable' };
308288
}
309289
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+
310325
function formatDayLabel(dateString) {
311326
const date = new Date(dateString + "T00:00:00Z");
312327
const today = new Date();
@@ -457,6 +472,9 @@
457472
{#each goalCalendar as goalData}
458473
{@const trend = getTrendLabel(goalData)}
459474
{@const hitRate = getHitRate(goalData)}
475+
{@const spark = getSparklineData(goalData)}
476+
{@const sparkColor = hitRate >= 70 ? '#4ade80' : '#f0b429'}
477+
{@const gradId = `sg${goalData.goal.id}`}
460478
<div class="goal-row" onclick={() => goto(`/goals/${goalData.goal.id}`)}>
461479
<div class="hero-avatar">
462480
{#if goalData.goal.hero_id !== null}
@@ -484,9 +502,6 @@
484502
{formatGoalDescription(goalData.goal)}
485503
{/if}
486504
</div>
487-
<div class="goal-progress-bar">
488-
<div class="goal-fill {hitRate >= 70 ? 'success' : ''}" style="width:{hitRate}%"></div>
489-
</div>
490505
<div class="goal-meta">
491506
{#if trend}
492507
<span class="trend-{trend.cls}">{trend.label}</span>
@@ -495,6 +510,30 @@
495510
<span>{goalData.goal.game_mode}</span>
496511
</div>
497512
</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>
498537
<div class="goal-dots">
499538
{#each goalData.daily_progress as day}
500539
<div class="dot {getDotClass(day)}" title="{formatDayLabel(day.date)}: {day.achieved}/{day.total}">

src/routes/matches/+page.svelte

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import HeroIcon from "$lib/HeroIcon.svelte";
88
import { trackPageView } from "$lib/analytics.js";
99
import { showToast } from "$lib/toast.js";
10+
import { pendingCheckinStore } from "$lib/checkin-store.js";
1011
1112
let isLoading = $state(true);
1213
let error = $state("");
@@ -67,8 +68,12 @@
6768
6869
async function autoRefreshAndParse() {
6970
try {
70-
const newMatches = await invoke("refresh_matches");
71-
matches = newMatches;
71+
const result = await invoke("refresh_matches");
72+
matches = result.matches;
73+
if (result.new_count > 0) {
74+
const checkin = await invoke("get_pending_checkin").catch(() => null);
75+
if (checkin) pendingCheckinStore.set(checkin);
76+
}
7277
const recentMatches = matches.slice(0, 10);
7378
for (const match of recentMatches) {
7479
if ((match.parse_state === "Unparsed" || match.parse_state === "Failed") &&
@@ -111,8 +116,15 @@
111116
error = "";
112117
isRefreshing = true;
113118
try {
114-
matches = await invoke("refresh_matches");
115-
showToast("Matches updated");
119+
const result = await invoke("refresh_matches");
120+
matches = result.matches;
121+
if (result.new_count > 0) {
122+
const checkin = await invoke("get_pending_checkin").catch(() => null);
123+
if (checkin) pendingCheckinStore.set(checkin);
124+
showToast(`${result.new_count} new match${result.new_count > 1 ? 'es' : ''} found`);
125+
} else {
126+
showToast("Matches up to date");
127+
}
116128
} catch (e) {
117129
error = String(e);
118130
showToast(String(e), 'error', 5000);

0 commit comments

Comments
 (0)