mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
docs(docs): revamp progress dashboard UI and add week field comment
This commit is contained in:
@@ -35,6 +35,7 @@ interface Stats {
|
|||||||
deferred_total: number;
|
deferred_total: number;
|
||||||
}[];
|
}[];
|
||||||
ladder: Record<Stage, number>;
|
ladder: Record<Stage, number>;
|
||||||
|
/** week is the 1-based campaign week — the Worker converts from ISO. */
|
||||||
gates: {
|
gates: {
|
||||||
week: number;
|
week: number;
|
||||||
issue: number | null;
|
issue: number | null;
|
||||||
@@ -76,7 +77,12 @@ function dayLabel(iso: string): string {
|
|||||||
return `${weekday} ${iso.slice(8)}`;
|
return `${weekday} ${iso.slice(8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── shared styles (theme tokens only — no hand-rolled palette) ───
|
// ── shared styles ────────────────────────────────────────────────
|
||||||
|
// Theme tokens only, with two deliberate exceptions: the phase-bar sets get
|
||||||
|
// fixed hues (accent-blue core, yellow optional, red deferred — they must be
|
||||||
|
// distinguishable, not shades of one colour), and the last-7-days heatmap
|
||||||
|
// reuses the GREENS ramp from apps/api/src/charts.ts so the page matches the
|
||||||
|
// README/email heatmaps (level 0 stays a theme token so light mode works).
|
||||||
|
|
||||||
const card: CSSProperties = {
|
const card: CSSProperties = {
|
||||||
border: "1px solid var(--blume-border)",
|
border: "1px solid var(--blume-border)",
|
||||||
@@ -142,32 +148,78 @@ function Header({ stats }: { stats: Stats }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One stacked bar per phase: solved core / optional / deferred segments over
|
||||||
|
// a muted track sized by the phase's total problem count, so both phase size
|
||||||
|
// and progress read at a glance.
|
||||||
|
const SETS = [
|
||||||
|
["core", "var(--blume-accent)"],
|
||||||
|
["optional", "#d29922"],
|
||||||
|
["deferred", "#e5534b"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
function Phases({ stats }: { stats: Stats }) {
|
function Phases({ stats }: { stats: Stats }) {
|
||||||
|
const maxTotal = Math.max(
|
||||||
|
1,
|
||||||
|
...stats.phases.map((p) => p.core_total + p.optional_total + p.deferred_total),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div style={card}>
|
<div style={card}>
|
||||||
<div style={heading}>Phases</div>
|
<div style={heading}>Phases</div>
|
||||||
<table style={{ borderCollapse: "collapse", width: "100%" }}>
|
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap", marginBottom: "0.6rem" }}>
|
||||||
<tbody>
|
{SETS.map(([set, color]) => (
|
||||||
{stats.phases.map((p) => (
|
<span key={set} style={muted}>
|
||||||
<tr key={p.milestone}>
|
<span
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>{p.name}</td>
|
style={{
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>
|
display: "inline-block",
|
||||||
<Bar done={p.core_done} total={p.core_total} />{" "}
|
width: "0.65rem",
|
||||||
<span style={muted}>
|
height: "0.65rem",
|
||||||
core {p.core_done}/{p.core_total}
|
borderRadius: "2px",
|
||||||
</span>
|
background: color,
|
||||||
</td>
|
marginRight: "0.35rem",
|
||||||
<td style={{ padding: "0.2rem 0" }}>
|
}}
|
||||||
<span style={muted}>
|
/>
|
||||||
optional {p.optional_done}/{p.optional_total}
|
{set}
|
||||||
{p.deferred_total > 0 &&
|
</span>
|
||||||
` · deferred ${p.deferred_done}/${p.deferred_total}`}
|
))}
|
||||||
</span>
|
</div>
|
||||||
</td>
|
{stats.phases.map((p) => {
|
||||||
</tr>
|
const total = p.core_total + p.optional_total + p.deferred_total;
|
||||||
))}
|
const done = p.core_done + p.optional_done + p.deferred_done;
|
||||||
</tbody>
|
return (
|
||||||
</table>
|
<div
|
||||||
|
key={p.milestone}
|
||||||
|
title={`core ${p.core_done}/${p.core_total} · optional ${p.optional_done}/${p.optional_total} · deferred ${p.deferred_done}/${p.deferred_total}`}
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: "0.75rem", padding: "0.25rem 0" }}
|
||||||
|
>
|
||||||
|
<span style={{ width: "9.5rem", flexShrink: 0 }}>{p.name}</span>
|
||||||
|
<span style={{ flex: 1 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
width: `${(total / maxTotal) * 100}%`,
|
||||||
|
height: "0.65rem",
|
||||||
|
borderRadius: "var(--blume-radius)",
|
||||||
|
background: "var(--blume-muted)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SETS.map(([set, color]) => {
|
||||||
|
const solved = p[`${set}_done`];
|
||||||
|
return solved > 0 ? (
|
||||||
|
<span
|
||||||
|
key={set}
|
||||||
|
style={{ width: `${(solved / total) * 100}%`, background: color }}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span style={{ ...muted, width: "3.5rem", textAlign: "right", flexShrink: 0 }}>
|
||||||
|
{done}/{total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -224,76 +276,95 @@ function Gates({ stats }: { stats: Stats }) {
|
|||||||
<div style={card}>
|
<div style={card}>
|
||||||
<div style={heading}>Gate log</div>
|
<div style={heading}>Gate log</div>
|
||||||
{stats.gates.length === 0 ? (
|
{stats.gates.length === 0 ? (
|
||||||
<span style={muted}>No gates yet.</span>
|
<span style={muted}>No gates yet — the first weekly review opens Saturday night.</span>
|
||||||
) : (
|
) : (
|
||||||
<table style={{ borderCollapse: "collapse", width: "100%" }}>
|
stats.gates.map((g) => (
|
||||||
<thead>
|
<div
|
||||||
<tr>
|
key={g.week}
|
||||||
{["Week", "Issue", "Pass rate", "Closed"].map((h) => (
|
style={{
|
||||||
<th key={h} style={{ ...muted, textAlign: "left", padding: "0.2rem 1rem 0.2rem 0" }}>
|
display: "flex",
|
||||||
{h}
|
gap: "0.75rem",
|
||||||
</th>
|
alignItems: "baseline",
|
||||||
))}
|
flexWrap: "wrap",
|
||||||
</tr>
|
padding: "0.15rem 0",
|
||||||
</thead>
|
}}
|
||||||
<tbody>
|
>
|
||||||
{stats.gates.map((g) => (
|
<span style={{ width: "4.5rem", flexShrink: 0 }}>Week {g.week}</span>
|
||||||
<tr key={g.week}>
|
{g.issue === null ? (
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>{g.week}</td>
|
<span style={muted}>no review issue</span>
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>
|
) : (
|
||||||
{g.issue === null ? (
|
<a
|
||||||
<span style={muted}>—</span>
|
href={`https://github.com/prdlk/leetcode/issues/${g.issue}`}
|
||||||
) : (
|
target="_blank"
|
||||||
<a
|
rel="noreferrer"
|
||||||
href={`https://github.com/prdlk/leetcode/issues/${g.issue}`}
|
>
|
||||||
target="_blank"
|
#{g.issue}
|
||||||
rel="noreferrer"
|
</a>
|
||||||
>
|
)}
|
||||||
#{g.issue}
|
{g.pass_rate === null ? (
|
||||||
</a>
|
<span style={muted}>in progress</span>
|
||||||
)}
|
) : (
|
||||||
</td>
|
<span>
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>
|
{percent(g.pass_rate)} passed
|
||||||
{g.pass_rate === null ? (
|
{g.closed_on && <span style={muted}> · closed {g.closed_on}</span>}
|
||||||
<span style={muted}>open</span>
|
</span>
|
||||||
) : (
|
)}
|
||||||
percent(g.pass_rate)
|
</div>
|
||||||
)}
|
))
|
||||||
</td>
|
|
||||||
<td style={{ padding: "0.2rem 0" }}>
|
|
||||||
{g.closed_on ?? <span style={muted}>—</span>}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
)}
|
||||||
|
<div style={{ ...muted, marginTop: "0.4rem" }}>
|
||||||
|
One gate per campaign week — scored when its review issue closes.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GREENS ramp from apps/api/src/charts.ts; level 0 swapped for the theme's
|
||||||
|
// muted token so an empty day looks right in both colour schemes.
|
||||||
|
const GREENS = [
|
||||||
|
"var(--blume-muted)",
|
||||||
|
"#0e4429",
|
||||||
|
"#006d32",
|
||||||
|
"#26a641",
|
||||||
|
"#39d353",
|
||||||
|
];
|
||||||
|
|
||||||
function Recent({ stats }: { stats: Stats }) {
|
function Recent({ stats }: { stats: Stats }) {
|
||||||
return (
|
return (
|
||||||
<div style={card}>
|
<div style={card}>
|
||||||
<div style={heading}>Last 7 days</div>
|
<div style={heading}>Last 7 days</div>
|
||||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
||||||
{stats.recent.map((r) => (
|
{stats.recent.map((r) => (
|
||||||
<span
|
<span
|
||||||
key={r.date}
|
key={r.date}
|
||||||
style={{
|
title={`${r.date}: ${r.passes}/${r.attempts} passed`}
|
||||||
border: "1px solid var(--blume-border)",
|
style={{ textAlign: "center" }}
|
||||||
borderRadius: "var(--blume-radius)",
|
|
||||||
padding: "0.35rem 0.6rem",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span style={{ ...muted, display: "block" }}>{dayLabel(r.date)}</span>
|
<span
|
||||||
<strong>{r.passes}</strong>
|
style={{
|
||||||
<span style={muted}>/{r.attempts}</span>
|
display: "block",
|
||||||
|
width: "2.6rem",
|
||||||
|
height: "2rem",
|
||||||
|
borderRadius: "var(--blume-radius)",
|
||||||
|
background: GREENS[Math.min(r.attempts, 4)],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ ...muted, display: "block", marginTop: "0.25rem", fontSize: "0.75em" }}>
|
||||||
|
{dayLabel(r.date)}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...muted, marginTop: "0.4rem" }}>passes / attempts</div>
|
<div style={{ ...muted, marginTop: "0.5rem", display: "flex", alignItems: "center", gap: "0.25rem" }}>
|
||||||
|
<span>Less</span>
|
||||||
|
{GREENS.map((color) => (
|
||||||
|
<span
|
||||||
|
key={color}
|
||||||
|
style={{ display: "inline-block", width: "0.65rem", height: "0.65rem", borderRadius: "2px", background: color }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span>More · colour = attempts, hover for passes</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user