Files
leetcode/apps/docs/components/ProblemBadges.astro
T

176 lines
7.5 KiB
Plaintext

---
/**
* The MDX `wrapper`: the badge row (and, below it, the company table) between
* a page's description and its body.
* Blume's page template renders the title and description itself and then
* the MDX content, with no slot in between, so wrapping the content is the
* only way to land right after the description. MDX hands a wrapper no
* route, so it is read off the URL, and the page's frontmatter is then
* looked up through the route manifest — the same `getEntry` call Blume's own
* page template makes. Pages with none of the keys below (landing pages, the
* changelog) render only their content.
*
* - `topics:` (problem pages, written by `apps/cli/sync.ts`) is LeetCode's own
* topic tags. A tag the campaign teaches (`topic-landings.ts`) links to that
* topic's landing page and is filled; any other tag links to its card on the
* Appendix glossary, `/appendix#<slug>` (`appendix.ts`), and is outlined.
* `tagTarget()` picks the route (and between two landings, e.g. DFS); the
* `PageFooter` backlinks list and the Appendix page read the same answer.
* Each tag badge is a pill carrying the icon its target shows (a topic's
* `meta.ts` icon in the sidebar, a term's `icon` on its card) and the tag's
* own colour (`lib/tag-colors.ts`), assigned over every tag the site has a
* page or card for — no two tags share one, and tags that share problem
* pages are spread furthest apart. A tag with neither (the LeetCode-list
* fallback) stays gray and iconless.
* - `video:` (problem pages, written by `apps/cli/videos.ts`) is the NeetCode
* walkthrough's YouTube id, the red "Explanation" badge.
* - `issue:` (problem pages, written by sync) is the problem's GitHub issue
* URL, a neutral "Issue #N" badge with the GitHub mark, right after
* "Explanation" at the end of the row.
* - `companies:` (problem pages, written by sync; `companies.ts`) is the
* companies that asked the problem in the last six months, a table laid
* out like Blume's TypeTable under the badge row: each company a pill
* linking to its card in the Appendix's Companies section, the most recent
* window it asked in, and its frequency there (relative within that
* company and window, so a bar of 100 is its most-asked problem).
*/
import { getEntry } from "astro:content";
import data from "blume:data";
import Badge from "blume/components/content/Badge.astro";
import { stripBase, withBase } from "blume/components/islands/base-path.ts";
import { APPENDIX } from "../appendix.ts";
import { RECENCY_LABEL, companyHref, type Company } from "../companies.ts";
import { tagColors } from "../lib/tag-colors.ts";
import { TOPIC_LANDINGS, tagTarget, type TopicTag } from "../topic-landings.ts";
// `/sub/tree-dfs/invert-binary-tree/` (or `.../index.html` in a static build)
// -> `/tree-dfs/invert-binary-tree`, the base-less shape of `data.routes`.
const route =
stripBase(import.meta.env.BASE_URL ?? "/", Astro.url.pathname)
.replace(/(\/index)?\.html$/u, "")
.replace(/(.)\/+$/u, "$1");
const at = data.routes.find((r: { path: string }) => r.path === route);
const entry = at ? await getEntry(at.collection, at.entryId) : undefined;
const tags: TopicTag[] = entry?.data.topics ?? [];
const video: string | undefined = entry?.data.video;
const issue: string | undefined = entry?.data.issue;
const companies: Company[] = entry?.data.companies ?? [];
const served = new Set<string>(data.routes.map((r: { path: string }) => r.path));
// route → the icon its sidebar row shows: a page's own, or a topic group's
// (from its `meta.ts`) for the landing page the group row links to.
interface NavNode {
route?: string;
icon?: string;
children?: NavNode[];
}
const icons = new Map<string, string>();
const walk = (nodes: NavNode[]) => {
for (const node of nodes) {
if (node.route && node.icon && !icons.has(node.route)) icons.set(node.route, node.icon);
if (node.children) walk(node.children);
}
};
walk(data.navigation.sidebar);
// Every tag with a landing or an Appendix card, coloured against how problem
// pages (`<num>. <name>` titles, the shape sync writes) combine them. Only a
// page with tags needs the answer.
const colors = tags.length
? tagColors(
[...Object.keys(TOPIC_LANDINGS), ...Object.keys(APPENDIX)],
await Promise.all(
data.routes
.filter((r: { title: string }) => /^\d+\.\s/u.test(r.title))
.map(async (r: { collection: string; entryId: string }) => {
const page = await getEntry(r.collection, r.entryId);
return (page?.data.topics ?? []).map((t: TopicTag) => t.slug);
}),
),
)
: new Map<string, string>();
const badges = tags.map((tag) => {
const target = tagTarget(tag, { route, tags }, served);
const external = target.kind === "external";
return {
name: tag.name,
href: external ? target.href : withBase(target.href),
external,
icon: target.kind === "appendix" ? APPENDIX[tag.slug]?.icon : external ? undefined : icons.get(target.href),
color: external ? undefined : colors.get(tag.slug),
stroke: target.kind !== "landing",
};
});
const linkClass = "no-underline transition-opacity hover:opacity-80";
// TypeTable's column track, so the header and every row align.
const columns = "grid grid-cols-[2fr_1fr_auto] items-center gap-4";
---
{
(badges.length > 0 || video || issue) && (
<div class="not-prose mb-6 flex flex-wrap gap-1.5">
{badges.map((b) => (
<a
href={b.href}
target={b.external ? "_blank" : undefined}
rel={b.external ? "noopener" : undefined}
class={linkClass}
>
<Badge color={b.color} icon={b.icon} shape="pill" stroke={b.stroke}>
{b.name}
</Badge>
</a>
))}
{video && (
<a href={`https://www.youtube.com/watch?v=${video}`} target="_blank" rel="noopener" class={linkClass}>
<Badge variant="danger" icon="youtube" shape="pill">
Explanation
</Badge>
</a>
)}
{issue && (
<a href={issue} target="_blank" rel="noopener" class={linkClass}>
<Badge icon="github" shape="pill">
Issue #{issue.slice(issue.lastIndexOf("/") + 1)}
</Badge>
</a>
)}
</div>
)
}
{
companies.length > 0 && (
<div class="not-prose mb-6 rounded-blume border border-border bg-muted/30 p-1.5" data-blume-narration="skip">
<div class={`${columns} px-3 py-2 font-medium text-muted-foreground text-xs`}>
<span>Company</span>
<span>Asked within</span>
<span title="Relative within the company and window: 100 is its most-asked problem there">Frequency</span>
</div>
<div class="mt-1 flex flex-col">
{companies.map((c) => (
<div class={`${columns} border-border border-t px-3 py-2 text-xs`}>
<a href={withBase(companyHref(c.name))} class={linkClass}>
<Badge icon="building-2" shape="pill" stroke>
{c.name}
</Badge>
</a>
<span class="whitespace-nowrap text-foreground">{RECENCY_LABEL[c.period]}</span>
<span class="flex items-center gap-2">
<span class="h-1.5 w-16 overflow-hidden rounded-full bg-muted" aria-hidden="true">
<span class="block h-full rounded-full bg-accent" style={`width: ${c.frequency}%`} />
</span>
<code class="w-8 text-right font-mono text-foreground tabular-nums">{Math.round(c.frequency)}</code>
</span>
</div>
))}
</div>
</div>
)
}
<slot />