mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
95 lines
3.5 KiB
Plaintext
95 lines
3.5 KiB
Plaintext
---
|
|
/**
|
|
* The `PageFooter` slot: an Obsidian-style backlinks list under a topic landing
|
|
* or guide page — every problem page that points at it, as `<num> <name>`
|
|
* rows in LeetCode-number order. Pages nothing points at (problem pages, the
|
|
* overview, the changelog) render nothing.
|
|
*
|
|
* A problem points at a page when either
|
|
* - one of its topic badges links there — the same `tagTarget()` answer
|
|
* `ProblemBadges.astro` renders, so this list and the badges cannot drift
|
|
* (a DFS problem is a backlink of the one DFS landing its badge picks); or
|
|
* - it lives under the page's route (`/hybrid-structures/lru-cache` under
|
|
* `/hybrid-structures`), so a topic no LeetCode tag names still lists its
|
|
* own problems.
|
|
*
|
|
* Problem pages are recognised by their `<num>. <name>` title, the shape
|
|
* `apps/cli/sync.ts` writes.
|
|
*
|
|
* The list is a native `<details>`, open on load: the chevron at the top
|
|
* right of the header row collapses it like an accordion, with no JS.
|
|
*/
|
|
import { getEntry } from "astro:content";
|
|
import data from "blume:data";
|
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
import Icon from "blume/components/Icon.astro";
|
|
import { tagTarget, type TopicTag } from "../topic-landings.ts";
|
|
|
|
interface Props {
|
|
route: string;
|
|
}
|
|
|
|
interface Route {
|
|
path: string;
|
|
title: string;
|
|
collection: string;
|
|
entryId: string;
|
|
}
|
|
|
|
const { route } = Astro.props;
|
|
const routes: Route[] = data.routes;
|
|
const served = new Set(routes.map((r) => r.path));
|
|
|
|
const problems = routes.flatMap((r) => {
|
|
const m = r.title.match(/^(\d+)\.\s+(.+)$/);
|
|
return m ? [{ route: r, num: Number(m[1]), name: m[2]! }] : [];
|
|
});
|
|
|
|
// A problem page is never a backlink target: skip the per-problem lookups there.
|
|
const backlinks = problems.some((p) => p.route.path === route)
|
|
? []
|
|
: (
|
|
await Promise.all(
|
|
problems.map(async (p) => {
|
|
if (p.route.path.startsWith(`${route}/`)) return p;
|
|
const entry = await getEntry(p.route.collection, p.route.entryId);
|
|
const tags: TopicTag[] = entry?.data.topics ?? [];
|
|
const page = { route: p.route.path, tags };
|
|
return tags.some((tag) => tagTarget(tag, page, served).href === route) ? p : undefined;
|
|
}),
|
|
)
|
|
)
|
|
.filter((p) => p !== undefined)
|
|
.sort((a, b) => a.num - b.num);
|
|
---
|
|
|
|
{
|
|
backlinks.length > 0 && (
|
|
<details
|
|
class="group not-prose mx-auto mt-12 max-w-content border-border border-t pt-6"
|
|
open
|
|
>
|
|
<summary class="mb-2 flex cursor-pointer list-none items-center justify-between gap-2 text-muted-foreground transition-colors hover:text-foreground [&::-webkit-details-marker]:hidden">
|
|
<h2 class="flex items-baseline gap-2 font-medium text-xs uppercase tracking-wide" id="backlinks">
|
|
Backlinks
|
|
<span class="tabular-nums">{backlinks.length}</span>
|
|
</h2>
|
|
<Icon class="shrink-0 transition-transform group-open:rotate-180" name="chevron-down" size={16} />
|
|
</summary>
|
|
<ul class="text-sm">
|
|
{backlinks.map((p) => (
|
|
<li>
|
|
<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.path)}
|
|
>
|
|
<span class="w-10 shrink-0 text-right font-mono text-muted-foreground text-xs tabular-nums">{p.num}</span>
|
|
<span class="truncate">{p.name}</span>
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</details>
|
|
)
|
|
}
|