mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
docs(appendix): unify terms and companies entries, add LeetCode links, refine description and navigation rendering
This commit is contained in:
1 parent
ddf5dc483c
commit
f33ded37ec
1 file changed
+80
-134
+80
-134
@@ -7,12 +7,12 @@
|
|||||||
* shows (the term's icon in the tag's colour from `lib/tag-colors.ts`, over
|
* 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
|
* 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.
|
* pages whose badge links here: the backlinks a per-term page used to have.
|
||||||
* After the terms, a Companies section: one card per company any problem
|
* Companies file into the same letters: one card per company any problem
|
||||||
* page's `companies:` names (`companies.ts`), the target of that company's
|
* page's `companies:` names (`companies.ts`), the target of that company's
|
||||||
* pill in the page's company table, listing the solved problems it asked in
|
* pill in the page's company table, whose backlinks are the solved problems
|
||||||
* the last six months and how recently. The filter is a custom element over
|
* it asked in the last six months, each with how recently. The filter is a
|
||||||
* static markup, the way the Carousel is built; with no JS the page is the
|
* custom element over static markup, the way the Carousel is built; with no
|
||||||
* full glossary.
|
* JS the page is the full glossary.
|
||||||
*/
|
*/
|
||||||
import { getEntry } from "astro:content";
|
import { getEntry } from "astro:content";
|
||||||
import data from "blume:data";
|
import data from "blume:data";
|
||||||
@@ -50,36 +50,49 @@ const colors = tagColors(
|
|||||||
problems.map((p) => p.tags.map((t) => t.slug)),
|
problems.map((p) => p.tags.map((t) => t.slug)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const terms = Object.entries(APPENDIX)
|
/** One card: a glossary term or a company, filed together under its first letter. */
|
||||||
.map(([slug, term]) => ({
|
interface Entry {
|
||||||
slug,
|
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 }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const terms: Entry[] = Object.entries(APPENDIX).map(([slug, term]) => ({
|
||||||
|
id: slug,
|
||||||
...term,
|
...term,
|
||||||
color: colors.get(slug),
|
color: colors.get(slug),
|
||||||
|
leetcode: `https://leetcode.com/problem-list/${slug}/`,
|
||||||
problems: problems
|
problems: problems
|
||||||
.filter((p) => p.tags.some((tag) => tagTarget(tag, p, served).href === `/appendix#${slug}`))
|
.filter((p) => p.tags.some((tag) => tagTarget(tag, p, served).href === `/appendix#${slug}`))
|
||||||
.sort((a, b) => a.num - b.num),
|
.sort((a, b) => a.num - b.num),
|
||||||
}))
|
}));
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
// Every company a problem page names, A–Z; its solved problems most recent
|
// Every company a problem page names; its backlinks most recent window first,
|
||||||
// window first, then by number.
|
// then by number.
|
||||||
const companies = [
|
const companies: Entry[] = [
|
||||||
...Map.groupBy(
|
...Map.groupBy(
|
||||||
problems.flatMap((p) => p.companies.map((c) => ({ ...p, company: c.name, period: c.period }))),
|
problems.flatMap((p) => p.companies.map((c) => ({ ...p, company: c.name, period: c.period }))),
|
||||||
(row) => row.company,
|
(row) => row.company,
|
||||||
),
|
),
|
||||||
]
|
].map(([name, rows]) => ({
|
||||||
.map(([name, rows]) => ({
|
|
||||||
name,
|
|
||||||
id: companyHref(name).slice("/appendix#".length),
|
id: companyHref(name).slice("/appendix#".length),
|
||||||
problems: rows.sort((a, b) => RECENCY.indexOf(a.period) - RECENCY.indexOf(b.period) || a.num - b.num),
|
name,
|
||||||
}))
|
icon: "building-2",
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
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]}` })),
|
||||||
|
}));
|
||||||
|
|
||||||
const sections = new Map<string, typeof terms>();
|
const sections = new Map<string, Entry[]>();
|
||||||
for (const term of terms) {
|
for (const entry of [...terms, ...companies].sort((a, b) => a.name.localeCompare(b.name))) {
|
||||||
const letter = term.name[0]!.toUpperCase();
|
const letter = entry.name[0]!.toUpperCase();
|
||||||
sections.set(letter, [...(sections.get(letter) ?? []), term]);
|
sections.set(letter, [...(sections.get(letter) ?? []), entry]);
|
||||||
}
|
}
|
||||||
const LETTERS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
const LETTERS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
||||||
// A card's Solved list shows this many rows at a time; a longer one (Array has
|
// A card's Solved list shows this many rows at a time; a longer one (Array has
|
||||||
@@ -94,7 +107,7 @@ const DIFFICULTY: Record<string, { label: string; class: string }> = {
|
|||||||
Hard: { label: "Hard", class: "border-chart-3/40 bg-chart-3/10 text-chart-3" },
|
Hard: { label: "Hard", class: "border-chart-3/40 bg-chart-3/10 text-chart-3" },
|
||||||
};
|
};
|
||||||
const description =
|
const description =
|
||||||
"The LeetCode topics the campaign meets without teaching them as a topic, what each one means and the solved problems that use it, then the companies that asked those problems in the last six months.";
|
"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.";
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout
|
<PageLayout
|
||||||
@@ -136,17 +149,6 @@ const description =
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
companies.length > 0 && (
|
|
||||||
<a
|
|
||||||
href="#companies"
|
|
||||||
data-letter-link="companies"
|
|
||||||
class="inline-flex h-7 items-center justify-center rounded-md px-2 text-foreground no-underline transition-colors hover:bg-muted"
|
|
||||||
>
|
|
||||||
Companies
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</nav>
|
</nav>
|
||||||
<label class="relative block sm:w-64">
|
<label class="relative block sm:w-64">
|
||||||
<span class="sr-only">Filter terms</span>
|
<span class="sr-only">Filter terms</span>
|
||||||
@@ -168,51 +170,55 @@ const description =
|
|||||||
<section id={`letter-${letter}`} data-letter={letter} class="scroll-mt-24 pt-10">
|
<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>
|
<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">
|
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||||
{group.map((term) => (
|
{group.map((entry) => (
|
||||||
<article
|
<article
|
||||||
id={term.slug}
|
id={entry.id}
|
||||||
data-term={`${term.name} ${term.category} ${term.definition}`.toLowerCase()}
|
data-term={`${entry.name} ${entry.category} ${entry.definition ?? ""}`.toLowerCase()}
|
||||||
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"
|
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">
|
<header class="flex items-start gap-3">
|
||||||
<span
|
<span
|
||||||
class="flex size-9 shrink-0 items-center justify-center rounded-md border border-border text-muted-foreground"
|
class="flex size-9 shrink-0 items-center justify-center rounded-md border border-border text-muted-foreground"
|
||||||
style={term.color && `color: ${term.color}; border-color: ${term.color}66; background: ${term.color}1f`}
|
style={entry.color && `color: ${entry.color}; border-color: ${entry.color}66; background: ${entry.color}1f`}
|
||||||
>
|
>
|
||||||
<Icon name={term.icon} size={18} />
|
<Icon name={entry.icon} size={18} />
|
||||||
</span>
|
</span>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<h3 class="m-0 text-base font-semibold leading-snug">
|
<h3 class="m-0 text-base font-semibold leading-snug">
|
||||||
<a class="text-foreground no-underline hover:underline" href={`#${term.slug}`}>
|
<a class="text-foreground no-underline hover:underline" href={`#${entry.id}`}>
|
||||||
{term.name}
|
{entry.name}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="m-0 mt-0.5 font-mono text-[11px] uppercase tracking-wide text-muted-foreground">
|
<p class="m-0 mt-0.5 font-mono text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||||
{term.category}
|
{entry.category}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{entry.leetcode && (
|
||||||
<a
|
<a
|
||||||
class="shrink-0 font-mono text-xs text-muted-foreground no-underline hover:underline"
|
class="shrink-0 font-mono text-xs text-muted-foreground no-underline hover:underline"
|
||||||
href={`https://leetcode.com/problem-list/${term.slug}/`}
|
href={entry.leetcode}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
LeetCode ↗
|
LeetCode ↗
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
<p class="mt-3 mb-0 text-sm leading-relaxed text-muted-foreground">{term.definition}</p>
|
{entry.definition && (
|
||||||
{term.problems.length > 0 && (
|
<p class="mt-3 mb-0 text-sm leading-relaxed text-muted-foreground">{entry.definition}</p>
|
||||||
|
)}
|
||||||
|
{entry.problems.length > 0 && (
|
||||||
<details class="group mt-4 border-border border-t pt-3" open>
|
<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">
|
<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">
|
<span class="flex items-baseline gap-2 font-medium text-xs uppercase tracking-wide">
|
||||||
Solved
|
Solved
|
||||||
<span class="tabular-nums">{term.problems.length}</span>
|
<span class="tabular-nums">{entry.problems.length}</span>
|
||||||
</span>
|
</span>
|
||||||
<Icon class="shrink-0 transition-transform group-open:rotate-180" name="chevron-down" size={14} />
|
<Icon class="shrink-0 transition-transform group-open:rotate-180" name="chevron-down" size={14} />
|
||||||
</summary>
|
</summary>
|
||||||
<solved-pages class="block">
|
<solved-pages class="block">
|
||||||
<ul class="m-0 list-none p-0 text-sm">
|
<ul class="m-0 list-none p-0 text-sm">
|
||||||
{term.problems.map((p, i) => (
|
{entry.problems.map((p, i) => (
|
||||||
<li data-page={Math.floor(i / PAGE_SIZE)}>
|
<li data-page={Math.floor(i / PAGE_SIZE)}>
|
||||||
<a
|
<a
|
||||||
class="-mx-2 flex items-baseline gap-3 rounded-md px-2 py-1 text-foreground no-underline transition-colors hover:bg-muted"
|
class="-mx-2 flex items-baseline gap-3 rounded-md px-2 py-1 text-foreground no-underline transition-colors hover:bg-muted"
|
||||||
@@ -222,6 +228,7 @@ const description =
|
|||||||
{p.num}
|
{p.num}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-0 flex-1 truncate">{p.name}</span>
|
<span class="min-w-0 flex-1 truncate">{p.name}</span>
|
||||||
|
{p.note && <span class="shrink-0 text-muted-foreground text-xs">{p.note}</span>}
|
||||||
{p.difficulty && DIFFICULTY[p.difficulty] && (
|
{p.difficulty && DIFFICULTY[p.difficulty] && (
|
||||||
<span
|
<span
|
||||||
class:list={[
|
class:list={[
|
||||||
@@ -236,13 +243,13 @@ const description =
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{term.problems.length > PAGE_SIZE && (
|
{entry.problems.length > PAGE_SIZE && (
|
||||||
<nav
|
<nav
|
||||||
aria-label={`${term.name} solved pages`}
|
aria-label={`${entry.name} solved pages`}
|
||||||
class="mt-2 flex flex-wrap justify-center gap-1 font-mono text-xs"
|
class="mt-2 flex flex-nowrap justify-center gap-1 font-mono text-xs"
|
||||||
hidden
|
hidden
|
||||||
>
|
>
|
||||||
{Array.from({ length: Math.ceil(term.problems.length / PAGE_SIZE) }, (_, page) => (
|
{Array.from({ length: Math.ceil(entry.problems.length / PAGE_SIZE) }, (_, page) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-goto={page}
|
data-goto={page}
|
||||||
@@ -263,84 +270,6 @@ const description =
|
|||||||
</section>
|
</section>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
{
|
|
||||||
companies.length > 0 && (
|
|
||||||
<section id="companies" data-letter="companies" class="scroll-mt-24 pt-10">
|
|
||||||
<h2 class="m-0 text-2xl font-semibold text-foreground">Companies</h2>
|
|
||||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
|
||||||
{companies.map((company) => (
|
|
||||||
<article
|
|
||||||
id={company.id}
|
|
||||||
data-term={`${company.name} company`.toLowerCase()}
|
|
||||||
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">
|
|
||||||
<Icon name="building-2" size={18} />
|
|
||||||
</span>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<h3 class="m-0 text-base font-semibold leading-snug">
|
|
||||||
<a class="text-foreground no-underline hover:underline" href={`#${company.id}`}>
|
|
||||||
{company.name}
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p class="m-0 mt-0.5 font-mono text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
||||||
Company · {company.problems.length} solved
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<solved-pages class="mt-4 block border-border border-t pt-3">
|
|
||||||
<ul class="m-0 list-none p-0 text-sm">
|
|
||||||
{company.problems.map((p, i) => (
|
|
||||||
<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>
|
|
||||||
<span class="shrink-0 text-muted-foreground text-xs">within {RECENCY_LABEL[p.period]}</span>
|
|
||||||
{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>
|
|
||||||
{company.problems.length > PAGE_SIZE && (
|
|
||||||
<nav
|
|
||||||
aria-label={`${company.name} solved pages`}
|
|
||||||
class="mt-2 flex flex-wrap justify-center gap-1 font-mono text-xs"
|
|
||||||
hidden
|
|
||||||
>
|
|
||||||
{Array.from({ length: Math.ceil(company.problems.length / PAGE_SIZE) }, (_, page) => (
|
|
||||||
<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>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<p data-empty hidden class="mt-10 text-muted-foreground">Nothing matches that filter.</p>
|
<p data-empty hidden class="mt-10 text-muted-foreground">Nothing matches that filter.</p>
|
||||||
</appendix-filter>
|
</appendix-filter>
|
||||||
</section>
|
</section>
|
||||||
@@ -376,20 +305,37 @@ const description =
|
|||||||
/**
|
/**
|
||||||
* Pages a card's Solved list: shows the rows of one page and marks its
|
* 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
|
* number button current. Server markup lists every row with the buttons
|
||||||
* hidden, so without JS the card shows the whole list.
|
* 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.
|
||||||
*/
|
*/
|
||||||
class SolvedPages extends HTMLElement {
|
class SolvedPages extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const nav = this.querySelector<HTMLElement>("nav");
|
const nav = this.querySelector<HTMLElement>("nav");
|
||||||
if (!nav) return;
|
if (!nav) return;
|
||||||
const rows = this.querySelectorAll<HTMLElement>("li[data-page]");
|
const rows = this.querySelectorAll<HTMLElement>("li[data-page]");
|
||||||
const buttons = nav.querySelectorAll<HTMLButtonElement>("button[data-goto]");
|
const buttons = [...nav.querySelectorAll<HTMLButtonElement>("button[data-goto]")];
|
||||||
|
const last = buttons.length - 1;
|
||||||
const show = (page: string) => {
|
const show = (page: string) => {
|
||||||
|
const current = Number(page);
|
||||||
for (const row of rows) row.hidden = row.dataset.page !== page;
|
for (const row of rows) row.hidden = row.dataset.page !== page;
|
||||||
for (const button of buttons) {
|
for (const gap of nav.querySelectorAll("[data-gap]")) gap.remove();
|
||||||
if (button.dataset.goto === page) button.setAttribute("aria-current", "page");
|
let previous = -1;
|
||||||
|
buttons.forEach((button, i) => {
|
||||||
|
if (i === current) button.setAttribute("aria-current", "page");
|
||||||
else button.removeAttribute("aria-current");
|
else button.removeAttribute("aria-current");
|
||||||
|
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;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
nav.addEventListener("click", (event) => {
|
nav.addEventListener("click", (event) => {
|
||||||
const button = (event.target as Element).closest<HTMLButtonElement>("button[data-goto]");
|
const button = (event.target as Element).closest<HTMLButtonElement>("button[data-goto]");
|
||||||
|
|||||||
Reference in new issue
Block a user