mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
feat: refactored docs structure
This commit is contained in:
1 parent
d388b3000e
commit
9de25fecff
212 files changed
+641
-4169
No files matched your search
@@ -1,293 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* `/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>
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* `/changelog`: every topic and review release on one page, newest first — a
|
||||
* vertical timeline, the date on the left and one card per release on the
|
||||
* right, with no per-release subpages. Replaces Blume's generated changelog
|
||||
* (a custom page wins its route); the header links here from
|
||||
* `navigation.actions` in blume.config.ts. Data: lib/releases.ts.
|
||||
*/
|
||||
import PageLayout from "blume/components/layout/PageLayout.astro";
|
||||
import data from "blume:data";
|
||||
import { withBase } from "blume/components/islands/base-path.ts";
|
||||
import redirects from "../redirects";
|
||||
import { readReleases } from "../lib/releases.ts";
|
||||
|
||||
const { config } = data;
|
||||
const releases = await readReleases();
|
||||
const DATE = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: "America/New_York",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
const issueUrl = (n: number) => `https://github.com/prdlk/leetcode/issues/${n}`;
|
||||
// LC number -> its docs page route, off sync's `/<num>` short links — the
|
||||
// page itself, not the redirect, so a click is one hop. A problem with no page
|
||||
// (none yet) links to its issue instead.
|
||||
const pages = new Map(redirects.map((r) => [Number(r.from.slice(1)), r.to]));
|
||||
const problemHref = (lc: number, issue: number) => {
|
||||
const route = pages.get(lc);
|
||||
return route ? withBase(route) : issueUrl(issue);
|
||||
};
|
||||
// The /progress cards' difficulty palette (--chart-1..3 in theme.css), so a
|
||||
// Hard reads the same colour here as in the SolvedRing gauge.
|
||||
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 = "Every topic and review the campaign finished, newest first.";
|
||||
---
|
||||
|
||||
<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: `Changelog - ${config.title}`, description, route: "/changelog" }}
|
||||
>
|
||||
<section class="mx-auto w-full max-w-4xl px-6 py-16">
|
||||
<h1 class="m-0 text-4xl font-semibold tracking-tight text-foreground">Changelog</h1>
|
||||
<p class="mt-3 text-lg text-muted-foreground">{description}</p>
|
||||
|
||||
<ol class="relative mt-12 list-none p-0">
|
||||
{
|
||||
releases.map((r, i) => (
|
||||
<li class="relative grid gap-4 pb-14 pl-8 sm:grid-cols-[11rem_1fr] sm:gap-8">
|
||||
{/* The rail: a line down the left edge, a ring at each release. */}
|
||||
{i < releases.length - 1 && (
|
||||
<span class="absolute top-3 bottom-0 left-[5px] w-px bg-border" aria-hidden="true" />
|
||||
)}
|
||||
<span
|
||||
class="absolute top-1.5 left-0 size-[11px] rounded-full border border-emerald-500 bg-background"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="font-mono text-sm">
|
||||
<time class="block uppercase tracking-wide text-foreground" datetime={r.finishedAt.toISOString()}>
|
||||
{DATE.format(r.finishedAt)}
|
||||
</time>
|
||||
<a class="mt-1 block text-xs text-muted-foreground no-underline hover:underline" href={r.url}>
|
||||
{r.kind} release ↗
|
||||
</a>
|
||||
</div>
|
||||
<article id={`issue-${r.issue}`} class="rounded-lg border border-border bg-muted/30 p-6">
|
||||
<header class="flex items-baseline justify-between gap-4">
|
||||
<h2 class="m-0 text-lg font-semibold text-foreground">{r.name}</h2>
|
||||
<a
|
||||
class="shrink-0 font-mono text-sm text-muted-foreground no-underline hover:underline"
|
||||
href={issueUrl(r.issue)}
|
||||
>
|
||||
#{r.issue}
|
||||
</a>
|
||||
</header>
|
||||
<ul class="mt-4 mb-0 list-none space-y-2 p-0">
|
||||
{r.problems.map((p) => (
|
||||
<li class="flex items-baseline gap-3 text-base leading-relaxed text-muted-foreground">
|
||||
<span class="select-none text-border" aria-hidden="true">—</span>
|
||||
<span class="min-w-0">
|
||||
<span class="mr-2 font-mono text-xs uppercase tracking-wide">{p.set}</span>
|
||||
<a class="text-foreground no-underline hover:underline" href={problemHref(p.lc, p.issue)}>
|
||||
{p.title}
|
||||
</a>
|
||||
{DIFFICULTY[p.difficulty] && (
|
||||
<span
|
||||
class:list={[
|
||||
"ml-2 inline-block rounded-full border px-2 py-px align-middle font-mono text-[11px] leading-4",
|
||||
DIFFICULTY[p.difficulty]!.class,
|
||||
]}
|
||||
>
|
||||
{DIFFICULTY[p.difficulty]!.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
</section>
|
||||
</PageLayout>
|
||||
Reference in new issue
Block a user