2026-10-05 14:22:28 -04:00
---
/**
* `/appendix`: the glossary of LeetCode topic tags the campaign meets without
* teaching them as a topic (`appendix.ts`), one card per term, filed A– Z under
* a letter index. A card is the target of that tag's badge on every problem
* page (`/appendix#<slug>`, from `tagTarget()`), so it carries what the badge
* shows (the term's icon in the tag's colour from `lib/tag-colors.ts`, over
* the same tag universe `ProblemBadges.astro` colours) and lists the problem
* pages whose badge links here: the backlinks a per-term page used to have.
2026-10-14 16:03:07 -04:00
* Companies file into the same letters: one card per company any problem
2026-10-14 15:52:02 -04:00
* page's `companies:` names (`companies.ts`), the target of that company's
2026-10-14 16:03:07 -04:00
* pill in the page's company table, whose backlinks are the solved problems
* it asked in the last six months, each with how recently. The filter is a
* custom element over static markup, the way the Carousel is built; with no
* JS the page is the full glossary.
2026-10-05 14:22:28 -04:00
*/
import { getEntry } from "astro:content";
import data from "blume:data";
import Icon from "blume/components/Icon.astro";
import { withBase } from "blume/components/islands/base-path.ts";
import PageLayout from "blume/components/layout/PageLayout.astro";
import { APPENDIX } from "../appendix.ts";
2026-10-14 15:52:02 -04:00
import { RECENCY, RECENCY_LABEL, companyHref, type Company } from "../companies.ts";
2026-10-05 14:22:28 -04:00
import { tagColors } from "../lib/tag-colors.ts";
import { TOPIC_LANDINGS, tagTarget, type TopicTag } from "../topic-landings.ts";
const { config } = data;
const served = new Set<string>(data.routes.map((r: { path: string }) => r.path));
// Problem pages (`<num>. <name>` titles, the shape sync writes) with their tags.
const problems = await Promise.all(
data.routes
.filter((r: { title: string }) => /^\d+\.\s/u.test(r.title))
.map(async (r: { path: string; title: string; collection: string; entryId: string }) => {
const entry = await getEntry(r.collection, r.entryId);
const [, num, name] = r.title.match(/^(\d+)\.\s+(.+)$/u)!;
return {
route: r.path,
num: Number(num),
name: name!,
difficulty: entry?.data.sidebar?.badge as string | undefined,
tags: (entry?.data.topics ?? []) as TopicTag[],
2026-10-14 15:52:02 -04:00
companies: (entry?.data.companies ?? []) as Company[],
2026-10-05 14:22:28 -04:00
};
}),
);
const colors = tagColors(
[...Object.keys(TOPIC_LANDINGS), ...Object.keys(APPENDIX)],
problems.map((p) => p.tags.map((t) => t.slug)),
);
2026-10-14 16:03:07 -04:00
/** One card: a glossary term or a company, filed together under its first letter. */
interface Entry {
id: string;
name: string;
icon: string;
color?: string;
category: string;
definition?: string;
leetcode?: string;
problems: { route: string; num: number; name: string; difficulty?: string; note?: string }[];
}
2026-10-05 14:22:28 -04:00
2026-10-14 16:03:07 -04:00
const terms: Entry[] = Object.entries(APPENDIX).map(([slug, term]) => ({
id: slug,
...term,
color: colors.get(slug),
leetcode: `https://leetcode.com/problem-list/${slug}/`,
problems: problems
.filter((p) => p.tags.some((tag) => tagTarget(tag, p, served).href === `/appendix#${slug}`))
.sort((a, b) => a.num - b.num),
}));
// Every company a problem page names; its backlinks most recent window first,
// then by number.
const companies: Entry[] = [
2026-10-14 15:52:02 -04:00
...Map.groupBy(
problems.flatMap((p) => p.companies.map((c) => ({ ...p, company: c.name, period: c.period }))),
(row) => row.company,
),
2026-10-14 16:03:07 -04:00
].map(([name, rows]) => ({
id: companyHref(name).slice("/appendix#".length),
name,
icon: "building-2",
category: "Company",
problems: rows
.sort((a, b) => RECENCY.indexOf(a.period) - RECENCY.indexOf(b.period) || a.num - b.num)
.map((row) => ({ ...row, note: `within ${RECENCY_LABEL[row.period]}` })),
}));
2026-10-14 15:52:02 -04:00
2026-10-14 16:03:07 -04:00
const sections = new Map<string, Entry[]>();
for (const entry of [...terms, ...companies].sort((a, b) => a.name.localeCompare(b.name))) {
const letter = entry.name[0]!.toUpperCase();
sections.set(letter, [...(sections.get(letter) ?? []), entry]);
2026-10-05 14:22:28 -04:00
}
const LETTERS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
// A card's Solved list shows this many rows at a time; a longer one (Array has
// dozens) pages through them with numbered buttons under the list.
const PAGE_SIZE = 5;
// The /progress cards' difficulty palette (--chart-1..3 in theme.css), as on
// the changelog, so a Hard reads the same colour everywhere.
const DIFFICULTY: Record<string, { label: string; class: string }> = {
Easy: { label: "Easy", class: "border-chart-1/40 bg-chart-1/10 text-chart-1" },
Medium: { label: "Med.", class: "border-chart-2/40 bg-chart-2/10 text-chart-2" },
Hard: { label: "Hard", class: "border-chart-3/40 bg-chart-3/10 text-chart-3" },
};
const description =
2026-10-14 16:03:07 -04:00
"The LeetCode topics the campaign meets without teaching them as a topic, what each one means and the solved problems that use it, and the companies that asked those problems in the last six months.";
2026-10-05 14:22:28 -04:00
---
<PageLayout
site={{ title: config.title, description: config.description }}
logo={config.logo}
favicon={config.favicon}
banner={config.banner}
analytics={config.analytics}
navigation={data.navigation}
fontCssVars={data.fontCssVars}
themeMode={config.theme.mode}
searchEnabled={config.search.enabled}
siteUrl={config.site}
ogEnabled={config.og.enabled}
structuredDataEnabled={config.structuredData}
page={{ title: `Appendix - ${config.title}`, description, route: "/appendix" }}
>
<section class="mx-auto w-full max-w-5xl px-6 py-16">
<h1 class="m-0 text-4xl font-semibold tracking-tight text-foreground">Appendix</h1>
<p class="mt-3 max-w-3xl text-lg text-muted-foreground">{description}</p>
<appendix-filter class="mt-10 block">
<div class="flex flex-col gap-4 border-border border-b pb-6 sm:flex-row sm:items-center sm:justify-between">
<nav aria-label="Letters" class="flex flex-wrap gap-1 font-mono text-sm">
{
LETTERS.map((letter) =>
sections.has(letter) ? (
<a
href={`#letter-${letter}`}
data-letter-link={letter}
class="inline-flex size-7 items-center justify-center rounded-md text-foreground no-underline transition-colors hover:bg-muted"
>
{letter}
</a>
) : (
<span class="inline-flex size-7 items-center justify-center text-muted-foreground/40" aria-hidden="true">
{letter}
</span>
),
)
}
</nav>
<label class="relative block sm:w-64">
<span class="sr-only">Filter terms</span>
<Icon
class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-muted-foreground"
name="search"
size={14}
/>
<input
type="search"
2026-10-14 15:52:02 -04:00
placeholder={`Filter ${terms.length} terms, ${companies.length} companies`}
2026-10-05 14:22:28 -04:00
class="w-full rounded-md border border-border bg-background py-1.5 pr-3 pl-8 text-foreground text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/40"
/>
</label>
</div>
{
[...sections].map(([letter, group]) => (
<section id={`letter-${letter}`} data-letter={letter} class="scroll-mt-24 pt-10">
<h2 class="m-0 font-mono text-2xl font-semibold text-foreground">{letter}</h2>
<div class="mt-4 grid gap-4 sm:grid-cols-2">
2026-10-14 16:03:07 -04:00
{group.map((entry) => (
2026-10-05 14:22:28 -04:00
<article
2026-10-14 16:03:07 -04:00
id={entry.id}
data-term={`${entry.name} ${entry.category} ${entry.definition ?? ""}`.toLowerCase()}
2026-10-05 14:22:28 -04:00
class="flex scroll-mt-24 flex-col rounded-lg border border-border bg-muted/30 p-5 transition-colors target:border-foreground/40 target:bg-muted/60"
>
<header class="flex items-start gap-3">
<span
class="flex size-9 shrink-0 items-center justify-center rounded-md border border-border text-muted-foreground"
2026-10-14 16:03:07 -04:00
style={entry.color && `color: ${entry.color}; border-color: ${entry.color}66; background: ${entry.color}1f`}
2026-10-05 14:22:28 -04:00
>
2026-10-14 16:03:07 -04:00
<Icon name={entry.icon} size={18} />
2026-10-05 14:22:28 -04:00
</span>
<div class="min-w-0 flex-1">
<h3 class="m-0 text-base font-semibold leading-snug">
2026-10-14 16:03:07 -04:00
<a class="text-foreground no-underline hover:underline" href={`#${entry.id}`}>
{entry.name}
2026-10-05 14:22:28 -04:00
</a>
</h3>
<p class="m-0 mt-0.5 font-mono text-[11px] uppercase tracking-wide text-muted-foreground">
2026-10-14 16:03:07 -04:00
{entry.category}
2026-10-05 14:22:28 -04:00
</p>
</div>
2026-10-14 16:03:07 -04:00
{entry.leetcode && (
<a
class="shrink-0 font-mono text-xs text-muted-foreground no-underline hover:underline"
href={entry.leetcode}
target="_blank"
rel="noopener"
>
LeetCode ↗
</a>
)}
2026-10-05 14:22:28 -04:00
</header>
2026-10-14 16:03:07 -04:00
{entry.definition && (
<p class="mt-3 mb-0 text-sm leading-relaxed text-muted-foreground">{entry.definition}</p>
)}
{entry.problems.length > 0 && (
2026-10-05 14:22:28 -04:00
<details class="group mt-4 border-border border-t pt-3" open>
<summary class="mb-1 flex cursor-pointer list-none items-center justify-between gap-2 text-muted-foreground transition-colors hover:text-foreground [&::-webkit-details-marker]:hidden">
<span class="flex items-baseline gap-2 font-medium text-xs uppercase tracking-wide">
Solved
2026-10-14 16:03:07 -04:00
<span class="tabular-nums">{entry.problems.length}</span>
2026-10-05 14:22:28 -04:00
</span>
<Icon class="shrink-0 transition-transform group-open:rotate-180" name="chevron-down" size={14} />
</summary>
<solved-pages class="block">
<ul class="m-0 list-none p-0 text-sm">
2026-10-14 16:03:07 -04:00
{entry.problems.map((p, i) => (
2026-10-05 14:22:28 -04:00
<li data-page={Math.floor(i / PAGE_SIZE)}>
<a
class="-mx-2 flex items-baseline gap-3 rounded-md px-2 py-1 text-foreground no-underline transition-colors hover:bg-muted"
href={withBase(p.route)}
>
<span class="w-10 shrink-0 text-right font-mono text-muted-foreground text-xs tabular-nums">
{p.num}
</span>
<span class="min-w-0 flex-1 truncate">{p.name}</span>
2026-10-14 16:03:07 -04:00
{p.note && <span class="shrink-0 text-muted-foreground text-xs">{p.note}</span>}
2026-10-05 14:22:28 -04:00
{p.difficulty && DIFFICULTY[p.difficulty] && (
<span
class:list={[
"shrink-0 rounded-full border px-2 py-px font-mono text-[11px] leading-4",
DIFFICULTY[p.difficulty]!.class,
]}
>
{DIFFICULTY[p.difficulty]!.label}
</span>
)}
</a>
</li>
))}
</ul>
2026-10-14 16:03:07 -04:00
{entry.problems.length > PAGE_SIZE && (
2026-10-05 14:22:28 -04:00
<nav
2026-10-14 16:03:07 -04:00
aria-label={`${entry.name} solved pages`}
class="mt-2 flex flex-nowrap justify-center gap-1 font-mono text-xs"
2026-10-05 14:22:28 -04:00
hidden
>
2026-10-14 16:03:07 -04:00
{Array.from({ length: Math.ceil(entry.problems.length / PAGE_SIZE) }, (_, page) => (
2026-10-05 14:22:28 -04:00
<button
type="button"
data-goto={page}
aria-label={`Page ${page + 1}`}
class="inline-flex size-7 cursor-pointer items-center justify-center rounded-md border border-transparent text-muted-foreground tabular-nums transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:border-border aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
>
{page + 1}
</button>
))}
</nav>
)}
</solved-pages>
</details>
)}
</article>
))}
</div>
</section>
))
}
2026-10-14 15:52:02 -04:00
<p data-empty hidden class="mt-10 text-muted-foreground">Nothing matches that filter.</p>
2026-10-05 14:22:28 -04:00
</appendix-filter>
</section>
</PageLayout>
<script>
/** Hides the cards (and letters) the filter text does not match. */
class AppendixFilter extends HTMLElement {
connectedCallback() {
const input = this.querySelector("input");
const empty = this.querySelector<HTMLElement>("[data-empty]");
if (!input || !empty) return;
input.addEventListener("input", () => {
const query = input.value.trim().toLowerCase();
let shown = 0;
for (const section of this.querySelectorAll<HTMLElement>("[data-letter]")) {
let visible = 0;
for (const card of section.querySelectorAll<HTMLElement>("[data-term]")) {
card.hidden = !card.dataset.term!.includes(query);
if (!card.hidden) visible++;
}
section.hidden = visible === 0;
const link = this.querySelector<HTMLElement>(`[data-letter-link="${section.dataset.letter}"]`);
link?.classList.toggle("opacity-30", visible === 0);
shown += visible;
}
empty.hidden = shown > 0;
});
}
}
customElements.define("appendix-filter", AppendixFilter);
/**
* Pages a card's Solved list: shows the rows of one page and marks its
* number button current. Server markup lists every row with the buttons
2026-10-14 16:03:07 -04:00
* hidden, so without JS the card shows the whole list. Only the first,
* last, current and its neighbours stay visible, a "…" standing for each
* gap, so a long list (Amazon has 20+ pages) still pages on one line.
2026-10-05 14:22:28 -04:00
*/
class SolvedPages extends HTMLElement {
connectedCallback() {
const nav = this.querySelector<HTMLElement>("nav");
if (!nav) return;
const rows = this.querySelectorAll<HTMLElement>("li[data-page]");
2026-10-14 16:03:07 -04:00
const buttons = [...nav.querySelectorAll<HTMLButtonElement>("button[data-goto]")];
const last = buttons.length - 1;
2026-10-05 14:22:28 -04:00
const show = (page: string) => {
2026-10-14 16:03:07 -04:00
const current = Number(page);
2026-10-05 14:22:28 -04:00
for (const row of rows) row.hidden = row.dataset.page !== page;
2026-10-14 16:03:07 -04:00
for (const gap of nav.querySelectorAll("[data-gap]")) gap.remove();
let previous = -1;
buttons.forEach((button, i) => {
if (i === current) button.setAttribute("aria-current", "page");
2026-10-05 14:22:28 -04:00
else button.removeAttribute("aria-current");
2026-10-14 16:03:07 -04:00
button.hidden = i !== 0 && i !== last && Math.abs(i - current) > 1;
if (button.hidden) return;
if (i - previous > 1 && previous >= 0) {
const gap = document.createElement("span");
gap.dataset.gap = "";
gap.ariaHidden = "true";
gap.className = "inline-flex size-7 items-center justify-center text-muted-foreground";
gap.textContent = "…";
button.before(gap);
}
previous = i;
});
2026-10-05 14:22:28 -04:00
};
nav.addEventListener("click", (event) => {
const button = (event.target as Element).closest<HTMLButtonElement>("button[data-goto]");
if (button) show(button.dataset.goto!);
});
show("0");
nav.hidden = false;
}
}
customElements.define("solved-pages", SolvedPages);
</script>