feat(api): enforce REVIEW_CAP, level overflow reviews, and display difficulty in digest

This commit is contained in:
Prad Nukala committed 2026-08-26 10:34:24 -04:00
1 parent f697a986b8
commit 1b4ad8ca5b
4 files changed
+110 -26

No files matched your search

+63 -19
View File
@@ -35,6 +35,7 @@ import {
} from "@react-email/components";
import { render } from "@react-email/render";
import type { CSSProperties } from "react";
import { REVIEW_CAP } from "./srs.ts";
// ── palette (GitHub dark) ────────────────────────────────────────
const CANVAS = "#010409";
@@ -82,6 +83,14 @@ export interface DigestRow {
failUrl: string;
}
/** A learning-day core problem — the only rows allowed a link. */
export interface TopicRow {
lc: number;
difficulty: string;
url: string;
solved: boolean;
}
export interface DigestData {
/** "Tuesday, August 25" */
day: string;
@@ -89,9 +98,9 @@ export interface DigestData {
progress: string;
streak: number;
rest: boolean;
topic?: { name: string; core: { lc: number; url: string; solved: boolean }[] };
topic?: { name: string; core: TopicRow[] };
reviews: DigestRow[];
/** Reviews past the daily cap; they simply stay due. */
/** Reviews past the daily cap; levelled onto the coming days. */
carried: number;
drills: DigestRow[];
gate?: { week: number; url: string };
@@ -196,6 +205,48 @@ function ProblemTable({ rows, staged }: { rows: DigestRow[]; staged: boolean })
);
}
/**
* The learning day's core list as the same table shape as reviews — this is
* the one section allowed to link problems, and ticks replace tap buttons.
*/
function TopicTable({ rows }: { rows: TopicRow[] }) {
return (
<table
width="100%"
border={0}
cellPadding={0}
cellSpacing={0}
style={{ borderCollapse: "collapse", width: "100%" }}
>
<thead>
<tr>
<th style={th}>Problem</th>
<th style={th}>Level</th>
<th style={{ ...th, textAlign: "right" }}>Solved</th>
</tr>
</thead>
<tbody>
{rows.map((p) => {
const difficulty = DIFFICULTY[p.difficulty];
return (
<tr key={p.lc}>
<td style={{ ...td, fontWeight: 600 }}>
<Link href={p.url} style={{ color: p.solved ? MUTED : LINK, textDecoration: "none" }}>
LC {p.lc}
</Link>
</td>
<td style={{ ...td, color: difficulty?.color ?? INK }}>{difficulty?.label ?? p.difficulty}</td>
<td style={{ ...td, color: p.solved ? GREEN : MUTED, textAlign: "right" }}>
{p.solved ? "✓" : "—"}
</td>
</tr>
);
})}
</tbody>
</table>
);
}
function Block({
title,
note,
@@ -265,18 +316,8 @@ function DigestEmail({ data }: { data: DigestData }) {
<Text style={{ color: INK, fontSize: "16px", fontWeight: 600, margin: "0 0 8px" }}>
{data.topic.name}
</Text>
<Text style={{ ...hint, margin: 0 }}>
Work through these — ticked ones you have already solved:{" "}
{data.topic.core.map((p, i) => (
<span key={p.lc}>
{i > 0 ? " · " : ""}
<Link href={p.url} style={{ color: p.solved ? MUTED : LINK, textDecoration: "none" }}>
LC {p.lc}
</Link>
{p.solved ? <span style={{ color: GREEN }}> ✓</span> : null}
</span>
))}
</Text>
<Text style={hint}>Work through these from the top — a tick means it is already solved.</Text>
<TopicTable rows={data.topic.core} />
</Section>
) : null}
@@ -289,8 +330,8 @@ function DigestEmail({ data }: { data: DigestData }) {
/>
{data.carried > 0 ? (
<Text style={{ ...hint, margin: "12px 0 0" }}>
{data.carried} more {data.carried === 1 ? "is" : "are"} waiting — they come back
tomorrow, oldest first.
{data.carried} more {data.carried === 1 ? "is" : "are"} waiting — spread over the
coming days, oldest first, never more than {REVIEW_CAP} a day.
</Text>
) : null}
@@ -373,8 +414,11 @@ function plainDigest(data: DigestData): string {
out.push(
"",
`SOMETHING NEW TODAY — ${data.topic.name}`,
"Work through these; the ones marked done you have already solved.",
...data.topic.core.map((p) => ` LC ${p.lc}${p.solved ? " (done)" : ""} — ${p.url}`),
"Work through these from the top; the ones marked done you have already solved.",
...data.topic.core.map(
(p) =>
` LC ${p.lc} — ${DIFFICULTY[p.difficulty]?.label ?? p.difficulty}${p.solved ? " (done)" : ""} — ${p.url}`,
),
);
}
@@ -411,7 +455,7 @@ function plainDigest(data: DigestData): string {
if (data.carried > 0) {
out.push(
"",
`${data.carried} more ${data.carried === 1 ? "is" : "are"} waiting — they come back tomorrow, oldest first.`,
`${data.carried} more ${data.carried === 1 ? "is" : "are"} waiting — spread over the coming days, oldest first, never more than ${REVIEW_CAP} a day.`,
);
}
if (data.gate) {