mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
feat(docs): add Appendix glossary and page for undocumented tags
This commit is contained in:
1 parent
0e9e06eb8d
commit
c2a375fffc
5 files changed
+549
-15
No files matched your search
@@ -0,0 +1,232 @@
|
|||||||
|
/**
|
||||||
|
* The Appendix glossary: one term per LeetCode topic tag that problem pages
|
||||||
|
* carry but no campaign topic teaches (not a key of `topic-landings.ts`).
|
||||||
|
* `pages/appendix.astro` renders each as a card — icon, category, definition,
|
||||||
|
* and the solved problems carrying the tag — anchored at `/appendix#<slug>`,
|
||||||
|
* which is where `tagTarget()` sends that tag's badge on a problem page.
|
||||||
|
*
|
||||||
|
* Hand-written: `apps/cli/sync.ts` only checks it, warning about a carried
|
||||||
|
* tag with no entry (its card renders bare), an entry whose tag a campaign
|
||||||
|
* topic now teaches (dead weight, delete it), and an icon that is a topic's
|
||||||
|
* (`TOPIC_ICONS`) or another term's — the icon is also the one on every
|
||||||
|
* badge linking here, so it must not read as anything else. Plain data, no
|
||||||
|
* Astro imports: sync imports this file under Bun.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type AppendixCategory = "Data Structures" | "Algorithms" | "Techniques";
|
||||||
|
|
||||||
|
export interface AppendixTerm {
|
||||||
|
/** LeetCode's tag name. */
|
||||||
|
name: string;
|
||||||
|
/** Lucide icon, unique across terms and topics. */
|
||||||
|
icon: string;
|
||||||
|
category: AppendixCategory;
|
||||||
|
/** One sentence: what the term means, the way a solution uses it. */
|
||||||
|
definition: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APPENDIX: Record<string, AppendixTerm> = {
|
||||||
|
// ── data structures ──────────────────────────────────────────────
|
||||||
|
array: {
|
||||||
|
name: "Array",
|
||||||
|
icon: "list-ordered",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "A contiguous, index-addressed sequence: O(1) access by position, O(n) to insert or delete in the middle.",
|
||||||
|
},
|
||||||
|
"binary-tree": {
|
||||||
|
name: "Binary Tree",
|
||||||
|
icon: "git-branch",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "A rooted tree in which every node has at most two children, left and right.",
|
||||||
|
},
|
||||||
|
"directed-acyclic-graph": {
|
||||||
|
name: "Directed Acyclic Graph",
|
||||||
|
icon: "route",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "A directed graph with no cycles, which is exactly what lets its nodes be put in a topological order.",
|
||||||
|
},
|
||||||
|
"doubly-linked-list": {
|
||||||
|
name: "Doubly-Linked List",
|
||||||
|
icon: "link-2",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "A linked list whose nodes point to both neighbours, so a node you already hold is unlinked in O(1).",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
name: "Graph Theory",
|
||||||
|
icon: "share-2",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "Nodes joined by edges, directed or not, weighted or not, stored as an adjacency list or matrix.",
|
||||||
|
},
|
||||||
|
"k-d-tree": {
|
||||||
|
name: "K-D Tree",
|
||||||
|
icon: "axis-3d",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "A binary tree that splits k-dimensional points on alternating axes, for nearest-neighbour and range queries.",
|
||||||
|
},
|
||||||
|
queue: {
|
||||||
|
name: "Queue",
|
||||||
|
icon: "list-end",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "First in, first out: push at the back, pop from the front, both O(1).",
|
||||||
|
},
|
||||||
|
string: {
|
||||||
|
name: "String",
|
||||||
|
icon: "quote",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "A sequence of characters; solutions lean on its small alphabet, its substrings, and its immutability.",
|
||||||
|
},
|
||||||
|
tree: {
|
||||||
|
name: "Tree",
|
||||||
|
icon: "tree-deciduous",
|
||||||
|
category: "Data Structures",
|
||||||
|
definition: "A connected acyclic graph, usually rooted, in which every node but the root has exactly one parent.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── algorithms ───────────────────────────────────────────────────
|
||||||
|
"bidirectional-search": {
|
||||||
|
name: "Bidirectional Search",
|
||||||
|
icon: "move-horizontal",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Search from the start and the goal at once and stop where the frontiers meet, about 2·b^(d/2) states instead of b^d.",
|
||||||
|
},
|
||||||
|
"binary-lifting": {
|
||||||
|
name: "Binary Lifting",
|
||||||
|
icon: "chevrons-up",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Precompute every node's 2^k-th ancestor so any k-th ancestor, and so the LCA, takes O(log n) jumps.",
|
||||||
|
},
|
||||||
|
"bit-manipulation": {
|
||||||
|
name: "Bit Manipulation",
|
||||||
|
icon: "binary",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Treat integers as bit vectors: masks, shifts, and XOR in place of arithmetic or extra memory.",
|
||||||
|
},
|
||||||
|
"bracket-sequences": {
|
||||||
|
name: "Bracket Sequences",
|
||||||
|
icon: "parentheses",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Strings of opening and closing brackets; balanced means the running depth never dips below zero and ends at zero.",
|
||||||
|
},
|
||||||
|
"bucket-sort": {
|
||||||
|
name: "Bucket Sort",
|
||||||
|
icon: "container",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Scatter values into buckets by range or frequency, then read the buckets in order: linear when the key range is bounded.",
|
||||||
|
},
|
||||||
|
"complete-knapsack": {
|
||||||
|
name: "Complete Knapsack",
|
||||||
|
icon: "package-plus",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Knapsack in which every item may be taken any number of times, so the DP walks capacity upward and reuses the row.",
|
||||||
|
},
|
||||||
|
"divide-and-conquer": {
|
||||||
|
name: "Divide and Conquer",
|
||||||
|
icon: "split",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Split the input into independent parts, solve each recursively, and combine their answers.",
|
||||||
|
},
|
||||||
|
"floyds-cycle-finding-algorithm": {
|
||||||
|
name: "Floyd's Cycle Finding Algorithm",
|
||||||
|
icon: "repeat",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "A slow and a fast pointer meet inside any cycle; restarting one from the head finds the cycle's entry, in O(1) space.",
|
||||||
|
},
|
||||||
|
"knapsack-problem": {
|
||||||
|
name: "Knapsack Problem",
|
||||||
|
icon: "backpack",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Pick items with weights and values to maximise value under a capacity; the 0/1 version is an O(n·W) DP.",
|
||||||
|
},
|
||||||
|
"longest-common-subsequence": {
|
||||||
|
name: "Longest Common Subsequence",
|
||||||
|
icon: "diff",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "The longest sequence that appears in order, not necessarily contiguously, in two strings: the classic O(n·m) 2-D DP.",
|
||||||
|
},
|
||||||
|
"lowest-common-ancestor": {
|
||||||
|
name: "Lowest Common Ancestor",
|
||||||
|
icon: "git-merge",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "The deepest node of a rooted tree that is an ancestor of both given nodes.",
|
||||||
|
},
|
||||||
|
memoization: {
|
||||||
|
name: "Memoization",
|
||||||
|
icon: "database",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Cache a pure function's result per argument so every subproblem is solved once: top-down dynamic programming.",
|
||||||
|
},
|
||||||
|
"merge-sort": {
|
||||||
|
name: "Merge Sort",
|
||||||
|
icon: "blend",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Sort each half recursively, then merge the two sorted halves: O(n log n), stable, O(n) extra space.",
|
||||||
|
},
|
||||||
|
quickselect: {
|
||||||
|
name: "Quickselect",
|
||||||
|
icon: "crosshair",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Partition around a pivot and recurse into one side only, finding the k-th smallest element in O(n) on average.",
|
||||||
|
},
|
||||||
|
quicksort: {
|
||||||
|
name: "Quicksort",
|
||||||
|
icon: "shuffle",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Partition around a pivot, then sort each side in place: O(n log n) on average, O(n²) at worst.",
|
||||||
|
},
|
||||||
|
recursion: {
|
||||||
|
name: "Recursion",
|
||||||
|
icon: "iteration-ccw",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Solve a problem by calling the same function on smaller inputs until a base case answers directly.",
|
||||||
|
},
|
||||||
|
simulation: {
|
||||||
|
name: "Simulation",
|
||||||
|
icon: "monitor-play",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Carry out the process the statement describes, step by step; the work is modelling the state faithfully.",
|
||||||
|
},
|
||||||
|
sorting: {
|
||||||
|
name: "Sorting",
|
||||||
|
icon: "arrow-down-wide-narrow",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "Order the input first so a later step, such as two pointers, a greedy choice, or binary search, becomes possible.",
|
||||||
|
},
|
||||||
|
"tournament-sort": {
|
||||||
|
name: "Tournament Sort",
|
||||||
|
icon: "trophy",
|
||||||
|
category: "Algorithms",
|
||||||
|
definition: "A winner tree of pairwise comparisons: the root is the extreme element, and replaying one path yields the next in O(log n).",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── techniques ───────────────────────────────────────────────────
|
||||||
|
combinatorics: {
|
||||||
|
name: "Combinatorics",
|
||||||
|
icon: "dices",
|
||||||
|
category: "Techniques",
|
||||||
|
definition: "Count arrangements and selections (permutations, combinations, Catalan numbers), usually modulo a prime.",
|
||||||
|
},
|
||||||
|
counting: {
|
||||||
|
name: "Counting",
|
||||||
|
icon: "tally-5",
|
||||||
|
category: "Techniques",
|
||||||
|
definition: "Tally occurrences in a hash map or fixed array, then answer from the counts instead of the raw input.",
|
||||||
|
},
|
||||||
|
design: {
|
||||||
|
name: "Design",
|
||||||
|
icon: "pencil-ruler",
|
||||||
|
category: "Techniques",
|
||||||
|
definition: "Implement a class to an API spec, choosing the internals so every operation meets its time bound.",
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
name: "Geometry",
|
||||||
|
icon: "shapes",
|
||||||
|
category: "Techniques",
|
||||||
|
definition: "Points, lines, and areas on a plane or grid: distances, slopes, overlaps, and orientation by cross product.",
|
||||||
|
},
|
||||||
|
math: {
|
||||||
|
name: "Math",
|
||||||
|
icon: "calculator",
|
||||||
|
category: "Techniques",
|
||||||
|
definition: "An arithmetic or number-theory insight (modular arithmetic, GCD, digit manipulation, a closed form) in place of brute force.",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ export default defineComponents({
|
|||||||
wrapper: "./components/ProblemBadges.astro",
|
wrapper: "./components/ProblemBadges.astro",
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
// Topic landing + guide pages' "what links here": every problem pointing at the page.
|
// Topic landing pages' "what links here": every problem pointing at the page.
|
||||||
PageFooter: "./components/BacklinksList.astro",
|
PageFooter: "./components/BacklinksList.astro",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* `/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.
|
||||||
|
* The filter is a custom element over static markup, the way the Carousel
|
||||||
|
* is built; with no JS the page is the full glossary.
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
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[],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const colors = tagColors(
|
||||||
|
[...Object.keys(TOPIC_LANDINGS), ...Object.keys(APPENDIX)],
|
||||||
|
problems.map((p) => p.tags.map((t) => t.slug)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const terms = Object.entries(APPENDIX)
|
||||||
|
.map(([slug, term]) => ({
|
||||||
|
slug,
|
||||||
|
...term,
|
||||||
|
color: colors.get(slug),
|
||||||
|
problems: problems
|
||||||
|
.filter((p) => p.tags.some((tag) => tagTarget(tag, p, served).href === `/appendix#${slug}`))
|
||||||
|
.sort((a, b) => a.num - b.num),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const sections = new Map<string, typeof terms>();
|
||||||
|
for (const term of terms) {
|
||||||
|
const letter = term.name[0]!.toUpperCase();
|
||||||
|
sections.set(letter, [...(sections.get(letter) ?? []), term]);
|
||||||
|
}
|
||||||
|
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 =
|
||||||
|
"The LeetCode topics the campaign meets without teaching them as a topic: what each one means, and the solved problems that use it.";
|
||||||
|
---
|
||||||
|
|
||||||
|
<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"
|
||||||
|
placeholder={`Filter ${terms.length} terms`}
|
||||||
|
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">
|
||||||
|
{group.map((term) => (
|
||||||
|
<article
|
||||||
|
id={term.slug}
|
||||||
|
data-term={`${term.name} ${term.category} ${term.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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
style={term.color && `color: ${term.color}; border-color: ${term.color}66; background: ${term.color}1f`}
|
||||||
|
>
|
||||||
|
<Icon name={term.icon} 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={`#${term.slug}`}>
|
||||||
|
{term.name}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p class="m-0 mt-0.5 font-mono text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||||
|
{term.category}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="shrink-0 font-mono text-xs text-muted-foreground no-underline hover:underline"
|
||||||
|
href={`https://leetcode.com/problem-list/${term.slug}/`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
LeetCode ↗
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<p class="mt-3 mb-0 text-sm leading-relaxed text-muted-foreground">{term.definition}</p>
|
||||||
|
{term.problems.length > 0 && (
|
||||||
|
<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
|
||||||
|
<span class="tabular-nums">{term.problems.length}</span>
|
||||||
|
</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">
|
||||||
|
{term.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>
|
||||||
|
{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>
|
||||||
|
{term.problems.length > PAGE_SIZE && (
|
||||||
|
<nav
|
||||||
|
aria-label={`${term.name} solved pages`}
|
||||||
|
class="mt-2 flex flex-wrap justify-center gap-1 font-mono text-xs"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
{Array.from({ length: Math.ceil(term.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>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<p data-empty hidden class="mt-10 text-muted-foreground">No term matches that filter.</p>
|
||||||
|
</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
|
||||||
|
* hidden, so without JS the card shows the whole list.
|
||||||
|
*/
|
||||||
|
class SolvedPages extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
const nav = this.querySelector<HTMLElement>("nav");
|
||||||
|
if (!nav) return;
|
||||||
|
const rows = this.querySelectorAll<HTMLElement>("li[data-page]");
|
||||||
|
const buttons = nav.querySelectorAll<HTMLButtonElement>("button[data-goto]");
|
||||||
|
const show = (page: string) => {
|
||||||
|
for (const row of rows) row.hidden = row.dataset.page !== page;
|
||||||
|
for (const button of buttons) {
|
||||||
|
if (button.dataset.goto === page) button.setAttribute("aria-current", "page");
|
||||||
|
else button.removeAttribute("aria-current");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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>
|
||||||
+15
-13
@@ -4,16 +4,19 @@
|
|||||||
* tag", read by the sides that must agree:
|
* tag", read by the sides that must agree:
|
||||||
*
|
*
|
||||||
* - `components/ProblemBadges.astro` links a covered tag's badge to its
|
* - `components/ProblemBadges.astro` links a covered tag's badge to its
|
||||||
* landing page, and every other tag's badge to its Appendix guide, `/appendix/<slug>`;
|
* landing page, and every other tag's badge to its Appendix card,
|
||||||
* - `components/BacklinksList.astro` lists, under a landing or guide page,
|
* `/appendix#<slug>` (`appendix.ts`);
|
||||||
* every problem whose badge links there — both via `tagTarget()`;
|
* - `components/BacklinksList.astro` lists, under a landing page, every
|
||||||
* - `apps/cli/sync.ts` creates a placeholder `content/appendix/<slug>.mdx` for
|
* problem whose badge links there — both via `tagTarget()`;
|
||||||
* every tag a problem page carries that is NOT a key here.
|
* - `apps/cli/sync.ts` warns about a tag a problem page carries that is
|
||||||
|
* neither a key here nor an Appendix term.
|
||||||
*
|
*
|
||||||
* Add a key when a campaign topic starts teaching a tag; its guide page is then
|
* Add a key when a campaign topic starts teaching a tag; its Appendix term is
|
||||||
* dead weight, delete it by hand. A tag naming two topics (DFS/BFS: tree and
|
* then dead weight, delete it. A tag naming two topics (DFS/BFS: tree and
|
||||||
* graph; DP: 1-D and grid) lists both; the badge picks per page.
|
* graph; DP: 1-D and grid) lists both; the badge picks per page.
|
||||||
*/
|
*/
|
||||||
|
import { APPENDIX } from "./appendix.ts";
|
||||||
|
|
||||||
export const TOPIC_LANDINGS: Record<string, readonly string[]> = {
|
export const TOPIC_LANDINGS: Record<string, readonly string[]> = {
|
||||||
"hash-table": ["/hash-table"],
|
"hash-table": ["/hash-table"],
|
||||||
"prefix-sum": ["/prefix-sum"],
|
"prefix-sum": ["/prefix-sum"],
|
||||||
@@ -43,9 +46,9 @@ export interface TopicTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where a problem page's badge for `tag` points: a root-relative landing or
|
* Where a problem page's badge for `tag` points: a root-relative landing
|
||||||
* guide route (the caller adds the base), or LeetCode's problem list when the
|
* route or Appendix anchor (the caller adds the base), or LeetCode's problem
|
||||||
* site serves neither, so a badge never links to a 404.
|
* list when the site has neither, so a badge never links to a 404.
|
||||||
*
|
*
|
||||||
* A tag naming two landings links to the page's own topic — its route's first
|
* A tag naming two landings links to the page's own topic — its route's first
|
||||||
* segment — when that is one of them; otherwise DFS/BFS prefer the tree topic
|
* segment — when that is one of them; otherwise DFS/BFS prefer the tree topic
|
||||||
@@ -55,13 +58,12 @@ export function tagTarget(
|
|||||||
tag: TopicTag,
|
tag: TopicTag,
|
||||||
page: { route: string; tags: readonly TopicTag[] },
|
page: { route: string; tags: readonly TopicTag[] },
|
||||||
served: ReadonlySet<string>,
|
served: ReadonlySet<string>,
|
||||||
): { href: string; kind: "landing" | "guide" | "external" } {
|
): { href: string; kind: "landing" | "appendix" | "external" } {
|
||||||
const landings = (TOPIC_LANDINGS[tag.slug] ?? []).filter((path) => served.has(path));
|
const landings = (TOPIC_LANDINGS[tag.slug] ?? []).filter((path) => served.has(path));
|
||||||
const own = `/${page.route.split("/")[1]}`;
|
const own = `/${page.route.split("/")[1]}`;
|
||||||
const prefer = page.tags.some((t) => t.slug === "tree") ? "/tree-" : "/graph-";
|
const prefer = page.tags.some((t) => t.slug === "tree") ? "/tree-" : "/graph-";
|
||||||
const landing = landings.includes(own) ? own : (landings.find((p) => p.startsWith(prefer)) ?? landings[0]);
|
const landing = landings.includes(own) ? own : (landings.find((p) => p.startsWith(prefer)) ?? landings[0]);
|
||||||
if (landing) return { href: landing, kind: "landing" };
|
if (landing) return { href: landing, kind: "landing" };
|
||||||
const guide = `/appendix/${tag.slug}`;
|
if (APPENDIX[tag.slug]) return { href: `/appendix#${tag.slug}`, kind: "appendix" };
|
||||||
if (served.has(guide)) return { href: guide, kind: "guide" };
|
|
||||||
return { href: `https://leetcode.com/problem-list/${tag.slug}/`, kind: "external" };
|
return { href: `https://leetcode.com/problem-list/${tag.slug}/`, kind: "external" };
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,14 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"paths": { "@/*": ["./*"] }
|
"paths": {
|
||||||
|
"@/components": ["./components"],
|
||||||
|
"@/components/*": ["./components/*"],
|
||||||
|
"@/lib": ["./lib"],
|
||||||
|
"@/lib/*": ["./lib/*"],
|
||||||
|
"@/hooks": ["./hooks"],
|
||||||
|
"@/hooks/*": ["./hooks/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["islands", "components/ui", "lib", "topic-landings.ts", "redirects.ts"]
|
"include": ["islands", "components/ui", "lib", "topic-landings.ts", "redirects.ts"]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user