mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c8f1a0f1 | ||
|
|
57a5917e09 | ||
|
|
f8e90d7f55 | ||
|
|
a39336f7ed | ||
|
|
6396f1fbec | ||
|
|
82fe05b225 | ||
|
|
d7fbe88eb8 | ||
|
|
856ab0667a | ||
|
|
2c574018fb | ||
|
|
1f6413f3d8 | ||
|
|
3b9079f87f | ||
|
|
d5496581df | ||
|
|
e931391200 | ||
|
|
790e256127 | ||
|
|
a8bd4fbcd7 | ||
|
|
a7737502e1 | ||
|
|
2ac54e9930 | ||
|
|
ef91c0d1fe | ||
|
|
910e83fa64 | ||
|
|
3c9feb33fe | ||
|
|
b76b199b17 | ||
|
|
cd9aef1229 | ||
|
|
34b944d32d | ||
|
|
b710c777c3 | ||
|
|
19dedc52ce | ||
|
|
d9dc90e433 | ||
|
|
9c2aaf5969 | ||
|
|
a30e593202 | ||
|
|
d194cedf7a | ||
|
|
5d2d1dff3c | ||
|
|
dde06daea3 | ||
|
|
6e9c407698 | ||
|
|
63f0a81af3 | ||
|
|
5ed33057ed | ||
|
|
f4fc0fd0e7 | ||
|
|
f2a2d2d7de | ||
|
|
fa74adeda7 | ||
|
|
e11c9723f8 | ||
|
|
d2c645bef8 | ||
|
|
2f94b02e15 | ||
|
|
6f1cb93395 | ||
|
|
b310f410a2 | ||
|
|
436ff3080e | ||
|
|
d3f7b1c6f9 | ||
|
|
8a331f4b54 | ||
|
|
bc6eb3e66b | ||
|
|
dd929cca6c | ||
|
|
5987fdce7d | ||
|
|
6879afa61c | ||
|
|
40e965aa06 | ||
|
|
eea8d242e0 | ||
|
|
50b42108eb | ||
|
|
6f375e6c14 | ||
|
|
667bc06965 | ||
|
|
e63d517c9a | ||
|
|
4c297b4524 | ||
|
|
32dadf5928 | ||
|
|
5b6a33088c | ||
|
|
e9f76a32be | ||
|
|
951f958bd9 | ||
|
|
d2888360e2 | ||
|
|
b035d1ffa0 | ||
|
|
0473a3651f | ||
|
|
a9550769f0 | ||
|
|
51c8c771c6 | ||
|
|
7cedd235b5 | ||
|
|
107de04587 | ||
|
|
8787246db7 | ||
|
|
85b54b565e | ||
|
|
8e8522a773 |
No files matched your search
@@ -5,14 +5,13 @@ description: Write the `## Approach` section — a Mermaid flowchart that traces
|
||||
|
||||
# Problem approach diagrams
|
||||
|
||||
Every solution page (`apps/docs/content/(<category>)/<num>-<slug>.mdx`) has three
|
||||
hand-owned sections around the script-owned one:
|
||||
Every solution page (`apps/docs/content/(<phase>)/<topic>/<num>-<slug>.mdx`) has a
|
||||
hand-owned section before the script-owned one:
|
||||
|
||||
```
|
||||
frontmatter → <Badge> → ::::warning (maybe) → ### Example N / ### Constraints
|
||||
frontmatter → ::::warning (maybe) → ### Example N / ### Constraints
|
||||
## Approach ← this skill (Mermaid flowchart of the code below)
|
||||
## Solution ← bun run sync owns this; never edit inside it
|
||||
## Explanation ← bun run videos --write (NeetCode embed); not this skill
|
||||
```
|
||||
|
||||
`sync.ts` regenerates only `## Solution`, so an `## Approach` placed **before** it
|
||||
@@ -144,7 +143,7 @@ there (then `bun run sync`).
|
||||
|
||||
- Do not write prose approaches, complexity tables or alternative algorithms — the
|
||||
section is one flowchart (plus the warning callout when the code is broken).
|
||||
- Do not touch `## Solution` or anything after it; `## Explanation` is filled by
|
||||
`bun run videos --write`.
|
||||
- Do not touch `## Solution` or anything after it, nor the frontmatter `topics:` /
|
||||
`video:` keys (`bun run sync` / `bun run videos --write` own them).
|
||||
- Do not add an approach to `reference/` pages or `index.mdx` — only numbered
|
||||
solution pages (`title: 'NN. …'`) carry one.
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Pre-commit: the docs pages under apps/docs/content (and the `/<num>` short
|
||||
# links in apps/docs/redirects.ts) are generated from work/1 by `bun run sync`,
|
||||
# and deploy.yml publishes whatever main holds. Regenerate them now and stage
|
||||
# what sync changed, so a solution, its page and its redirect land in the same
|
||||
# commit and a stale `## Solution` never reaches main.
|
||||
#
|
||||
# Sync reads the working tree, not the index, so the pages it writes only
|
||||
# match this commit when work/1 has nothing unstaged: an untracked or
|
||||
# half-staged solution would ship a page whose source is not in the commit.
|
||||
# In that case refuse and say what to stage.
|
||||
#
|
||||
# Only paths that became dirty during sync are staged. Anything already dirty
|
||||
# beforehand (hand-written `## Approach` prose in progress, a new index.mdx)
|
||||
# stays where the author left it.
|
||||
#
|
||||
# Bypass for a deliberate partial commit: git commit --no-verify
|
||||
set -eu
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
unstaged=$(git status --porcelain -- work/1 | grep -v '^[MADR] ' || true)
|
||||
if [ -n "$unstaged" ]; then
|
||||
printf 'pre-commit: work/1 has unstaged changes; stage or stash them so the generated pages match this commit:\n%s\n' "$unstaged" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
before=$(mktemp)
|
||||
trap 'rm -f "$before"' EXIT
|
||||
git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort > "$before"
|
||||
report=$(bun run --silent sync)
|
||||
generated=$(git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort | comm -13 "$before" - | cut -c4-)
|
||||
if [ -n "$generated" ]; then
|
||||
printf '%s\n' "$report" | grep -v 'skipped (in sync)' >&2
|
||||
printf '%s\n' "$generated" | xargs git add --
|
||||
fi
|
||||
@@ -54,8 +54,9 @@ jobs:
|
||||
|
||||
# Deploys on every main push (not path-filtered): apps/api/data/schedule.json
|
||||
# is bundled at deploy time, and re-deploying an unchanged Worker is a no-op.
|
||||
# Needs repo secrets CLOUDFLARE_API_TOKEN (Workers Scripts: Edit) and
|
||||
# CLOUDFLARE_ACCOUNT_ID.
|
||||
# Needs repo secrets CLOUDFLARE_API_TOKEN (Workers Scripts: Edit),
|
||||
# CLOUDFLARE_ACCOUNT_ID, and OPENROUTER_API_KEY (the docs assistant's
|
||||
# POST /ask; the Actions secret is the one copy, mirrored into the Worker).
|
||||
deploy-api:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -68,6 +69,13 @@ jobs:
|
||||
# DST cron guards must hold before the Worker ships.
|
||||
- run: bun run api:test
|
||||
|
||||
- run: printf %s "$OPENROUTER_API_KEY" | bunx wrangler secret put OPENROUTER_API_KEY
|
||||
working-directory: apps/api
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
|
||||
- run: bun run api:deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
.openlogs
|
||||
tmp/
|
||||
*.log
|
||||
.crush
|
||||
|
||||
@@ -38,7 +38,7 @@ flowchart LR
|
||||
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `apps/api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
|
||||
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `apps/api/src/srs.test.ts`.
|
||||
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
|
||||
- **One "finished topic" rule, two consumers.** `apps/cli/topics.ts` (`coreSet()`: every `set:core` sub-issue closed, zero core = never) is read by `close-topics.ts` (closes the topic issue) and `release-topics.ts` (the `release` job of `close-solved.yml`). The release reads the sub-issues, not the topic issue, so the push that finishes a topic releases it in the same run instead of waiting for Close Topics to chain after it. Version = [timever](https://gist.github.com/twolfson/de1b004dd22536b8e668) (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the instant the last core problem closed; the tag is annotated and backdated to that instant, on the last first-parent main commit before it; the body is a changelog snapshot at that instant (a one-sentence summary first — Blume's changelog makes it the page's meta description — then closed on the finish day / earlier / open at release, with solution links at the tag). A release is found by its `<!-- release-topics: #N -->` body marker, never by tag name, so re-closing a core problem never mints a second one; an existing release keeps its tag and only has its body rewritten when the render changes. The docs `/changelog` is these releases (`githubReleases` in `apps/docs/blume.config.ts`, fetched at build with `GITHUB_TOKEN`), dated by `published_at` — which the API cannot backdate, so the Aug 24 – Sep 24 backfill reads Sep 25 there, and its true finish time lives in the version and the summary.
|
||||
- **One "finished topic" rule, two consumers.** `apps/cli/topics.ts` (`coreSet()`: every `set:core` sub-issue closed, zero core = never) is read by `close-topics.ts` (closes the topic issue) and `release-topics.ts` (the `release` job of `close-solved.yml`). The release reads the sub-issues, not the topic issue, so the push that finishes a topic releases it in the same run instead of waiting for Close Topics to chain after it. Version = [timever](https://gist.github.com/twolfson/de1b004dd22536b8e668) (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the instant the last core problem closed; the tag is annotated and backdated to that instant, on the last first-parent main commit before it; the body is a changelog snapshot at that instant (a one-sentence summary first — Blume's changelog makes it the page's meta description — then closed on the finish day / earlier / open at release, with solution links at the tag). A release is found by its `<!-- release-topics: #N -->` body marker, never by tag name, so re-closing a core problem never mints a second one; an existing release keeps its tag and has its name (the topic title) and body rewritten whenever the render changes. The docs `/changelog` is these releases (`githubReleases` in `apps/docs/blume.config.ts`, fetched at build with `GITHUB_TOKEN`), dated by the timever tag, not `published_at` (which the API cannot backdate): `releasesDatedByTag` rewrites each timever release's `published_at` before Blume maps it, via `custom(githubReleasesSource(…))` because the config-level `githubReleases()` takes no `fetchImpl`. Changing the tag format breaks the changelog dates.
|
||||
|
||||
## Key Directories
|
||||
|
||||
@@ -46,9 +46,9 @@ flowchart LR
|
||||
|---|---|
|
||||
| `work/<1\|3\|7>/<Difficulty>/<Category>/<num>.<slug>.{js,py}` | Solutions, e.g. `work/1/Easy/Array/1.two-sum.py`. The top directory is the spaced-repetition bucket: `1` = first solve, `3`/`7` = the blind re-solves of that problem 3 and 7 days later. Machine-parsed header comment (title / `Difficulty:` / URL / `─` rule / statement) — preserve its exact shape |
|
||||
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
|
||||
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `components/` + `components.ts`, `islands/`, `public/`). `content/(0N-<phase>)/<topic>/<num>-<slug>.mdx` plus one `meta.ts` per phase and per topic folder, all generated by sync: the phase folder is a `(group)` (sidebar section, no route segment), the topic folder is the README campaign topic slug (`tree-dfs/`, `heap-priority-queue/`) and a real route segment (`/tree-dfs/invert-binary-tree`), the `meta.ts` files carry the exact labels, phase/topic numbers and icons so the sidebar reads in curriculum order with each topic a collapsible group, and the page badge is the `work/` data-structure category. A hand-written `(0N-<phase>)/<topic>/index.mdx` (`hidden: true`) is that topic's landing page: Blume ≥ 1.7.3 links the sidebar group row to it (`sliding-window/index.mdx` → `/sliding-window`) and the hidden flag drops the duplicate row. Two kinds of custom MDX component, both import-free on every page: `islands/<Name>.tsx` is hydrated React (auto-discovered; `ProgressDashboard`, `DependencySpine` — the README topic map as data, zoom/pan, nodes link to whatever route Blume's `/api/docs/navigation.json` reports for the topic folder, inert until a topic has pages), `components/<Name>.astro` registered in `components.ts` is static markup plus a custom element for behaviour, the way Blume's own `Frame` is built (`Carousel`/`CarouselImage`: `src` takes a URL, `/file.png`, or the page-relative `../../public/file.png`) |
|
||||
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `components/` + `components.ts`, `islands/`, `public/`). `content/(0N-<phase>)/<topic>/<num>-<slug>.mdx` plus one `meta.ts` per phase and per topic folder, all generated by sync (a phase's once, then hand-owned): the phase folder is a `(group)` (sidebar section, no route segment), the topic folder is the README campaign topic slug (`tree-dfs/`, `heap-priority-queue/`) and a real route segment (`/tree-dfs/invert-binary-tree`), the `meta.ts` files carry the exact labels, phase/topic numbers and icons so the sidebar reads in curriculum order with each topic a collapsible group. A problem page's badge row (between the description and the body) is rendered, not written: the MDX `wrapper` (`components/ProblemBadges.astro`, registered as `mdx.wrapper` in `components.ts` — Blume's page template renders the title and description itself with no slot before the body, so the wrapper, which MDX hands no route, reads it off `Astro.url`) reads the page's `topics:` (LeetCode's topic tags, from sync) and `video:` (NeetCode YouTube id, from `bun run videos`) frontmatter — both declared via `frontmatter.extend` in `blume.config.ts`, hand-rolled Standard Schemas, no zod — and links a tag the campaign teaches to its topic landing page (filled), any other tag to its guide page `/guides/<slug>` (outlined; LeetCode's problem list if the guide is gone, gray and iconless), and the video as a red `Explanation` badge. Every badge in the row is a pill. A tag badge carries the icon its target shows in the sidebar (read off `data.navigation`: the topic `meta.ts` icon, the guide's `icon:`) and the tag's own colour from `lib/tag-colors.ts` — unique per tag site-wide, assigned greedily over every tag with a page so tags that share problem pages sit furthest apart in OKLab, in a lightness band that keeps ≥ 3:1 contrast in both themes (Blume's Badge takes one hex for both). The reverse direction is the `PageFooter` slot (`components/BacklinksList.astro`): an Obsidian-style `<num> <name>` list under every topic landing and guide page of the problems whose badges link there, plus a topic's own folder members (so a topic no tag names, `hybrid-structures`, still lists its problems), in a native `<details>` open on load whose top-right chevron collapses it (no JS); problem pages are recognised by their `<num>. <name>` title and render no list. Which tags the campaign teaches is ONE map, `apps/docs/topic-landings.ts` (tag slug → landing routes), and where a badge points is ONE function beside it, `tagTarget()` — read by both components (so badges and backlinks cannot disagree) and the map by sync. `content/guides/` (the `Guides` header tab) is one page per tag outside that map, filed by hand into `(group)` categories — `(1-data-structures)/`, `(2-algorithms)/`, `(3-techniques)/` (hand-written `meta.ts` each; no route segment, so every guide stays `/guides/<slug>`). Sync finds a guide by slug anywhere under `guides/` and creates a placeholder only when none exists or it is empty — at the `guides/` root, for you to file into a category and give a Lucide `icon:` — the one every badge linking to it shows — that no topic and no other guide uses (sync warns while it is missing, a `TOPIC_ICONS` value, or another guide's) — and after that owns exactly one key of it: `sidebar.badge`, the sidebar count, rewritten every run to the guide's backlink count (problem pages carrying the tag — equal to the `BacklinksList` rows, since a guide's tag has no landing; dropped at zero). Everything else is hand-owned (a guide whose tag no page carries any more is left alone, badge dropped), with frontmatter `list:` — the tag's LeetCode problem list, rendered by the same wrapper as an orange `LeetCode list` badge; sync also writes `guides/meta.ts`. A hand-written `(0N-<phase>)/<topic>/index.mdx` (`hidden: true`) is that topic's landing page: Blume ≥ 1.7.3 links the sidebar group row to it (`sliding-window/index.mdx` → `/sliding-window`) and the hidden flag drops the duplicate row. Two kinds of custom MDX component, both import-free on every page: `islands/<Name>.tsx` is hydrated React (auto-discovered; the five `/progress` cards `SolvedRing`/`SubmissionHeatmap`/`SubmissionChart`/`TopicBubbles`/`RecentSolves`, and `DependencySpine` — the README topic map as data, zoom/pan, nodes link to whatever route Blume's `/api/docs/navigation.json` reports for the topic folder, inert until a topic has pages), `components/<Name>.astro` registered in `components.ts` is static markup plus a custom element for behaviour, the way Blume's own `Frame` is built (`Carousel`/`CarouselImage`: `src` takes a URL, `/file.png`, or the page-relative `../../public/file.png`) |
|
||||
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (the plan: `date → { topics | problems | gate | sweep }`, human-edited, bundled at deploy), `migrations/` (append-only; `0002` is the +3/+7 ladder rebuild) |
|
||||
| `.github/workflows/` | deploy (docs → Pages with `GITHUB_TOKEN` for the releases-backed changelog, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`; then the `release` job: timever tag + release per finished topic, `contents: write`), close-topics, sync-d1 (issue edits → `/admin/reconcile`), spaced-repetition (daily 06:00 ET cron → today's review issue; `GITHUB_TOKEN` only, no secrets) |
|
||||
| `.github/workflows/` | deploy (docs → Pages with `GITHUB_TOKEN` for the releases-backed changelog, Worker → Cloudflare with the `OPENROUTER_API_KEY` Actions secret put into it first, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`; then the `release` job: timever tag + release per finished topic, `contents: write`), close-topics, sync-d1 (issue edits → `/admin/reconcile`), spaced-repetition (daily 06:00 ET cron → today's review issue; `GITHUB_TOKEN` only, no secrets) |
|
||||
| `.githooks/` | Tracked git hooks; `bun install` points `core.hooksPath` here (root `prepare` script, no husky). `pre-commit` runs `bun run sync` and stages only what sync changed (pages, `apps/docs/redirects.ts`), so a solution, its page and its `/<num>` short link land in one commit; it refuses to run while `work/1` has anything unstaged (sync reads the tree, so the page would not match the commit), and leaves pre-existing dirt under `apps/docs/content` (in-progress `## Approach` prose) unstaged. `git commit --no-verify` for a deliberate partial commit |
|
||||
|
||||
## Development Commands
|
||||
@@ -71,9 +71,9 @@ bun run submit # submit ONE solution to LeetCode (newest scaffold first
|
||||
# hidden; last_submitted stamped in the db on exit 0)
|
||||
bun run sync # work/1 → apps/docs/content/ pages + apps/docs/redirects.ts (/<num> short
|
||||
# links; re-solves are never published)
|
||||
bun run videos # docs pages with no <YouTube> embed → NeetCode video per page
|
||||
# (channel search scraped, no API key; --write appends the
|
||||
# `## Explanation` embed; "no NeetCode match" pages stay untouched)
|
||||
bun run videos # docs pages with no `video:` frontmatter → NeetCode video per page
|
||||
# (channel search scraped, no API key; --write adds the
|
||||
# `video:` key; "no NeetCode match" pages stay untouched)
|
||||
bun run dev|build # Blume docs site (runs in apps/docs/)
|
||||
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
|
||||
bun run close-topics -- --dry-run
|
||||
@@ -107,20 +107,21 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
||||
## Important Files
|
||||
|
||||
- `apps/api/src/srs.ts` — SRS domain: the ladder (`new → +3 → +7 → retired`; fail resets to `+3`; first-ever log enters at `+3`), `WINDOWS`/`STAGES`/`TEMPERATURES`/`DIFFICULTIES`/`SETS` (the one vocabulary shared by the charts, the `work/<n>` buckets, the review issues and the stats dashboard), ET date math, `workingDay()` (no scheduled date is ever a Sunday), `temperatureOf()` (days since a concept was last exercised → `hot`/`fresh`/`fading`/`cold`, bands derived from `WINDOWS` so they cannot drift; read-side only, nothing is scheduled off it), seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at most `REVIEW_CAP` (3) reviews surface per day — `levelReviews()` (run by `sendDigest` on real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 3 per day, oldest first, instead of letting the due pile grow.
|
||||
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
|
||||
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`, and `OPENROUTER_API_KEY`, whose one copy is the repo's Actions secret (deploy.yml puts it before every deploy — rotate it there).
|
||||
- `apps/api/src/ask.ts` — `POST /ask`, the docs assistant's backend. Blume's own assistant route is server-rendered and cannot ship in the static Pages build (`BLUME_SERVER_FEATURE_REQUIRED`), so `blume.config.ts` sets `ai.assistant.endpoint` to this route instead of a `provider`. It speaks the Blume island's wire contract (`{ messages, page: { path } }` in, a plain-text stream out, a 503 "The assistant is not configured…" when the key is missing), validates like Blume's generated route (1–40 user/assistant messages, 24 000 chars, 64 KB — no caller system prompt), grounds on the published `llms-full.txt` (current page first, then keyword-ranked excerpts in Blume's 6 × 2 000 / 10 000-char budget, cached 5 min), and streams OpenRouter's chat completions over plain `fetch` — no AI SDK — for the `ASK_MODEL` var. `:free` models share an upstream pool and often answer 429, which the island shows as its rate-limit notice.
|
||||
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
|
||||
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
|
||||
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the `<Badge>` line and the `## Solution` section are script-owned, the latter bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside `## Solution`. Two classifications, two homes: the sidebar group is the README topic (`readmeTopics()`, first listing wins for a problem under two topics; a warm-up outside the plan groups under its category at `order: 0`, ahead of topic 1) and the badge is the `work/1/<Difficulty>/<Category>/` category. Topics nest under their README phase (`## Phase N — Name` headings, shortened via `PHASE_ALIASES`): `content/(0N-<phase>)/<topic>/`, both `meta.ts` script-owned and rewritten every run (phase: `Phase N: Name`, `order: N`; topic: label, curriculum order, `TOPIC_ICONS` icon, `display: "group"`). The phase folder is a Blume `(group)` — no route segment, so `/tree-dfs` is unchanged and a landing page there is one level deep, never at `/` — and the topic folder is the real segment; never put a topic `index.mdx` directly inside a `(group)`. A page is `rename`d when its topic or phase changes; a folder every page has left is deleted (climbing into an emptied phase folder) unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one — so page-relative asset paths in a moved landing page are yours to fix). Slugs come from `slugify()`, which glues a leading number to the next token (`1d-dynamic-programming`) because Blume strips `<digits>-` prefixes from routes. The two hand-owned sections around the solution have their own owners: `## Approach` (a Mermaid flowchart tracing the page's code, placed *before* `## Solution` so a re-sync keeps it) is written per the `problem-approach` skill (`/problem-approach` fills every page missing one); `## Explanation` (the NeetCode `<YouTube>` embed, after `## Solution`) is appended by `bun run videos --write` (`apps/cli/videos.ts`).
|
||||
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the frontmatter `icon:` and the `## Solution` section are regenerated, and `topics:` (LeetCode's `topicTags` as `{ name, slug }`, fetched by GraphQL) is filled once — on create, or on any run that finds it missing — never refreshed (delete the key to refetch; offline, the page is written without it and the next sync retries). `## Solution` is the latter bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside `## Solution`. Two classifications, two homes: the sidebar group is the README topic (`readmeTopics()`, first listing wins for a problem under two topics; a warm-up outside the plan groups under its category at `order: 0`, ahead of topic 1) and the `work/1/<Difficulty>/<Category>/` category, which only places an unplanned problem — the page's topic badges are LeetCode's tags, not either classification. Topics nest under their README phase (`## Phase N — Name` headings, shortened via `PHASE_ALIASES`): `content/(0N-<phase>)/<topic>/`. The topic `meta.ts` is script-owned and rewritten every run (label, curriculum order, `TOPIC_ICONS` icon, `display: "group"`); the phase `meta.ts` is written once when missing (`Phase N: Name`, `order: N`) and hand-owned after — rename its title freely. Never rewriting it loses nothing: its order and name are also the folder name, so a README change to either lands in a new folder with a fresh meta. The phase folder is a Blume `(group)` — no route segment, so `/tree-dfs` is unchanged and a landing page there is one level deep, never at `/` — and the topic folder is the real segment; never put a topic `index.mdx` directly inside a `(group)`. A page is `rename`d when its topic or phase changes; a folder every page has left is deleted (climbing into an emptied phase folder) unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one — so page-relative asset paths in a moved landing page are yours to fix). Slugs come from `slugify()`, which glues a leading number to the next token (`1d-dynamic-programming`) because Blume strips `<digits>-` prefixes from routes. The hand-owned parts have their own owners: `## Approach` (a Mermaid flowchart tracing the page's code, placed *before* `## Solution` so a re-sync keeps it) is written per the `problem-approach` skill (`/problem-approach` fills every page missing one); the frontmatter `video:` (the NeetCode walkthrough, rendered as the red `Explanation` badge) is added by `bun run videos --write` (`apps/cli/videos.ts`).
|
||||
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
|
||||
- `apps/api/src/stats.ts` / `apps/cli/stats.ts` / `apps/cli/tui.ts` / `apps/cli/api.ts` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, per-topic temperature, gates, the 14-day due queue, and `heat` (every campaign day, zeros and future days included — the docs page windows the tail off it rather than asking a second question). Two readers consume it, the docs island and `bun run stats`, so its shape is both their contract: change all three together. `tui.ts` is hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule.
|
||||
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
|
||||
- `apps/api/src/stats.ts` / `apps/cli/stats.ts` / `apps/cli/tui.ts` / `apps/cli/api.ts` / `apps/docs/lib/stats.ts` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, `attempting` (open misses, via `MISS_WHERE` in `srs.ts` — the sweep day's definition), per-topic temperature, gates, the 14-day due queue, `heat` (every campaign day, zeros and future days included — the TUI windows its tail rather than asking a second question), `recent` (the ten problems most recently solved, dated by each one's first pass) and `year` (the 365 days ending today — always the calendar year to date — each split by difficulty into attempts and first solves; the docs cards window and bucket it themselves). Two readers consume it, the docs `/progress` islands and `bun run stats`, so its shape is their contract: change the Worker and both readers together (additive fields a reader ignores are safe). `tui.ts` is hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule.
|
||||
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache). The docs `/progress` page is five LeetCode-style islands — `SolvedRing` (difficulty gauge + "Attempting"), `SubmissionHeatmap` and `SubmissionChart` sharing one row (the heatmap: the last six months in month blocks, the README's colour ramp as theme.css `--heat-0..4`, its attempts / active days / max streak counted from the cells it draws; the chart: this calendar year as stacked Easy/Med./Hard bars per month or week, first solves or every attempt, via two shadcn `ToggleGroup`s), `TopicBubbles` (the campaign topics circle-packed by `d3-hierarchy`, filled by solved share), `RecentSolves` (the ten most recent, linked through `redirects.ts`) — composed from shadcn/ui (`components/ui/{card,chart,skeleton,toggle,toggle-group}.tsx`, Recharts under `chart`, Radix under the toggles) on Blume's tokens: `apps/docs/theme.css` maps shadcn's names onto `--blume-*` (never `accent`, which is Blume's brand colour — vendored components that use it, like `toggle.tsx`, are retokened to `background`/`muted`) and gives shadcn's `data-slot` parts their border colour. They share `lib/stats.ts` (`useStats()`), so the page makes ONE `/api/stats` request however many islands hydrate. shadcn sources live outside `islands/` (every `.tsx` there becomes an island) and import `lib/utils` relatively — Blume's Vite has no `@/` alias; `components.json` + `tsconfig.json` exist for the shadcn CLI and `tsc`, so after `shadcn add` rewrite `@/…` imports to relative ones — and check `package.json`: the CLI has twice installed an unrelated `cn` package for `lib/utils` and skipped real deps (`class-variance-authority`). Blume's `.prose :where(a, p, ol)` rules ignore `not-prose`, so island markup overrides them with `!` utilities, as Blume's own components do.
|
||||
- `apps/docs/blume.config.ts` — site base `/leetcode`; imports `apps/docs/redirects.ts`, sync-generated (`/<num>` → `/<topic>/<slug>`, one row per page, exact root-relative paths — Blume adds the base and, for the static Pages build, emits a meta-refresh page per rule plus `blume-redirects.json`); `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`, `PHASE_ALIASES`).
|
||||
- Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record.
|
||||
|
||||
## Runtime/Tooling Preferences
|
||||
|
||||
- **Bun only**: run scripts with `bun apps/cli/<name>.ts` (or the root `bun run` aliases), install with `bun install --frozen-lockfile`. One workspace (`apps/*`), one root `bun.lock`. Node 22 appears only in `deploy.yml` because Blume requires it.
|
||||
- **Near-zero runtime npm dependencies** — SVG, PNG, terminal ANSI, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. The **one** exception is the digest email: `apps/api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `apps/api/tsconfig.json`. Do not extend this exception to any other module.
|
||||
- **Near-zero runtime npm dependencies** — SVG, PNG, terminal ANSI, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. Two exceptions, each confined to one surface. The digest email: `apps/api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `apps/api/tsconfig.json`. And the docs `/progress` islands: shadcn/ui (`recharts`, `radix-ui`, `class-variance-authority`, `clsx`, `tailwind-merge`) plus `d3-hierarchy` for the bubble packing, client-side in `apps/docs` only. Do not extend either exception to any other module.
|
||||
- Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions.
|
||||
|
||||
## Testing & QA
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
GH_PAT=github_pat_or_gho_token_with_issues_and_projects_rw
|
||||
WEBHOOK_SECRET=random_hex_matching_the_repo_webhook
|
||||
LINK_KEY=random_hex_signing_one_tap_links
|
||||
OPENROUTER_API_KEY=sk-or-v1_key_for_the_docs_assistant
|
||||
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* POST /ask — the docs assistant's backend.
|
||||
*
|
||||
* The docs are a static GitHub Pages site, and Blume's own assistant route is
|
||||
* server-rendered (it reads the provider key per request), so the site points
|
||||
* `ai.assistant.endpoint` here instead. This speaks the Blume island's wire
|
||||
* contract (`blume/components/islands/hooks.ts`):
|
||||
*
|
||||
* - request: `{ messages: [{ role, content }], page: { path } }` as JSON,
|
||||
* `path` the base-stripped route the reader is on;
|
||||
* - response: the answer as a plain UTF-8 text stream; any non-2xx shows the
|
||||
* island's error notice, and a 503 whose body starts "The assistant is not
|
||||
* configured" shows that body verbatim.
|
||||
*
|
||||
* Validation matches Blume's generated route: 1–40 user/assistant messages of
|
||||
* string content, ≤ 24 000 chars serialized, a 64 KB body, and only role and
|
||||
* content ever reach the model, so the endpoint cannot be repurposed as an
|
||||
* open LLM proxy with a caller's own system prompt.
|
||||
*
|
||||
* Grounding reads what the site already publishes: `llms-full.txt`, every
|
||||
* page's Markdown under `# Title` / `Source: <url>`. The page the reader is on
|
||||
* goes in first, then the best keyword matches, within Blume's default budget
|
||||
* (6 pages, 2 000 chars each, 10 000 total), under Blume's grounded system
|
||||
* prompt — answer only from the excerpts, cite pages as `[Title](/route)`.
|
||||
*
|
||||
* The model call is OpenRouter's OpenAI-compatible chat endpoint over plain
|
||||
* `fetch` (no SDK — see the repo's near-zero-dependency rule), streamed, with
|
||||
* `reasoning.effort: "none"`, for the model the `ASK_MODEL` var names (a
|
||||
* wrangler.jsonc change swaps it); its SSE deltas are re-emitted as bare text. The
|
||||
* reader closing the panel aborts the request, which aborts the model call.
|
||||
*/
|
||||
|
||||
const BODY_LIMIT = 64 * 1024;
|
||||
const MAX_MESSAGES = 40;
|
||||
const MAX_CHARS = 24_000;
|
||||
|
||||
const MAX_RESULTS = 6;
|
||||
const EXCERPT_CHARS = 2000;
|
||||
const CONTEXT_BUDGET = 10_000;
|
||||
/** Lead-in kept before the matched region of a long page. */
|
||||
const EXCERPT_LEAD = 160;
|
||||
/** How long one isolate reuses the parsed corpus (the edge caches the fetch as long). */
|
||||
const CORPUS_TTL_MS = 5 * 60_000;
|
||||
|
||||
const SYSTEM_PROMPT =
|
||||
"You are a helpful documentation assistant for this project, a LeetCode interview-prep campaign: solved problems grouped by topic, with guides to the techniques behind them. Answer the user's question using ONLY the documentation excerpts below. Each excerpt is headed by its page as `## Page Title (/route)`. If the answer is not covered by the excerpts, say you don't know and suggest where in the docs to look — do not invent details. Always cite the pages you drew from, and write every citation as a Markdown link to that page using its route, e.g. [Page Title](/route).";
|
||||
|
||||
/** Words that match everything and rank nothing. */
|
||||
const STOPWORDS = new Set(
|
||||
"a an and are as at be but by can do does for from how i if in is it its me my of on or so that the this to was what when where which who why with you your".split(
|
||||
" ",
|
||||
),
|
||||
);
|
||||
|
||||
interface AskMessage {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
title: string;
|
||||
route: string;
|
||||
body: string;
|
||||
/** Lower-cased title + body, searched per query term. */
|
||||
text: string;
|
||||
titleText: string;
|
||||
}
|
||||
|
||||
// ── corpus ───────────────────────────────────────────────────────
|
||||
|
||||
let corpus: { at: number; sections: Section[] } | undefined;
|
||||
|
||||
/** `llms-full.txt` split into pages, cached per isolate for `CORPUS_TTL_MS`. */
|
||||
async function sections(docsUrl: string): Promise<Section[]> {
|
||||
if (corpus && Date.now() - corpus.at < CORPUS_TTL_MS) return corpus.sections;
|
||||
const res = await fetch(`${docsUrl}/llms-full.txt`, {
|
||||
cf: { cacheTtl: CORPUS_TTL_MS / 1000, cacheEverything: true },
|
||||
});
|
||||
if (!res.ok) throw new Error(`GET llms-full.txt -> ${res.status}`);
|
||||
const full = await res.text();
|
||||
const heads = [...full.matchAll(/^# (.+)\nSource: (\S+)$/gm)];
|
||||
const out = heads.map((head, i): Section => {
|
||||
const start = head.index + head[0].length;
|
||||
const end = heads[i + 1]?.index ?? full.length;
|
||||
const body = full.slice(start, end).replace(/\n-{3,}\s*$/, "").trim();
|
||||
const title = head[1]!.trim();
|
||||
return {
|
||||
title,
|
||||
route: new URL(head[2]!).pathname.replace(/(.)\/+$/, "$1"),
|
||||
body,
|
||||
text: `${title}\n${body}`.toLowerCase(),
|
||||
titleText: title.toLowerCase(),
|
||||
};
|
||||
});
|
||||
corpus = { at: Date.now(), sections: out };
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── retrieval ────────────────────────────────────────────────────
|
||||
|
||||
function terms(text: string): string[] {
|
||||
return [...new Set(text.toLowerCase().match(/[a-z0-9][a-z0-9+#-]*/g) ?? [])].filter(
|
||||
(t) => !STOPWORDS.has(t) && (t.length > 1 || /\d/.test(t)),
|
||||
);
|
||||
}
|
||||
|
||||
function occurrences(haystack: string, needle: string): number {
|
||||
let count = 0;
|
||||
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pages to inject, best first: term frequency damped by log and weighted
|
||||
* by rarity across pages (idf), a title hit counting triple — so "217" or
|
||||
* "sliding window" lands on the page named that rather than every page that
|
||||
* mentions it in passing.
|
||||
*/
|
||||
function rank(all: Section[], query: string[]): Section[] {
|
||||
const idf = new Map(
|
||||
query.map((t) => [t, Math.log(1 + all.length / (1 + all.filter((s) => s.text.includes(t)).length))]),
|
||||
);
|
||||
return all
|
||||
.map((section) => {
|
||||
let score = 0;
|
||||
for (const t of query) {
|
||||
const tf = occurrences(section.text, t);
|
||||
if (tf) score += idf.get(t)! * (Math.log(1 + tf) + (section.titleText.includes(t) ? 3 : 0));
|
||||
}
|
||||
return { section, score };
|
||||
})
|
||||
.filter((r) => r.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, MAX_RESULTS)
|
||||
.map((r) => r.section);
|
||||
}
|
||||
|
||||
/** A page whole when it fits, else `chars` from just before its first query hit. */
|
||||
function excerpt(section: Section, query: string[], chars: number): string {
|
||||
if (section.body.length <= chars) return section.body;
|
||||
const lower = section.body.toLowerCase();
|
||||
const hits = query.map((t) => lower.indexOf(t)).filter((i) => i !== -1);
|
||||
const start = hits.length ? Math.max(0, Math.min(...hits) - EXCERPT_LEAD) : 0;
|
||||
return `${start > 0 ? "…" : ""}${section.body.slice(start, start + chars)}…`;
|
||||
}
|
||||
|
||||
/** The system prompt: the current page, then the ranked pages, within the budget. */
|
||||
async function instructions(env: Env, messages: AskMessage[], path: string | undefined): Promise<string> {
|
||||
const all = await sections(env.DOCS_URL);
|
||||
// A follow-up ("and in Python?") leans on the question before it.
|
||||
const asked = messages.filter((m) => m.role === "user").slice(-2).map((m) => m.content);
|
||||
const query = terms(asked.join(" "));
|
||||
const current = path ? all.find((s) => s.route === path) : undefined;
|
||||
const picked = [...(current ? [current] : []), ...rank(all, query).filter((s) => s !== current)];
|
||||
const parts: string[] = [];
|
||||
let budget = CONTEXT_BUDGET;
|
||||
for (const section of picked) {
|
||||
if (budget < 200) break;
|
||||
const text = excerpt(section, query, Math.min(EXCERPT_CHARS, budget));
|
||||
parts.push(`## ${section.title} (${section.route})\n\n${text}`);
|
||||
budget -= text.length;
|
||||
}
|
||||
return parts.length ? `${SYSTEM_PROMPT}\n\n# Documentation excerpts\n\n${parts.join("\n\n")}` : SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
// ── request handling ─────────────────────────────────────────────
|
||||
|
||||
/** The validated conversation, or a reason it was turned away. */
|
||||
function readMessages(body: unknown): AskMessage[] | undefined {
|
||||
if (!body || typeof body !== "object" || !("messages" in body) || !Array.isArray(body.messages)) return;
|
||||
const raw: unknown[] = body.messages;
|
||||
if (raw.length === 0 || raw.length > MAX_MESSAGES || JSON.stringify(raw).length > MAX_CHARS) return;
|
||||
const messages: AskMessage[] = [];
|
||||
for (const m of raw) {
|
||||
if (!m || typeof m !== "object" || !("role" in m) || !("content" in m)) return;
|
||||
if ((m.role !== "user" && m.role !== "assistant") || typeof m.content !== "string") return;
|
||||
messages.push({ role: m.role, content: m.content });
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
function readPath(body: unknown): string | undefined {
|
||||
if (!body || typeof body !== "object" || !("page" in body)) return;
|
||||
const page = body.page;
|
||||
if (!page || typeof page !== "object" || !("path" in page) || typeof page.path !== "string") return;
|
||||
return page.path.replace(/(.)\/+$/, "$1");
|
||||
}
|
||||
|
||||
/** OpenRouter's SSE (`data: {…choices[0].delta.content…}`) → the answer's bare text. */
|
||||
function textStream(sse: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
||||
const decoder = new TextDecoder();
|
||||
const encoder = new TextEncoder();
|
||||
let buffer = "";
|
||||
return sse.pipeThrough(
|
||||
new TransformStream<Uint8Array, Uint8Array>({
|
||||
transform(chunk, controller) {
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data: ")) continue; // `: OPENROUTER PROCESSING` keep-alives
|
||||
const data = line.slice(6).trim();
|
||||
if (data === "[DONE]") continue;
|
||||
let event: unknown;
|
||||
try {
|
||||
event = JSON.parse(data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (event && typeof event === "object" && "error" in event) {
|
||||
// After the 200: the stream just ends, and the island shows its
|
||||
// error notice for an empty answer.
|
||||
console.error(`ask: provider error mid-stream: ${JSON.stringify(event.error)}`);
|
||||
continue;
|
||||
}
|
||||
if (!event || typeof event !== "object" || !("choices" in event) || !Array.isArray(event.choices)) continue;
|
||||
const choice: unknown = event.choices[0];
|
||||
if (!choice || typeof choice !== "object" || !("delta" in choice)) continue;
|
||||
const delta = choice.delta;
|
||||
if (!delta || typeof delta !== "object" || !("content" in delta)) continue;
|
||||
const content = delta.content;
|
||||
if (typeof content === "string" && content) controller.enqueue(encoder.encode(content));
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleAsk(request: Request, env: Env): Promise<Response> {
|
||||
// The docs serve from the custom domain and the Pages origin; reflect the
|
||||
// requesting origin only when it is on the allowlist (as /api/stats does).
|
||||
const allowed = env.DOCS_ORIGIN.split(",").map((o) => o.trim());
|
||||
const origin = request.headers.get("origin");
|
||||
const cors = {
|
||||
"access-control-allow-origin": origin && allowed.includes(origin) ? origin : allowed[0]!,
|
||||
"access-control-allow-methods": "POST",
|
||||
"access-control-allow-headers": "content-type",
|
||||
"access-control-max-age": "86400",
|
||||
vary: "Origin",
|
||||
};
|
||||
const text = (body: string, status: number) =>
|
||||
new Response(body, { status, headers: { ...cors, "content-type": "text/plain; charset=utf-8" } });
|
||||
|
||||
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
|
||||
if (request.method !== "POST") return text("POST only.", 405);
|
||||
|
||||
const raw = await request.arrayBuffer();
|
||||
if (raw.byteLength > BODY_LIMIT) return text("Request too large: the body must be at most 64 KB.", 413);
|
||||
let body: unknown;
|
||||
try {
|
||||
body = JSON.parse(new TextDecoder().decode(raw));
|
||||
} catch {
|
||||
body = undefined;
|
||||
}
|
||||
const messages = readMessages(body);
|
||||
if (!messages) return text("Invalid request: send 1-40 user/assistant messages with string content.", 400);
|
||||
if (!env.OPENROUTER_API_KEY) return text("The assistant is not configured: set OPENROUTER_API_KEY.", 503);
|
||||
|
||||
let system: string;
|
||||
try {
|
||||
system = await instructions(env, messages, readPath(body));
|
||||
} catch (err) {
|
||||
// Ungrounded beats unanswered: the prompt still forbids inventing details.
|
||||
console.warn(`ask: grounding unavailable: ${err}`);
|
||||
system = SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${env.OPENROUTER_API_KEY}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: env.ASK_MODEL,
|
||||
stream: true,
|
||||
reasoning: { effort: "none" },
|
||||
messages: [{ role: "system", content: system }, ...messages],
|
||||
}),
|
||||
signal: request.signal,
|
||||
});
|
||||
if (!res.ok || !res.body) {
|
||||
console.error(`ask: POST /chat/completions -> ${res.status} ${await res.text().catch(() => "")}`);
|
||||
return text("Failed to generate a response.", res.status === 429 ? 429 : 502);
|
||||
}
|
||||
return new Response(textStream(res.body), {
|
||||
headers: { ...cors, "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
import {
|
||||
CAMPAIGN_DAYS,
|
||||
MISS_WHERE,
|
||||
type ProblemRow,
|
||||
RESERVED,
|
||||
REVIEW_CAP,
|
||||
@@ -84,8 +85,7 @@ async function openMisses(db: D1Database): Promise<ProblemRow[]> {
|
||||
.prepare(
|
||||
`SELECT p.* FROM problems p
|
||||
JOIN attempts a ON a.lc_number = p.lc_number
|
||||
WHERE a.id = (SELECT MAX(id) FROM attempts WHERE lc_number = p.lc_number)
|
||||
AND a.result = 'fail' AND p.stage != 'retired'
|
||||
WHERE ${MISS_WHERE}
|
||||
ORDER BY a.date, p.lc_number`,
|
||||
)
|
||||
.all<ProblemRow>();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* Writes are owner-only (webhook author check, HMAC one-tap links, bearer
|
||||
* admin routes); everything else is read-only public.
|
||||
*/
|
||||
import { handleAsk } from "./ask.ts";
|
||||
import { reconcileCatalog } from "./catalog.ts";
|
||||
import { gateBadge, heatmapChart, heatmapPng, ladderChart, progressChart } from "./charts.ts";
|
||||
import { sendDigest } from "./digest.ts";
|
||||
@@ -322,6 +323,8 @@ export default {
|
||||
if (path.startsWith("/admin/") && request.method === "POST") {
|
||||
return await handleAdmin(request, env, path);
|
||||
}
|
||||
// The docs assistant (the static Pages site's `ai.assistant.endpoint`).
|
||||
if (path === "/ask") return await handleAsk(request, env);
|
||||
|
||||
if (path === "/chart/progress.svg") {
|
||||
return new Response(await progressChart(env.DB), { headers: SVG_HEADERS });
|
||||
|
||||
@@ -310,6 +310,15 @@ export async function lookupProblems(
|
||||
export const DUE_WHERE = `stage != 'retired' AND next_review IS NOT NULL AND next_review <= ?1
|
||||
AND (defer_until IS NULL OR defer_until <= ?1)`;
|
||||
|
||||
/**
|
||||
* What an open miss means, in one place: a problem `p` whose most recent
|
||||
* attempt (highest id) failed and that has not retired. The sweep day works
|
||||
* these and the stats' "attempting" count is how many there are, so the two
|
||||
* cannot disagree. Expects `problems p` joined to `attempts a` on lc_number.
|
||||
*/
|
||||
export const MISS_WHERE = `a.id = (SELECT MAX(id) FROM attempts WHERE lc_number = p.lc_number)
|
||||
AND a.result = 'fail' AND p.stage != 'retired'`;
|
||||
|
||||
/** Reviews due on/before `date`, oldest first — the overflow carry order. */
|
||||
export async function dueReviews(db: D1Database, date: string): Promise<ProblemRow[]> {
|
||||
const { results } = await db
|
||||
|
||||
+90
-3
@@ -8,12 +8,17 @@
|
||||
* srs.ts turns a topic's days-since-last-attempt into a band, so "which
|
||||
* concepts have gone cold" has exactly one definition and the dashboard is
|
||||
* free to colour it however it likes.
|
||||
*
|
||||
* `recent`, `attempting` and `year` feed the docs page's LeetCode-style cards
|
||||
* (recently solved, the solved ring, the heatmap and the submissions chart).
|
||||
* They are additive: the terminal dashboard ignores them.
|
||||
*/
|
||||
import {
|
||||
CAMPAIGN_DAYS,
|
||||
CAMPAIGN_START,
|
||||
DIFFICULTIES,
|
||||
DUE_WHERE,
|
||||
MISS_WHERE,
|
||||
addDays,
|
||||
campaignDay,
|
||||
campaignWeek,
|
||||
@@ -54,6 +59,12 @@ interface TopicStat {
|
||||
temperature: Temperature;
|
||||
}
|
||||
|
||||
/** Days in `year`: the 365 ending today, which always covers the current calendar year to date. */
|
||||
const YEAR_DAYS = 365;
|
||||
|
||||
/** How many problems `recent` lists. */
|
||||
const RECENT_LIMIT = 10;
|
||||
|
||||
async function buildTopics(db: D1Database, today: string): Promise<TopicStat[]> {
|
||||
// Three shapes, three statements: batch() carries one row type, and casting
|
||||
// its results apart would cost more than the round trips it saves.
|
||||
@@ -195,9 +206,9 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
||||
);
|
||||
const queue = dates.map((date, i) => ({ date, due: queueRows[i]?.results[0]?.n ?? 0 }));
|
||||
|
||||
// Attempts per campaign day, every day present. This is the heatmap's data
|
||||
// model — the docs page reads the tail of it for "last 7 days" rather than
|
||||
// asking a second question that could answer differently.
|
||||
// Attempts per campaign day, every day present. This is the README heatmap's
|
||||
// data model — the terminal dashboard lays it out by campaign week and reads
|
||||
// its tail for "last 14 days" rather than asking a second question.
|
||||
const end = addDays(CAMPAIGN_START, CAMPAIGN_DAYS - 1);
|
||||
const { results: heatRows } = await db
|
||||
.prepare(
|
||||
@@ -215,16 +226,92 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
||||
return { date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 };
|
||||
});
|
||||
|
||||
// The ten problems most recently solved, dated by each one's FIRST passing
|
||||
// attempt — when it was completed, not when it was last re-solved (LeetCode's
|
||||
// "Recent AC"). Same-day ties fall back to log order.
|
||||
const { results: recentRows } = await db
|
||||
.prepare(
|
||||
`SELECT p.lc_number AS lc, p.title AS title, p.difficulty AS difficulty,
|
||||
MIN(a.date) AS date, MIN(a.id) AS seq
|
||||
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
|
||||
WHERE a.result = 'pass'
|
||||
GROUP BY p.lc_number
|
||||
ORDER BY date DESC, seq DESC
|
||||
LIMIT ?1`,
|
||||
)
|
||||
.bind(RECENT_LIMIT)
|
||||
.all<{ lc: number; title: string; difficulty: string; date: string; seq: number }>();
|
||||
const recent = recentRows.map(({ lc, title, difficulty, date }) => ({ lc, title, difficulty, date }));
|
||||
|
||||
// Problems still standing on a failure — the sweep day's miss log, counted.
|
||||
const missRow = await db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS n FROM problems p
|
||||
JOIN attempts a ON a.lc_number = p.lc_number
|
||||
WHERE ${MISS_WHERE}`,
|
||||
)
|
||||
.first<{ n: number }>();
|
||||
|
||||
// The past year, one entry per day (zeros included), split by difficulty two
|
||||
// ways: every logged attempt, and every first solve — the same first-pass
|
||||
// date `recent` uses. `heat` stays campaign-scoped (the terminal dashboard
|
||||
// lays it out by campaign week); this is the calendar-scoped view the docs
|
||||
// heatmap and submissions chart window and bucket for themselves.
|
||||
const yearStart = addDays(today, -(YEAR_DAYS - 1));
|
||||
const { results: attemptRows } = await db
|
||||
.prepare(
|
||||
`SELECT a.date AS date, p.difficulty AS difficulty,
|
||||
COUNT(*) AS n,
|
||||
SUM(CASE WHEN a.result = 'pass' THEN 1 ELSE 0 END) AS passes
|
||||
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
|
||||
WHERE a.date BETWEEN ?1 AND ?2
|
||||
GROUP BY a.date, p.difficulty`,
|
||||
)
|
||||
.bind(yearStart, today)
|
||||
.all<{ date: string; difficulty: string; n: number; passes: number | null }>();
|
||||
const { results: solvedRows } = await db
|
||||
.prepare(
|
||||
`SELECT date, difficulty, COUNT(*) AS n
|
||||
FROM (SELECT MIN(a.date) AS date, p.difficulty AS difficulty
|
||||
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
|
||||
WHERE a.result = 'pass'
|
||||
GROUP BY p.lc_number)
|
||||
WHERE date BETWEEN ?1 AND ?2
|
||||
GROUP BY date, difficulty`,
|
||||
)
|
||||
.bind(yearStart, today)
|
||||
.all<{ date: string; difficulty: string; n: number }>();
|
||||
const year = Array.from({ length: YEAR_DAYS }, (_, i) => ({
|
||||
date: addDays(yearStart, i),
|
||||
passes: 0,
|
||||
attempts: Object.fromEntries(DIFFICULTIES.map((d) => [d, 0])),
|
||||
solved: Object.fromEntries(DIFFICULTIES.map((d) => [d, 0])),
|
||||
}));
|
||||
const yearIndex = new Map(year.map((d, i) => [d.date, i]));
|
||||
for (const row of attemptRows) {
|
||||
const day = year[yearIndex.get(row.date) ?? -1];
|
||||
if (!day || !(row.difficulty in day.attempts)) continue;
|
||||
day.attempts[row.difficulty] = row.n;
|
||||
day.passes += row.passes ?? 0;
|
||||
}
|
||||
for (const row of solvedRows) {
|
||||
const day = year[yearIndex.get(row.date) ?? -1];
|
||||
if (day && row.difficulty in day.solved) day.solved[row.difficulty] = row.n;
|
||||
}
|
||||
|
||||
return {
|
||||
generated: new Date().toISOString(),
|
||||
campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS },
|
||||
phases: [...phases.values()],
|
||||
ladder,
|
||||
difficulty,
|
||||
attempting: missRow?.n ?? 0,
|
||||
topics: await buildTopics(db, today),
|
||||
gates,
|
||||
streak: await streak(db, today),
|
||||
queue,
|
||||
heat,
|
||||
recent,
|
||||
year,
|
||||
};
|
||||
}
|
||||
@@ -34,8 +34,13 @@
|
||||
"TO_EMAIL": "prnk28@gmail.com",
|
||||
"DOCS_URL": "https://lc.prd.lk",
|
||||
"DOCS_ORIGIN": "https://lc.prd.lk,https://prdlk.github.io",
|
||||
"PUBLIC_URL": "https://srs-api.prdlk.workers.dev"
|
||||
"PUBLIC_URL": "https://srs-api.prdlk.workers.dev",
|
||||
// The docs assistant's OpenRouter model (POST /ask). `:free` variants share
|
||||
// an upstream pool and answer 429 when it is saturated.
|
||||
"ASK_MODEL": "qwen/qwen3.8-27b:free"
|
||||
}
|
||||
|
||||
// Secrets (wrangler secret put): GH_PAT, WEBHOOK_SECRET, LINK_KEY.
|
||||
// OPENROUTER_API_KEY (the docs assistant, POST /ask) is the repo's Actions
|
||||
// secret of that name, put by deploy.yml before every deploy.
|
||||
}
|
||||
+19
-12
@@ -19,9 +19,9 @@
|
||||
* The tag is annotated and backdated (tagger date = finish), so
|
||||
* `git tag --sort=taggerdate` reads as the campaign's history even for tags
|
||||
* minted by a backfill. The release's own published date cannot be backdated
|
||||
* (the API has no field for it), and the docs changelog (githubReleases in
|
||||
* apps/docs/blume.config.ts) dates entries by it — so a backfilled entry
|
||||
* carries its real finish time in the version and the body's first sentence.
|
||||
* (the API has no field for it), so the docs changelog dates each entry from
|
||||
* this timever tag instead (`releasesDatedByTag` in apps/docs/blume.config.ts)
|
||||
* — the tag format is that contract.
|
||||
*
|
||||
* Reconciles instead of reacting: every finished topic without a release gets
|
||||
* one, so a first run backfills the whole campaign; an existing release keeps
|
||||
@@ -111,15 +111,16 @@ finished.sort((a, b) => a.finishedAt.localeCompare(b.finishedAt) || a.topic.numb
|
||||
|
||||
// ── existing releases ────────────────────────────────────────────
|
||||
|
||||
/** Topic -> its release. The body is kept in sync with render() on every run. */
|
||||
const released = new Map<number, { id: number; tag: string; body: string }>();
|
||||
/** Topic -> its release. Name and body are kept in sync with the render on every run. */
|
||||
const released = new Map<number, { id: number; tag: string; name: string; body: string }>();
|
||||
for await (const raw of gh.list(`/repos/${gh.repo}/releases`)) {
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
if (!("id" in raw) || typeof raw.id !== "number") continue;
|
||||
if (!("tag_name" in raw) || typeof raw.tag_name !== "string") continue;
|
||||
if (!("body" in raw) || typeof raw.body !== "string") continue;
|
||||
const name = "name" in raw && typeof raw.name === "string" ? raw.name : "";
|
||||
const m = raw.body.match(/<!-- release-topics: #(\d+) -->/);
|
||||
if (m) released.set(Number(m[1]), { id: raw.id, tag: raw.tag_name, body: raw.body });
|
||||
if (m) released.set(Number(m[1]), { id: raw.id, tag: raw.tag_name, name, body: raw.body });
|
||||
}
|
||||
|
||||
// ── timever ──────────────────────────────────────────────────────
|
||||
@@ -243,8 +244,13 @@ for (const f of finished) {
|
||||
const { topic } = f;
|
||||
const finishedEt = etDate(new Date(f.finishedAt));
|
||||
const existing = released.get(topic.number);
|
||||
// An existing release keeps its tag and commit; only its body is reconciled.
|
||||
// An existing release keeps its tag and commit; only its name and body are
|
||||
// reconciled. Its tag may postdate this clone (released by another run), so
|
||||
// fetch it when missing — read-only, so a dry run does it too.
|
||||
const tag = existing?.tag ?? tags.get(topic.number)!;
|
||||
if (existing && Bun.spawnSync(["git", "rev-parse", "-q", "--verify", `refs/tags/${tag}`], { cwd: ROOT }).exitCode !== 0) {
|
||||
git("fetch", "-q", "origin", `refs/tags/${tag}:refs/tags/${tag}`);
|
||||
}
|
||||
const sha = existing
|
||||
? git("rev-parse", `${tag}^{commit}`)
|
||||
: git("rev-list", "-1", "--first-parent", `--before=${f.finishedAt}`, "HEAD");
|
||||
@@ -263,17 +269,18 @@ for (const f of finished) {
|
||||
files.set(Number(lc), [...(files.get(Number(lc)) ?? []), path]);
|
||||
}
|
||||
const body = render(f, tag, sha, files);
|
||||
const name = topic.title;
|
||||
|
||||
if (existing) {
|
||||
if (existing.body === body) {
|
||||
if (existing.body === body && existing.name === name) {
|
||||
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "released"]);
|
||||
} else if (DRY) {
|
||||
previews.push(`## ${tag} — ${topic.title}\n\n${body}`);
|
||||
previews.push(`## ${name} (${tag})\n\n${body}`);
|
||||
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would update"]);
|
||||
} else {
|
||||
await gh.api(`/repos/${gh.repo}/releases/${existing.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ body }),
|
||||
body: JSON.stringify({ name, body }),
|
||||
});
|
||||
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "updated"]);
|
||||
updated++;
|
||||
@@ -282,7 +289,7 @@ for (const f of finished) {
|
||||
}
|
||||
|
||||
if (DRY) {
|
||||
previews.push(`## ${tag} — ${topic.title}\n\n${body}`);
|
||||
previews.push(`## ${name} (${tag})\n\n${body}`);
|
||||
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would release"]);
|
||||
continue;
|
||||
}
|
||||
@@ -303,7 +310,7 @@ for (const f of finished) {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
tag_name: tag,
|
||||
name: topic.title,
|
||||
name,
|
||||
body,
|
||||
make_latest: topic.number === latest ? "true" : "false",
|
||||
}),
|
||||
|
||||
+223
-29
@@ -2,29 +2,50 @@
|
||||
/**
|
||||
* Sync work/1 leetcode solutions into apps/docs/content/ pages.
|
||||
*
|
||||
* - New problems get a full page (frontmatter, badge, warning, examples,
|
||||
* - New problems get a full page (frontmatter, warning, examples,
|
||||
* constraints, solution) in the gold-standard format.
|
||||
* - Already-ported pages only get their script-owned parts regenerated — the
|
||||
* frontmatter `icon:` (`PAGE_ICON`), the `<Badge>` line and the
|
||||
* `## Solution` section — so hand-curated prose is never touched.
|
||||
* frontmatter `icon:` (`PAGE_ICON`) and the `## Solution` section — so
|
||||
* hand-curated prose is never touched.
|
||||
* - When a problem is solved in both JavaScript and Python the solution
|
||||
* renders as a <CodeGroup> (Python first); single-language solutions
|
||||
* render as a plain fence.
|
||||
* - Every page carries LeetCode's own topic tags as frontmatter `topics:`
|
||||
* (`{ name, slug }` per tag, LeetCode's order), fetched from the GraphQL
|
||||
* `topicTags` once — when the page is created, or on any later run that
|
||||
* finds the key missing — and never refreshed after, so a sync with nothing
|
||||
* new makes no request. Delete the key to refetch it. The page itself
|
||||
* renders no badges: the docs MDX `wrapper`
|
||||
* (`apps/docs/components/ProblemBadges.astro`) turns `topics:` and the
|
||||
* `video:` key `videos.ts` writes into the badge row under the description.
|
||||
* - Every tag a page carries that no campaign topic teaches (not a key of
|
||||
* `apps/docs/topic-landings.ts`) gets a placeholder guide page,
|
||||
* `guides/<slug>.mdx`, with `list:` pointing at LeetCode's problem list for
|
||||
* the tag. Created once (a missing or empty file), then hand-owned except
|
||||
* for `sidebar.badge`, which sync keeps at the guide's backlink count (the
|
||||
* problem pages carrying the tag; dropped at zero); the tag's badge on a
|
||||
* problem page links to it. A guide may be filed into a category `(group)`
|
||||
* folder (e.g. `guides/(2-algorithms)/`) — no route segment, so
|
||||
* `/guides/<slug>` holds — and is found there by slug; a new placeholder
|
||||
* lands at the `guides/` root for a human to file (and give an `icon:`,
|
||||
* which every badge linking to the guide shows; sync warns while one is
|
||||
* missing or shared).
|
||||
*
|
||||
* Two classifications, two homes. The sidebar group is the campaign TOPIC
|
||||
* from the README table (`tree-dfs/`, ordered by topic number via a
|
||||
* script-owned `meta.ts`); the page badge is the data-structure CATEGORY the
|
||||
* file sits under in `work/1/<Difficulty>/<Category>/`. A problem outside the
|
||||
* plan (warm-ups) has no topic, so it groups under its category instead.
|
||||
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`,
|
||||
* ordered by topic number via a script-owned `meta.ts`). A problem outside the
|
||||
* plan (warm-ups) has no topic, so it groups under the data-structure CATEGORY
|
||||
* its file sits under in `work/1/<Difficulty>/<Category>/` instead.
|
||||
*
|
||||
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
|
||||
* The phase folder is a Blume `(group)` — a sidebar section with its own
|
||||
* script-owned `meta.ts`, adding no route segment — while the topic folder is
|
||||
* a real segment (`/tree-dfs/invert-binary-tree`), so a hand-written
|
||||
* `index.mdx` in it is the topic's landing page at `/tree-dfs` and Blume links
|
||||
* the sidebar group row to it. Sync never writes or moves an `index.mdx`, and
|
||||
* never removes a folder that still holds one. An unplanned category group has
|
||||
* no phase and sits at the content root.
|
||||
* The phase folder is a Blume `(group)` — a sidebar section adding no route
|
||||
* segment — while the topic folder is a real segment
|
||||
* (`/tree-dfs/invert-binary-tree`), so a hand-written `index.mdx` in it is the
|
||||
* topic's landing page at `/tree-dfs` and Blume links the sidebar group row to
|
||||
* it. Sync never writes or moves an `index.mdx`, and never removes a folder
|
||||
* that still holds one. A phase folder's `meta.ts` is written once, when
|
||||
* missing, then hand-owned (its title is yours to rename); a topic's is
|
||||
* script-owned. An unplanned category group has no phase and sits at the
|
||||
* content root.
|
||||
*
|
||||
* Only the first-solve bucket is read: work/3 and work/7 hold blind re-solves
|
||||
* of problems whose page already exists (see work.ts), and a fresh scaffold
|
||||
@@ -38,6 +59,8 @@
|
||||
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
|
||||
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
|
||||
|
||||
const ROOT = join(import.meta.dir, "..", "..");
|
||||
const WORK = join(ROOT, "work", "1");
|
||||
const DOCS = join(ROOT, "apps", "docs", "content");
|
||||
@@ -295,7 +318,13 @@ function phaseDir(phase: Phase): string {
|
||||
return `(${String(phase.order).padStart(2, "0")}-${slugify(phase.name)})`;
|
||||
}
|
||||
|
||||
/** Blume `(group)` meta for a phase: the overview page's heading, in phase order. */
|
||||
/**
|
||||
* Blume `(group)` meta for a phase: the overview page's heading, in phase
|
||||
* order. Written only when the folder has none — the title is hand-owned after
|
||||
* that. Nothing is lost by never rewriting it: the order and name it is built
|
||||
* from are also the folder name (`phaseDir`), so a README change to either
|
||||
* lands in a new folder that gets a fresh meta.
|
||||
*/
|
||||
function phaseMeta(phase: Phase): string {
|
||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: ${JSON.stringify(`Phase ${phase.order}: ${phase.name}`)}, order: ${phase.order} });\n`;
|
||||
}
|
||||
@@ -316,6 +345,104 @@ function groupMeta(topic: Topic): string {
|
||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
||||
}
|
||||
|
||||
// ── LeetCode topic tags ──────────────────────────────────────────
|
||||
|
||||
/** One of LeetCode's topic tags. `slug` is not derivable from `name` (`Graph Theory` → `graph`). */
|
||||
interface TopicTag {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The problem's topic tags, in LeetCode's order — or undefined, warned, when
|
||||
* LeetCode cannot answer (offline commit, unknown slug): the page is written
|
||||
* without `topics:` and the next sync fills it in.
|
||||
*/
|
||||
async function fetchTopicTags(sol: Solution): Promise<TopicTag[] | undefined> {
|
||||
try {
|
||||
const res = await fetch("https://leetcode.com/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"user-agent": "Mozilla/5.0",
|
||||
referer: `https://leetcode.com/problems/${sol.slug}/`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: "query topicTags($titleSlug: String!) { question(titleSlug: $titleSlug) { topicTags { name slug } } }",
|
||||
variables: { titleSlug: sol.slug },
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`POST /graphql -> ${res.status} ${res.statusText}`);
|
||||
const data = (await res.json()) as { data?: { question?: { topicTags?: TopicTag[] } | null } };
|
||||
const tags = data.data?.question?.topicTags;
|
||||
if (!Array.isArray(tags)) throw new Error("POST /graphql -> no such question");
|
||||
return tags.map(({ name, slug }) => ({ name, slug }));
|
||||
} catch (err) {
|
||||
console.warn(`no topics for ${sol.num}.${sol.slug}: ${err instanceof Error ? err.message : err}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** The frontmatter `topics:` block, one flow mapping per tag (JSON strings are valid YAML). */
|
||||
function topicsYaml(tags: TopicTag[]): string {
|
||||
if (tags.length === 0) return "topics: []";
|
||||
return ["topics:", ...tags.map((t) => ` - { name: ${JSON.stringify(t.name)}, slug: ${JSON.stringify(t.slug)} }`)].join("\n");
|
||||
}
|
||||
|
||||
/** The frontmatter's `topics:` as sync wrote them (or a human edited them); [] when absent. */
|
||||
function frontmatterTopics(page: string): TopicTag[] {
|
||||
const fm = page.match(/^---\n([\s\S]*?)\n---/)?.[1];
|
||||
const data: unknown = fm ? Bun.YAML.parse(fm) : undefined;
|
||||
if (!data || typeof data !== "object" || !("topics" in data) || !Array.isArray(data.topics)) return [];
|
||||
return data.topics.filter((t): t is TopicTag => typeof t?.name === "string" && typeof t?.slug === "string");
|
||||
}
|
||||
|
||||
// ── guides ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Placeholder for a tag no campaign topic teaches: the `/guides/<slug>` page a
|
||||
* problem's topic badge links to. `list:` is the tag's LeetCode problem list,
|
||||
* rendered as a badge by the docs MDX `wrapper`. Written once — the page
|
||||
* is hand-owned from then on.
|
||||
*/
|
||||
function guidePage(tag: TopicTag): string {
|
||||
return [
|
||||
"---",
|
||||
`title: ${JSON.stringify(tag.name)}`,
|
||||
`list: ${JSON.stringify(`https://leetcode.com/problem-list/${tag.slug}/`)}`,
|
||||
"---",
|
||||
"",
|
||||
":::note",
|
||||
`This guide has not been written yet. The LeetCode list above has every problem tagged ${tag.name}.`,
|
||||
":::",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** Folder meta for `guides/`: its own header tab (`navigation.tabs` in blume.config.ts), so only the label. */
|
||||
const GUIDES_META = `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: "Guides" });\n`;
|
||||
|
||||
/**
|
||||
* A guide's frontmatter with `sidebar.badge` set to its backlink count — the
|
||||
* one key sync owns on a hand-owned guide — or dropped (with a `sidebar:` left
|
||||
* empty) at zero, so the sidebar shows a count only where there is something
|
||||
* to count.
|
||||
*/
|
||||
function withBacklinkBadge(page: string, count: number): string {
|
||||
const fm = page.match(/^---\n[\s\S]*?\n---/);
|
||||
if (!fm) return page;
|
||||
let head = fm[0].replace(/^ {2}badge: .*\n/m, "");
|
||||
if (count > 0) {
|
||||
const line = ` badge: '${count}'`;
|
||||
head = /^sidebar:$/m.test(head)
|
||||
? head.replace(/^sidebar:$/m, `sidebar:\n${line}`)
|
||||
: head.replace(/\n---$/, `\nsidebar:\n${line}\n---`);
|
||||
} else {
|
||||
head = head.replace(/^sidebar:\n(?! {2})/m, "");
|
||||
}
|
||||
return head + page.slice(fm[0].length);
|
||||
}
|
||||
|
||||
// ── inline-code formatting heuristics ────────────────────────────
|
||||
|
||||
/** Identifiers worth backticking in prose, harvested from the code. */
|
||||
@@ -420,11 +547,7 @@ function warningText(h: Header): string {
|
||||
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
|
||||
const PAGE_ICON = "dot";
|
||||
|
||||
function badgeLine(sol: Solution): string {
|
||||
return `<Badge variant="accent">${sol.category}</Badge>`;
|
||||
}
|
||||
|
||||
function renderPage(sol: Solution): string {
|
||||
function renderPage(sol: Solution, tags: TopicTag[] | undefined): string {
|
||||
const h = sol.header;
|
||||
const ids = identifiers(sol);
|
||||
const description = h.statement[0]?.replace(/\.\s*$/, "") ?? "";
|
||||
@@ -435,13 +558,12 @@ function renderPage(sol: Solution): string {
|
||||
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
|
||||
/[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`,
|
||||
`icon: ${PAGE_ICON}`,
|
||||
...(tags ? [topicsYaml(tags)] : []),
|
||||
"sidebar:",
|
||||
` label: '${h.title.replaceAll("'", "''")}'`,
|
||||
` badge: '${h.difficulty}'`,
|
||||
"---",
|
||||
"",
|
||||
badgeLine(sol),
|
||||
"",
|
||||
);
|
||||
|
||||
const warning = warningText(h);
|
||||
@@ -523,10 +645,19 @@ function ensureIcon(page: string): string {
|
||||
return next + page.slice(fm[0].length);
|
||||
}
|
||||
|
||||
/** Insert `topics:` before `sidebar:` when the frontmatter lacks it; an existing list is kept as is. */
|
||||
async function ensureTopics(page: string, sol: Solution): Promise<string> {
|
||||
const fm = page.match(/^---\n[\s\S]*?\n---/);
|
||||
if (!fm || /^topics:/m.test(fm[0])) return page;
|
||||
const tags = await fetchTopicTags(sol);
|
||||
if (!tags) return page;
|
||||
return fm[0].replace(/^sidebar:$/m, `${topicsYaml(tags)}\nsidebar:`) + page.slice(fm[0].length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset just past the script-owned `## Solution` section: the next top-level
|
||||
* heading that is not inside a code fence, or end of file. Sections a human
|
||||
* appends after the solution (`## Explanation`, …) survive regeneration.
|
||||
* appends after the solution survive regeneration.
|
||||
*/
|
||||
function solutionEnd(page: string, from: number): number {
|
||||
const lines = page.slice(from).split("\n");
|
||||
@@ -555,6 +686,8 @@ const groups = new Map<string, Topic>(); // topic folder -> topic, for meta.ts
|
||||
const phases = new Map<string, Phase>(); // phase folder -> phase, for meta.ts
|
||||
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
|
||||
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
|
||||
const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag
|
||||
const tagged = new Map<string, number>(); // tag slug -> how many problem pages carry it
|
||||
|
||||
for (const sol of ordered) {
|
||||
const langs = Object.keys(sol.code).sort().join("+");
|
||||
@@ -570,6 +703,7 @@ for (const sol of ordered) {
|
||||
const existing = pages.get(sol.num);
|
||||
await mkdir(join(DOCS, dir), { recursive: true });
|
||||
|
||||
let written: string; // the page as it stands after this run, for its topics
|
||||
if (existing) {
|
||||
const page = await Bun.file(join(DOCS, existing)).text();
|
||||
const at = page.indexOf("## Solution");
|
||||
@@ -579,7 +713,7 @@ for (const sol of ordered) {
|
||||
}
|
||||
const tail = page.slice(solutionEnd(page, at));
|
||||
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
||||
const next = ensureIcon(ensureLabel(body, sol.header)).replace(/^<Badge variant="accent">.*<\/Badge>$/m, badgeLine(sol));
|
||||
const next = await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol);
|
||||
const actions: string[] = [];
|
||||
if (existing !== name) {
|
||||
await rename(join(DOCS, existing), join(DOCS, name));
|
||||
@@ -591,9 +725,16 @@ for (const sol of ordered) {
|
||||
actions.push("updated");
|
||||
}
|
||||
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
||||
written = next;
|
||||
} else {
|
||||
await Bun.write(join(DOCS, name), renderPage(sol));
|
||||
const page = renderPage(sol, await fetchTopicTags(sol));
|
||||
await Bun.write(join(DOCS, name), page);
|
||||
report.push([workRef, name, "created"]);
|
||||
written = page;
|
||||
}
|
||||
for (const tag of frontmatterTopics(written)) {
|
||||
tags.set(tag.slug, tag);
|
||||
tagged.set(tag.slug, (tagged.get(tag.slug) ?? 0) + 1);
|
||||
}
|
||||
// The route drops the phase `(group)` and the page's number prefix.
|
||||
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
|
||||
@@ -615,11 +756,64 @@ for (const sol of ordered) {
|
||||
}
|
||||
}
|
||||
|
||||
// Script-owned folder metas — one per live phase and topic folder — rewritten
|
||||
// only when they differ.
|
||||
// One guide page per tag that no campaign topic teaches, created when missing
|
||||
// (an empty file counts as missing) and never rewritten. A guide is found by
|
||||
// slug anywhere under guides/ — a human files it into a category `(group)`,
|
||||
// which leaves its route alone — and a new one lands at the guides/ root. A
|
||||
// guide whose tag no page carries any more is left alone: it is hand-owned.
|
||||
const guideTags = [...tags.values()]
|
||||
.filter((tag) => !TOPIC_LANDINGS[tag.slug])
|
||||
.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||
const guideFiles = new Map<string, string>(); // slug -> path relative to DOCS
|
||||
for await (const rel of new Bun.Glob("guides/**/*.mdx").scan(DOCS)) guideFiles.set(basename(rel, ".mdx"), rel);
|
||||
for (const tag of guideTags) {
|
||||
const rel = guideFiles.get(tag.slug) ?? `guides/${tag.slug}.mdx`;
|
||||
const file = Bun.file(join(DOCS, rel));
|
||||
if ((await file.exists()) && file.size > 0) continue;
|
||||
await Bun.write(file, guidePage(tag));
|
||||
report.push([`tag ${tag.slug}`, rel, "created"]);
|
||||
}
|
||||
|
||||
// Every guide's sidebar badge is its backlink count: the problem pages whose
|
||||
// tag badge links to it, which is every page carrying the tag, since a guide's
|
||||
// tag has no landing (`tagTarget()`). A guide whose tag a landing has since
|
||||
// claimed has no backlinks left. Counts move with every solved problem, so the
|
||||
// badge is rewritten whenever it differs; the rest of the guide is untouched.
|
||||
// The same pass checks each guide's hand-picked `icon:`, which is also the icon
|
||||
// on every problem-page badge linking to the guide: it must exist, must not be
|
||||
// a topic group's (`TOPIC_ICONS`), so a guide badge never reads as a topic's,
|
||||
// and must not be another guide's.
|
||||
const topicIcons = new Set(Object.values(TOPIC_ICONS));
|
||||
const guideIcons = new Map<string, string>(); // icon -> the guide that took it first
|
||||
for await (const rel of new Bun.Glob("guides/**/*.mdx").scan(DOCS)) {
|
||||
const slug = basename(rel, ".mdx");
|
||||
const count = TOPIC_LANDINGS[slug] ? 0 : (tagged.get(slug) ?? 0);
|
||||
const page = await Bun.file(join(DOCS, rel)).text();
|
||||
const icon = page.match(/^---\n[\s\S]*?\n---/)?.[0].match(/^icon: (\S+)$/m)?.[1];
|
||||
if (!icon) console.warn(`${rel}: no icon; its badges render without one — file it and add \`icon:\``);
|
||||
else if (topicIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is a topic's (TOPIC_ICONS); pick another`);
|
||||
else if (guideIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is ${guideIcons.get(icon)}'s too; pick another`);
|
||||
else guideIcons.set(icon, rel);
|
||||
const next = withBacklinkBadge(page, count);
|
||||
if (next === page) continue;
|
||||
await Bun.write(join(DOCS, rel), next);
|
||||
report.push([`${count} backlinks`, rel, "badged"]);
|
||||
}
|
||||
|
||||
// Phase metas: created when missing (an empty file counts as missing), never
|
||||
// rewritten, so a renamed phase title survives every sync.
|
||||
for (const [dir, phase] of phases) {
|
||||
const file = Bun.file(join(DOCS, dir, "meta.ts"));
|
||||
if ((await file.exists()) && file.size > 0) continue;
|
||||
await Bun.write(file, phaseMeta(phase));
|
||||
report.push([`phase ${phase.order}`, `${dir}/meta.ts`, "created"]);
|
||||
}
|
||||
|
||||
// Script-owned folder metas — one per live topic folder, plus guides/ —
|
||||
// rewritten only when they differ.
|
||||
const metas: [label: string, dir: string, meta: string][] = [
|
||||
...[...phases].map(([dir, phase]): [string, string, string] => [`phase ${phase.order}`, dir, phaseMeta(phase)]),
|
||||
...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]),
|
||||
...(guideTags.length ? [["guides", "guides", GUIDES_META] as [string, string, string]] : []),
|
||||
];
|
||||
for (const [label, dir, meta] of metas) {
|
||||
const path = join(DOCS, dir, "meta.ts");
|
||||
|
||||
+22
-24
@@ -2,24 +2,23 @@
|
||||
/**
|
||||
* Find the NeetCode explanation video for every docs page that has none.
|
||||
*
|
||||
* A finished solution page ends with `## Explanation` holding one
|
||||
* `<YouTube id="…" title="…" />` — NeetCode's walkthrough of the problem.
|
||||
* `sync.ts` never writes that section (it owns only `## Solution`), so pages
|
||||
* accumulate without one. This script lists every numbered page lacking a
|
||||
* `<YouTube` embed and looks each up on NeetCode's two channels, main channel
|
||||
* first, accepting only a title that names the page's exact LC number
|
||||
* (`… - Leetcode 226 - …`) and is not a #short. No YouTube API key: the
|
||||
* channel search page embeds its results as `ytInitialData` JSON, which is
|
||||
* all we parse. Ranking is the channel's own — the first exact-number match
|
||||
* wins.
|
||||
* A finished solution page carries frontmatter `video: <YouTube id>` —
|
||||
* NeetCode's walkthrough of the problem — which the docs MDX `wrapper`
|
||||
* (`apps/docs/components/ProblemBadges.astro`) renders as the red
|
||||
* "Explanation" badge beside the topic badges. `sync.ts` never writes that key
|
||||
* (it owns `icon:`, `topics:` and `## Solution`), so pages accumulate without
|
||||
* one. This script lists every numbered page lacking `video:` and looks each
|
||||
* up on NeetCode's two channels, main channel first, accepting only a title
|
||||
* that names the page's exact LC number (`… - Leetcode 226 - …`) and is not a
|
||||
* #short. No YouTube API key: the channel search page embeds its results as
|
||||
* `ytInitialData` JSON, which is all we parse. Ranking is the channel's own —
|
||||
* the first exact-number match wins.
|
||||
*
|
||||
* bun apps/cli/videos.ts # report: page → video (or "no match")
|
||||
* bun apps/cli/videos.ts --write # also append `## Explanation` + embed
|
||||
* bun apps/cli/videos.ts --write # also add `video:` to the frontmatter
|
||||
*
|
||||
* --write appends at end of file (below `## Solution`, where sync.ts leaves
|
||||
* human sections alone) and adds the `## Explanation` heading only when the
|
||||
* page lacks it. A page with no match is left untouched, so re-runs are
|
||||
* no-ops once every page has its embed.
|
||||
* --write inserts the key before `sidebar:`. A page with no match is left
|
||||
* untouched, so re-runs are no-ops once every page has its video.
|
||||
*/
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -126,23 +125,21 @@ interface Page {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** Numbered solution pages without a `<YouTube` embed, by LC number. */
|
||||
/** The page's frontmatter block, `---` to `---`. */
|
||||
const FRONTMATTER = /^---\n[\s\S]*?\n---/;
|
||||
|
||||
/** Numbered solution pages without a `video:` key, by LC number. */
|
||||
async function pagesMissingVideo(): Promise<Page[]> {
|
||||
const pages: Page[] = [];
|
||||
for (const rel of await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))) {
|
||||
const text = await Bun.file(join(DOCS, rel)).text();
|
||||
const t = text.match(/^title: '(\d+)\./m);
|
||||
if (t && !text.includes("<YouTube")) pages.push({ num: Number(t[1]), rel, text });
|
||||
const fm = text.match(FRONTMATTER)?.[0] ?? "";
|
||||
if (t && !/^video:/m.test(fm)) pages.push({ num: Number(t[1]), rel, text });
|
||||
}
|
||||
return pages.sort((a, b) => a.num - b.num);
|
||||
}
|
||||
|
||||
function withEmbed(page: Page, video: Video): string {
|
||||
const embed = `<YouTube id="${video.id}" title="${video.title.replaceAll('"', "'")}" />`;
|
||||
const heading = /^## Explanation\s*$/m.test(page.text) ? "" : "## Explanation\n\n";
|
||||
return `${page.text.replace(/\s*$/, "\n")}\n${heading}${embed}\n`;
|
||||
}
|
||||
|
||||
// ── main ─────────────────────────────────────────────────────────
|
||||
|
||||
const missing = await pagesMissingVideo();
|
||||
@@ -162,7 +159,8 @@ for (const page of missing) {
|
||||
found++;
|
||||
let status = "";
|
||||
if (WRITE) {
|
||||
await Bun.write(join(DOCS, page.rel), withEmbed(page, video));
|
||||
const next = page.text.replace(FRONTMATTER, (fm) => fm.replace(/^sidebar:$/m, `video: ${JSON.stringify(video.id)}\nsidebar:`));
|
||||
await Bun.write(join(DOCS, page.rel), next);
|
||||
status = "written";
|
||||
}
|
||||
rows.push([page.rel, `https://youtu.be/${video.id}`, `${video.title} (${video.channel})`, status]);
|
||||
|
||||
+127
-6
@@ -1,7 +1,43 @@
|
||||
import { defineConfig } from "blume";
|
||||
import { filesystem, githubReleases } from "blume/sources";
|
||||
import type { StandardSchema } from "blume/core/standard-schema.ts";
|
||||
import { custom, filesystem } from "blume/sources";
|
||||
import { githubReleasesSource } from "blume/sources/github-releases.ts";
|
||||
import redirects from "./redirects";
|
||||
|
||||
/**
|
||||
* GitHub releases → /changelog, dated by the topic's real finish instant.
|
||||
*
|
||||
* Blume dates a release entry by `published_at`, which the GitHub API cannot
|
||||
* backdate — so the topic releases backfilled on one day (apps/cli/
|
||||
* release-topics.ts) would all read that day. Their tags are timever
|
||||
* (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the moment the topic finished, so this
|
||||
* fetch rewrites each timever release's `published_at` to that instant before
|
||||
* Blume maps it; anything else passes through untouched. The config-level
|
||||
* `githubReleases()` accepts no `fetchImpl`, hence the `custom()` wrap of the
|
||||
* underlying source (the `blume/sources/*.ts` path the Blume docs use for
|
||||
* custom sources), with the context the built-in adapter would receive.
|
||||
*/
|
||||
const TIMEVER = /^(\d{4})(\d{2})(\d{2})\.(\d{2})(\d{2})(\d{2})\.\d{9}$/;
|
||||
|
||||
const releasesDatedByTag: typeof fetch = async (input, init) => {
|
||||
const res = await fetch(input, init);
|
||||
if (!res.ok) return res;
|
||||
const releases: unknown = await res.json();
|
||||
if (Array.isArray(releases)) {
|
||||
for (const release of releases) {
|
||||
const m =
|
||||
typeof release?.tag_name === "string"
|
||||
? release.tag_name.match(TIMEVER)
|
||||
: null;
|
||||
if (m)
|
||||
release.published_at = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
|
||||
}
|
||||
}
|
||||
// Blume reads only `ok` and the JSON body, so the original headers (whose
|
||||
// content-length/encoding describe the upstream bytes) are not carried over.
|
||||
return Response.json(releases, { status: res.status });
|
||||
};
|
||||
|
||||
// Blume's defaults (`inter`, `ibm-plex-mono`) are fetched from fonts.gstatic.com
|
||||
// on every build, which has timed out on the Pages runner and, fully offline,
|
||||
// silently ships no fonts at all. The same two families from Fontsource, as
|
||||
@@ -30,6 +66,49 @@ const plexMono = {
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom frontmatter, rendered as the badge row by
|
||||
* components/ProblemBadges.astro. Problem pages: `topics:` is LeetCode's topic
|
||||
* tags (written by apps/cli/sync.ts), `video:` the NeetCode walkthrough's
|
||||
* YouTube id (apps/cli/videos.ts). Guide pages (content/guides/, created by
|
||||
* sync): `list:` is the tag's LeetCode problem list. Blume rejects undeclared
|
||||
* keys and takes any Standard Schema; these are hand-rolled so the docs
|
||||
* workspace needs no zod. All optional — every other page carries none.
|
||||
*/
|
||||
const optional = <T>(
|
||||
message: string,
|
||||
valid: (value: unknown) => value is T,
|
||||
): StandardSchema<unknown, T | undefined> => ({
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "prdlk",
|
||||
validate: (value) =>
|
||||
value === undefined || valid(value)
|
||||
? { value }
|
||||
: { issues: [{ message }] },
|
||||
},
|
||||
});
|
||||
|
||||
const topicsSchema = optional(
|
||||
"expected a list of { name, slug } LeetCode topic tags",
|
||||
(value): value is { name: string; slug: string }[] =>
|
||||
Array.isArray(value) &&
|
||||
value.every(
|
||||
(t) => typeof t?.name === "string" && typeof t?.slug === "string",
|
||||
),
|
||||
);
|
||||
const videoSchema = optional(
|
||||
"expected an 11-character YouTube video id",
|
||||
(value): value is string =>
|
||||
typeof value === "string" && /^[\w-]{11}$/.test(value),
|
||||
);
|
||||
const listSchema = optional(
|
||||
"expected a https://leetcode.com/problem-list/<slug>/ URL",
|
||||
(value): value is string =>
|
||||
typeof value === "string" &&
|
||||
/^https:\/\/leetcode\.com\/problem-list\/[\w-]+\/$/.test(value),
|
||||
);
|
||||
|
||||
export default defineConfig({
|
||||
title: "prdlk",
|
||||
description: "Documentation for my LeetCode solutions",
|
||||
@@ -43,14 +122,52 @@ export default defineConfig({
|
||||
},
|
||||
lastModified: "git",
|
||||
redirects,
|
||||
frontmatter: {
|
||||
extend: { topics: topicsSchema, video: videoSchema, list: listSchema },
|
||||
},
|
||||
seo: {
|
||||
og: { enabled: true },
|
||||
rss: { enabled: true, types: ["changelog"] },
|
||||
sitemap: true,
|
||||
robots: true,
|
||||
structuredData: true,
|
||||
x: { handle: "@prdlk" },
|
||||
},
|
||||
// Blume's own assistant route is server-rendered (it reads the provider key
|
||||
// per request), which a static Pages build cannot ship. The SRS Worker
|
||||
// answers instead (apps/api/src/ask.ts: OpenRouter, grounded on this site's
|
||||
// llms-full.txt, which `agents.llmsTxt` publishes).
|
||||
ai: {
|
||||
assistant: {
|
||||
enabled: true,
|
||||
endpoint: "https://srs-api.prdlk.workers.dev/ask",
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
llmsTxt: true,
|
||||
mcp: { enabled: false },
|
||||
},
|
||||
content: {
|
||||
sources: [
|
||||
filesystem({ root: "content" }),
|
||||
githubReleases({
|
||||
prefix: "changelog",
|
||||
owner: "prdlk",
|
||||
repo: "leetcode",
|
||||
}),
|
||||
custom(
|
||||
githubReleasesSource(
|
||||
{
|
||||
name: "changelog",
|
||||
prefix: "changelog",
|
||||
owner: "prdlk",
|
||||
repo: "leetcode",
|
||||
fetchImpl: releasesDatedByTag,
|
||||
},
|
||||
// What resolve.ts hands the built-in adapter: `.blume/cache/<name>/`,
|
||||
// and a refresh on every load (the source reads nothing else).
|
||||
{
|
||||
projectRoot: import.meta.dirname,
|
||||
cacheDir: `${import.meta.dirname}/.blume/cache/changelog`,
|
||||
mode: "build",
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
@@ -60,5 +177,9 @@ export default defineConfig({
|
||||
sidebar: {
|
||||
display: "flat", // "flat" | "group" | "page"
|
||||
},
|
||||
tabs: [
|
||||
{ label: "Guides", path: "/guides" },
|
||||
{ label: "Changelog", path: "/changelog" },
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "theme.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Project MDX components, available on every page with no import. Static
|
||||
* `.astro` files ship no framework JS (a custom element carries any
|
||||
* behaviour); interactive React lives in `islands/` instead.
|
||||
* Project MDX components, available on every page with no import, and layout
|
||||
* slot overrides. Static `.astro` files ship no framework JS (a custom element
|
||||
* carries any behaviour); interactive React lives in `islands/` instead.
|
||||
*/
|
||||
import { defineComponents } from "blume";
|
||||
|
||||
@@ -9,5 +9,12 @@ export default defineComponents({
|
||||
mdx: {
|
||||
Carousel: "./components/Carousel.astro",
|
||||
CarouselImage: "./components/CarouselImage.astro",
|
||||
// MDX's own content wrapper: problem pages' topic + explanation badges, from
|
||||
// `topics:` / `video:` frontmatter, between the description and the body.
|
||||
wrapper: "./components/ProblemBadges.astro",
|
||||
},
|
||||
layout: {
|
||||
// Topic landing + guide pages' "what links here": every problem pointing at the page.
|
||||
PageFooter: "./components/BacklinksList.astro",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
/**
|
||||
* 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
/**
|
||||
* The MDX `wrapper`: the badge row 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 guide
|
||||
* page, `/guides/<slug>` (sync creates one per such tag), and is outlined.
|
||||
* `tagTarget()` picks the route (and between two landings, e.g. DFS); the
|
||||
* `PageFooter` backlinks list reads the same answer. Each tag badge is a
|
||||
* pill carrying the icon its target shows in the sidebar (the topic's
|
||||
* `meta.ts` icon, the guide's `icon:`) and the tag's own colour
|
||||
* (`lib/tag-colors.ts`), assigned over every tag the site has a page for —
|
||||
* no two tags share one, and tags that share problem pages are spread
|
||||
* furthest apart. A tag with no page (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 at the end of the row.
|
||||
* - `list:` (guide pages, written by sync on create) is the tag's LeetCode
|
||||
* problem list, an orange "LeetCode list" badge.
|
||||
*/
|
||||
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 { 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 list: string | undefined = entry?.data.list;
|
||||
|
||||
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 page — the ones a landing teaches, plus every served guide —
|
||||
// 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),
|
||||
...[...served].flatMap((path) => path.match(/^\/guides\/([^/]+)$/u)?.slice(1) ?? []),
|
||||
],
|
||||
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: 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";
|
||||
---
|
||||
|
||||
{
|
||||
(badges.length > 0 || video || list) && (
|
||||
<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>
|
||||
)}
|
||||
{list && (
|
||||
<a href={list} target="_blank" rel="noopener" class={linkClass}>
|
||||
<Badge variant="warning" icon="list" shape="pill">
|
||||
LeetCode list
|
||||
</Badge>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<slot />
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* What the four progress islands share besides the data: the not-ready body
|
||||
* (a skeleton while /api/stats loads, one muted line if it fails) and the
|
||||
* hover tooltip for the two charts drawn as plain SVG (the heatmap calendar
|
||||
* and the topic bubbles — Recharts has neither layout). The tooltip wears the
|
||||
* exact classes of shadcn's ChartTooltipContent, so all four cards' tooltips
|
||||
* look like one component.
|
||||
*/
|
||||
import { type PointerEvent, type ReactNode, useRef, useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import type { Load } from "../../lib/stats";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function StatsPending({ load, className }: { load: Exclude<Load, { phase: "ready" }>; className: string }) {
|
||||
if (load.phase === "loading") return <Skeleton className={className} />;
|
||||
return (
|
||||
<p className="my-0! text-muted-foreground text-sm">
|
||||
Stats unavailable — they are served live from the SRS Worker, and it did not respond.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
interface Tip {
|
||||
x: number;
|
||||
y: number;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A pointer-following tooltip for SVG marks. Put `ref` on a `relative`
|
||||
* wrapper, call `show` from a mark's pointer events, render `tip` inside the
|
||||
* wrapper. Positions are relative to the wrapper, so page scroll never skews it.
|
||||
*/
|
||||
export function useHoverTip<T extends HTMLElement>() {
|
||||
const ref = useRef<T>(null);
|
||||
const [tip, setTip] = useState<Tip | null>(null);
|
||||
const show = (event: PointerEvent, content: ReactNode) => {
|
||||
const box = ref.current?.getBoundingClientRect();
|
||||
if (box) setTip({ x: event.clientX - box.left, y: event.clientY - box.top, content });
|
||||
};
|
||||
const hide = () => setTip(null);
|
||||
const node = tip && (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute z-10 grid min-w-[8rem] -translate-x-1/2 -translate-y-[calc(100%+0.75rem)] items-start gap-1.5",
|
||||
"rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
)}
|
||||
style={{ left: tip.x, top: tip.y }}
|
||||
>
|
||||
{tip.content}
|
||||
</div>
|
||||
);
|
||||
return { ref, show, hide, node };
|
||||
}
|
||||
|
||||
/** One tooltip row, the shape ChartTooltipContent draws: colour dot, label, mono value. */
|
||||
export function TipRow({ color, label, value }: { color: string; label: ReactNode; value: ReactNode }) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="size-2.5 shrink-0 rounded-[2px]" style={{ background: color }} />
|
||||
<div className="flex flex-1 items-center justify-between gap-4 leading-none">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="font-medium font-mono text-foreground tabular-nums">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "../../lib/utils"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
import type { TooltipValueType } from "recharts"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }. Blume switches dark mode with
|
||||
// [data-theme="dark"] on <html>, not shadcn's .dark class.
|
||||
const THEMES = { light: "", dark: ':root[data-theme="dark"]' } as const
|
||||
|
||||
const INITIAL_DIMENSION = { width: 320, height: 200 } as const
|
||||
type TooltipNameType = number | string
|
||||
|
||||
export type ChartConfig = Record<
|
||||
string,
|
||||
{
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
>
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
initialDimension = INITIAL_DIMENSION,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
initialDimension?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
}) {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer
|
||||
initialDimension={initialDimension}
|
||||
>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme ?? config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
} & Omit<
|
||||
RechartsPrimitive.DefaultTooltipContentProps<
|
||||
TooltipValueType,
|
||||
TooltipNameType
|
||||
>,
|
||||
"accessibilityLayer"
|
||||
>) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? (config[label]?.label ?? label)
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item, index) => {
|
||||
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color ?? item.payload?.fill ?? item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label ?? item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value != null && (
|
||||
<span className="font-mono font-medium text-foreground tabular-nums">
|
||||
{typeof item.value === "number"
|
||||
? item.value.toLocaleString()
|
||||
: String(item.value)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
} & RechartsPrimitive.DefaultLegendContentProps) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item, index) => {
|
||||
const key = `${nameKey ?? item.dataKey ?? "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "../../lib/utils"
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
|
||||
|
||||
import { toggleVariants } from "./toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number
|
||||
}
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 0,
|
||||
})
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number
|
||||
}) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-spacing={spacing}
|
||||
style={{ "--gap": spacing } as React.CSSProperties}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
data-spacing={context.spacing}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
||||
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "../../lib/utils"
|
||||
import { Toggle as TogglePrimitive } from "radix-ui"
|
||||
|
||||
// Blume's `accent` is its brand colour, not shadcn's subtle surface: the on
|
||||
// and hover states use background/muted instead (see theme.css).
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-muted hover:text-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 min-w-9 px-2",
|
||||
sm: "h-8 min-w-8 px-1.5",
|
||||
lg: "h-10 min-w-10 px-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
+4
-6
@@ -2,13 +2,15 @@
|
||||
title: '153. Find Minimum in Rotated Sorted Array'
|
||||
description: 'Suppose an array of length n sorted in ascending order is rotated between 1 and n times. For example, the array nums = [0,1,2,4,5,6,7] might become:'
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Binary Search", slug: "binary-search" }
|
||||
video: "nIVW4P8b1VA"
|
||||
sidebar:
|
||||
label: 'Find Minimum in Rotated Sorted Array'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
Notice that rotating an array [a[0], a[1], a[2], ..., a[n-1]] 1 time results in the array [a[n-1], a[0], a[1], a[2], ..., a[n-2]].
|
||||
::::
|
||||
@@ -69,7 +71,3 @@ class Solution:
|
||||
|
||||
return nums[lowest_index]
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="nIVW4P8b1VA" title="Find Minimum in Rotated Sorted Array - Binary Search - Leetcode 153 - Python" />
|
||||
+4
-6
@@ -2,13 +2,15 @@
|
||||
title: '33. Search in Rotated Sorted Array'
|
||||
description: There is an integer array nums sorted in ascending order (with distinct values)
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Binary Search", slug: "binary-search" }
|
||||
video: "U8XENwh8Oy8"
|
||||
sidebar:
|
||||
label: 'Search in Rotated Sorted Array'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
You must write an algorithm with O(log n) runtime complexity.
|
||||
::::
|
||||
@@ -93,7 +95,3 @@ var search = function(nums, target) {
|
||||
return -1;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="U8XENwh8Oy8" title="Search in rotated sorted array - Leetcode 33 - Python" />
|
||||
+4
-6
@@ -2,13 +2,15 @@
|
||||
title: '35. Search Insert Position'
|
||||
description: Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Binary Search", slug: "binary-search" }
|
||||
video: "K-RYzDZkzCI"
|
||||
sidebar:
|
||||
label: 'Search Insert Position'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
You must write an algorithm with O(log n) runtime complexity.
|
||||
::::
|
||||
@@ -66,7 +68,3 @@ class Solution:
|
||||
|
||||
return (l + r) // 2 + 1
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="K-RYzDZkzCI" title="Search Insert Position - Binary Search - Leetcode 35 - Python" />
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '704. Binary Search'
|
||||
description: Given an array of integers nums which is sorted in ascending order, and an integer target, write a function to search target in nums. If target exists, then return its index. Otherwise, return -1
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Binary Search", slug: "binary-search" }
|
||||
video: "s4DPM8ct1pI"
|
||||
sidebar:
|
||||
label: 'Binary Search'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
You must write an algorithm with O(log n) runtime complexity.
|
||||
::::
|
||||
@@ -71,7 +73,3 @@ var search = function(nums, target) {
|
||||
return -1;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="s4DPM8ct1pI" title="Binary Search - Leetcode 704 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '74. Search a 2D Matrix'
|
||||
description: 'You are given an m x n integer matrix matrix with the following two properties:'
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Binary Search", slug: "binary-search" }
|
||||
- { name: "Matrix", slug: "matrix" }
|
||||
video: "Ber2pi2C0j0"
|
||||
sidebar:
|
||||
label: 'Search a 2D Matrix'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
You must write a solution in O(log(m * n)) time complexity.
|
||||
::::
|
||||
@@ -86,7 +89,3 @@ class Solution:
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="Ber2pi2C0j0" title="Search a 2D Matrix - Leetcode 74 - Python" />
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '875. Koko Eating Bananas'
|
||||
description: Koko loves to eat bananas. There are n piles of bananas, the i^th pile has piles[i] bananas. The guards have gone and will come back in h hours
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Binary Search", slug: "binary-search" }
|
||||
video: "U2SozAs9RzA"
|
||||
sidebar:
|
||||
label: 'Koko Eating Bananas'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `piles = [3,6,7,11], h = 8`
|
||||
- Output: `4`
|
||||
@@ -77,7 +79,3 @@ var minEatingSpeed = function(piles, h) {
|
||||
return l;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="U2SozAs9RzA" title="Koko Eating Bananas - Binary Search - Leetcode 875 - Python" />
|
||||
@@ -1,3 +1,8 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Binary Search", order: 5, icon: "search", display: "group" });
|
||||
export default defineMeta({
|
||||
title: "Binary Search",
|
||||
order: 5,
|
||||
icon: "search",
|
||||
display: "group",
|
||||
});
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '1. Two Sum'
|
||||
description: You are given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
video: "KLlXCFG5TnA"
|
||||
sidebar:
|
||||
label: 'Two Sum'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
Can you come up with an algorithm that is less than O(n^2) time complexity?
|
||||
::::
|
||||
@@ -60,7 +62,3 @@ class Solution:
|
||||
seen[num] = i
|
||||
return []
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="KLlXCFG5TnA" title="Two Sum - Leetcode 1 - HashMap - Python" />
|
||||
+3
-2
@@ -2,13 +2,14 @@
|
||||
title: '1832. Check if the Sentence Is Pangram'
|
||||
description: A pangram is a sentence where every letter of the English alphabet appears at least once
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
sidebar:
|
||||
label: 'Check if the Sentence Is Pangram'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `sentence = "thequickbrownfoxjumpsoverthelazydog"`
|
||||
- Output: `true`
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '217. Contains Duplicate'
|
||||
description: Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
video: "3OamzN90kPg"
|
||||
sidebar:
|
||||
label: 'Contains Duplicate'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `nums = [1,2,3,1]`
|
||||
- Output: `true`
|
||||
@@ -48,7 +51,3 @@ class Solution:
|
||||
def containsDuplicate(self, nums: List[int]) -> bool:
|
||||
return len(set(nums)) != len(nums)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '242. Valid Anagram'
|
||||
description: Given two strings s and t, return true if t is an anagram of s, and false otherwise
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
video: "9UtInBqnCgA"
|
||||
sidebar:
|
||||
label: 'Valid Anagram'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
::::warning
|
||||
What if the inputs contain Unicode characters? How would you adapt your solution to such a case?
|
||||
::::
|
||||
@@ -60,7 +63,3 @@ var isAnagram = function(s, t) {
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="9UtInBqnCgA" title="Valid Anagram - Leetcode 242 - Python" />
|
||||
+10
-6
@@ -2,13 +2,21 @@
|
||||
title: '347. Top K Frequent Elements'
|
||||
description: Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
|
||||
- { name: "Bucket Sort", slug: "bucket-sort" }
|
||||
- { name: "Counting", slug: "counting" }
|
||||
- { name: "Quickselect", slug: "quickselect" }
|
||||
video: "YPTqKIgVk-k"
|
||||
sidebar:
|
||||
label: 'Top K Frequent Elements'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
Your algorithm's time complexity must be better than O(n log n), where n is the array's size.
|
||||
::::
|
||||
@@ -60,7 +68,3 @@ var topKFrequent = function (nums, k) {
|
||||
.slice(0, k);
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="YPTqKIgVk-k" title="Top K Frequent Elements - Bucket Sort - Leetcode 347 - Python" />
|
||||
+6
-6
@@ -2,13 +2,17 @@
|
||||
title: '387. First Unique Character in a String'
|
||||
description: Given a string s, find the first non-repeating character in it and return its index. If it does not exist, return -1
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Queue", slug: "queue" }
|
||||
- { name: "Counting", slug: "counting" }
|
||||
video: "rBENYgWy3xU"
|
||||
sidebar:
|
||||
label: 'First Unique Character in a String'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `s = "leetcode"`
|
||||
- Output: `0`
|
||||
@@ -58,7 +62,3 @@ var firstUniqChar = function (s) {
|
||||
return -1;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="rBENYgWy3xU" title="First Unique Character in a String - Leetcode 387 - Python" />
|
||||
+8
-6
@@ -2,13 +2,19 @@
|
||||
title: '451. Sort Characters By Frequency'
|
||||
description: Given a string s, sort it in decreasing order based on the frequency of the characters. The frequency of a character is the number of times it appears in the string
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
|
||||
- { name: "Bucket Sort", slug: "bucket-sort" }
|
||||
- { name: "Counting", slug: "counting" }
|
||||
video: "OXdXc9HTrIg"
|
||||
sidebar:
|
||||
label: 'Sort Characters By Frequency'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `s = "tree"`
|
||||
- Output: `"eert"`
|
||||
@@ -60,7 +66,3 @@ var frequencySort = function (s) {
|
||||
.join("");
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="OXdXc9HTrIg" title="Sort Characters By Frequency - Leetcode 451 - Python" />
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '49. Group Anagrams'
|
||||
description: Given an array of strings strs, group the anagrams together. You can return the answer in any order
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
video: "vzdNOK2oB2E"
|
||||
sidebar:
|
||||
label: 'Group Anagrams'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `strs = ["eat","tea","tan","ate","nat","bat"]`
|
||||
- Output: `[["bat"],["nat","tan"],["ate","eat","tea"]]`
|
||||
@@ -65,7 +69,3 @@ var groupAnagrams = function(strs) {
|
||||
return Object.values(anagrams);
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="vzdNOK2oB2E" title="Group Anagrams - Categorize Strings by Count - Leetcode 49" />
|
||||
@@ -1,3 +1,8 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Hash Table", order: 1, icon: "hash", display: "group" });
|
||||
export default defineMeta({
|
||||
title: "Hash Table",
|
||||
order: 1,
|
||||
icon: "hash",
|
||||
display: "group",
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 1: Linear Structures", order: 1 });
|
||||
export default defineMeta({ title: "1. Linear Structures", order: 1 });
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '739. Daily Temperatures'
|
||||
description: Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the i^th day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Stack", slug: "stack" }
|
||||
- { name: "Monotonic Stack", slug: "monotonic-stack" }
|
||||
video: "cTBiBSnjO3c"
|
||||
sidebar:
|
||||
label: 'Daily Temperatures'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `temperatures = [73,74,75,71,69,72,76,73]`
|
||||
- Output: `[1,1,4,2,1,1,0,0]`
|
||||
@@ -54,7 +57,3 @@ class Solution:
|
||||
stack.append(i)
|
||||
return ans
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="cTBiBSnjO3c" title="Daily Temperatures - Monotonic Stack - Leetcode 739 - Python" />
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '853. Car Fleet'
|
||||
description: There are n cars at given miles away from the starting mile 0, traveling to reach the mile target
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Stack", slug: "stack" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
- { name: "Monotonic Stack", slug: "monotonic-stack" }
|
||||
video: "Pr6T-3yB9RM"
|
||||
sidebar:
|
||||
label: 'Car Fleet'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]`
|
||||
- Output: `3`
|
||||
@@ -62,7 +66,3 @@ class Solution:
|
||||
|
||||
return len(stack)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="Pr6T-3yB9RM" title="Car Fleet - Leetcode 853 - Python" />
|
||||
+4
-6
@@ -2,13 +2,15 @@
|
||||
title: '238. Product of Array Except Self'
|
||||
description: Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i]
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Prefix Sum", slug: "prefix-sum" }
|
||||
video: "bNvIQI2wAjk"
|
||||
sidebar:
|
||||
label: 'Product of Array Except Self'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
You must write an algorithm that runs in O(n) time and without using the division operation.
|
||||
Can you solve the problem in O(1) extra space complexity? (The output array does not count as extra space for space complexity analysis.)
|
||||
@@ -63,7 +65,3 @@ var productExceptSelf = function(nums) {
|
||||
return answer;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="bNvIQI2wAjk" title="Product of Array Except Self - Leetcode 238 - Python" />
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '303. Range Sum Query - Immutable'
|
||||
description: Given an integer array nums, handle multiple queries of the following type
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Design", slug: "design" }
|
||||
- { name: "Prefix Sum", slug: "prefix-sum" }
|
||||
video: "2pndAmo_sMA"
|
||||
sidebar:
|
||||
label: 'Range Sum Query - Immutable'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `["NumArray", "sumRange", "sumRange", "sumRange"]` `[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]`
|
||||
- Output: `[null, 1, -1, -3]`
|
||||
@@ -66,7 +69,3 @@ class NumArray {
|
||||
* var param_1 = obj.sumRange(left,right)
|
||||
*/
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="2pndAmo_sMA" title="Range Sum Query Immutable - Leetcode 303 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '560. Subarray Sum Equals K'
|
||||
description: Given an array of integers nums and an integer k, return the total number of subarrays whose sum equals to k
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Prefix Sum", slug: "prefix-sum" }
|
||||
video: "fFVZt-6sgyo"
|
||||
sidebar:
|
||||
label: 'Subarray Sum Equals K'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `nums = [1,1,1], k = 2`
|
||||
- Output: `2`
|
||||
@@ -65,7 +68,3 @@ var subarraySum = function(nums, k) {
|
||||
return count
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="fFVZt-6sgyo" title="Subarray Sum Equals K - Prefix Sums - Leetcode 560 - Python" />
|
||||
+4
-6
@@ -2,13 +2,15 @@
|
||||
title: '121. Best Time to Buy and Sell Stock'
|
||||
description: You are given an array prices where prices[i] is the price of a given stock on the i^th day
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Dynamic Programming", slug: "dynamic-programming" }
|
||||
video: "1pkOgXD63yU"
|
||||
sidebar:
|
||||
label: 'Best Time to Buy and Sell Stock'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `prices = [7,1,5,3,6,4]`
|
||||
- Output: `5`
|
||||
@@ -55,7 +57,3 @@ class Solution:
|
||||
ans = max(ans, curr)
|
||||
return ans
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="1pkOgXD63yU" title="Sliding Window: Best Time to Buy and Sell Stock - Leetcode 121 - Python" />
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '3. Longest Substring Without Repeating Characters'
|
||||
description: Given a string s, find the length of the longest substring without duplicate characters
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Sliding Window", slug: "sliding-window" }
|
||||
video: "wiGpQwVHdE0"
|
||||
sidebar:
|
||||
label: 'Longest Substring Without Repeating Characters'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `s = "abcabcbb"`
|
||||
- Output: `3`
|
||||
@@ -61,7 +64,3 @@ class Solution:
|
||||
|
||||
return ans
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="wiGpQwVHdE0" title="Longest Substring Without Repeating Characters - Leetcode 3 - Python" />
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '424. Longest Repeating Character Replacement'
|
||||
description: You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Sliding Window", slug: "sliding-window" }
|
||||
video: "gqXU1UyA8pk"
|
||||
sidebar:
|
||||
label: 'Longest Repeating Character Replacement'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `s = "ABAB", k = 2`
|
||||
- Output: `4`
|
||||
@@ -61,7 +64,3 @@ class Solution:
|
||||
res = max(res, r - l + 1)
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="gqXU1UyA8pk" title="Longest Repeating Character Replacement - Leetcode 424 - Python" />
|
||||
+6
-6
@@ -2,13 +2,17 @@
|
||||
title: '567. Permutation in String'
|
||||
description: Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Sliding Window", slug: "sliding-window" }
|
||||
video: "UbyhOgBN834"
|
||||
sidebar:
|
||||
label: 'Permutation in String'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `s1 = "ab", s2 = "eidbaooo"`
|
||||
- Output: `true`
|
||||
@@ -68,7 +72,3 @@ class Solution:
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="UbyhOgBN834" title="Permutation in String - Leetcode 567 - Python" />
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '150. Evaluate Reverse Polish Notation'
|
||||
description: You are given an array of strings tokens that represents an arithmetic expression in a Reverse Polish Notation
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Math", slug: "math" }
|
||||
- { name: "Stack", slug: "stack" }
|
||||
video: "iu0082c4HDE"
|
||||
sidebar:
|
||||
label: 'Evaluate Reverse Polish Notation'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `tokens = ["2","1","+","3","*"]`
|
||||
- Output: `9`
|
||||
@@ -69,7 +72,3 @@ class Solution:
|
||||
|
||||
return stack[0]
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="iu0082c4HDE" title="Evaluate Reverse Polish Notation - Leetcode 150 - Python" />
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '155. Min Stack'
|
||||
description: Design a stack that supports push, pop, top, and retrieving the minimum element in constant time
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Stack", slug: "stack" }
|
||||
- { name: "Design", slug: "design" }
|
||||
video: "qkLl7nAwDPo"
|
||||
sidebar:
|
||||
label: 'Min Stack'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Stack</Badge>
|
||||
|
||||
::::warning
|
||||
You must implement a solution with O(1) time complexity for each function.
|
||||
::::
|
||||
@@ -77,7 +79,3 @@ class MinStack:
|
||||
# param_3 = obj.top()
|
||||
# param_4 = obj.getMin()
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="qkLl7nAwDPo" title="Design Min Stack - Amazon Interview Question - Leetcode 155 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '20. Valid Parentheses'
|
||||
description: Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Stack", slug: "stack" }
|
||||
- { name: "Bracket Sequences", slug: "bracket-sequences" }
|
||||
video: "WTzjTskDFMg"
|
||||
sidebar:
|
||||
label: 'Valid Parentheses'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">String</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `s = "()"`
|
||||
- Output: `true`
|
||||
@@ -71,7 +74,3 @@ class Solution:
|
||||
|
||||
return True if not stack else False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="WTzjTskDFMg" title="Valid Parentheses - Stack - Leetcode 20 - Python" />
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '11. Container With Most Water'
|
||||
description: You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the i^th line are (i, 0) and (i, height[i])
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "Greedy", slug: "greedy" }
|
||||
video: "UuiTKBwPgAo"
|
||||
sidebar:
|
||||
label: 'Container With Most Water'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
Notice that you may not slant the container.
|
||||
::::
|
||||
@@ -63,7 +66,3 @@ class Solution:
|
||||
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="UuiTKBwPgAo" title="Container with Most Water - Leetcode 11 - Python" />
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '125. Valid Palindrome'
|
||||
description: A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "String", slug: "string" }
|
||||
video: "jJXJ16kPFWg"
|
||||
sidebar:
|
||||
label: 'Valid Palindrome'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Two Pointers</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `s = "A man, a plan, a canal: Panama"`
|
||||
- Output: `true`
|
||||
@@ -64,7 +66,3 @@ var isPalindrome = function(s) {
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="jJXJ16kPFWg" title="Valid Palindrome - Leetcode 125 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '15. 3Sum'
|
||||
description: Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
video: "jzZsG8n2R9A"
|
||||
sidebar:
|
||||
label: '3Sum'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
Notice that the solution set must not contain duplicate triplets.
|
||||
::::
|
||||
@@ -99,7 +102,3 @@ class Solution:
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="jzZsG8n2R9A" title="3Sum - Leetcode 15 - Python" />
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '167. Two Sum II - Input Array Is Sorted'
|
||||
description: Given a 1-indexed array of integers numbers that is already sorted in non-decreasing order, find two numbers such that they add up to a specific target number. Let these two numbers be numbers[index1] and numbers[index2] where 1 <= index1 < index2 <= numbers.length
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "Binary Search", slug: "binary-search" }
|
||||
video: "cQ1Oz4ckceM"
|
||||
sidebar:
|
||||
label: 'Two Sum II - Input Array Is Sorted'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
Your solution must use only constant extra space.
|
||||
::::
|
||||
@@ -70,7 +73,3 @@ class Solution:
|
||||
j-=1
|
||||
return []
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="cQ1Oz4ckceM" title="TWO SUM II - Amazon Coding Interview Question - Leetcode 167 - Python" />
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '344. Reverse String'
|
||||
description: Write a function that reverses a string. The input string is given as an array of characters s
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "String", slug: "string" }
|
||||
video: "_d0T_2Lk2qA"
|
||||
sidebar:
|
||||
label: 'Reverse String'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Two Pointers</Badge>
|
||||
|
||||
:::warning
|
||||
You must do this by modifying the input array in-place with O(1) extra memory.
|
||||
:::
|
||||
@@ -56,7 +58,3 @@ var reverseString = function(s) {
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="_d0T_2Lk2qA" title="Reverse String - 3 Ways - Leetcode 344 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '392. Is Subsequence'
|
||||
description: Given two strings s and t, return true if s is a subsequence of t, or false otherwise
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Dynamic Programming", slug: "dynamic-programming" }
|
||||
video: "99RVfqklbCE"
|
||||
sidebar:
|
||||
label: 'Is Subsequence'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Two Pointers</Badge>
|
||||
|
||||
::::warning
|
||||
Suppose there are lots of incoming s, say s1, s2, ..., sk where k >= 10^9, and you want to check one by one to see if t has its subsequence. In this scenario, how would you change your code?
|
||||
::::
|
||||
@@ -59,7 +62,3 @@ class Solution:
|
||||
|
||||
return i == len(s)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="99RVfqklbCE" title="Is Subsequence - Leetcode 392" />
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '977. Squares of a Sorted Array'
|
||||
description: Given an integer array nums sorted in non-decreasing order, return an array of the squares of each number sorted in non-decreasing order
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
video: "FPCZsG_AkUg"
|
||||
sidebar:
|
||||
label: 'Squares of a Sorted Array'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
Squaring each element and sorting the new array is very trivial, could you find an O(n) solution using a different approach?
|
||||
::::
|
||||
@@ -69,7 +72,3 @@ var sortedSquares = function(nums) {
|
||||
return ans;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="FPCZsG_AkUg" title="Squares of a Sorted Array - Leetcode 977 - Python" />
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '146. LRU Cache'
|
||||
description: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Linked List", slug: "linked-list" }
|
||||
- { name: "Design", slug: "design" }
|
||||
- { name: "Doubly-Linked List", slug: "doubly-linked-list" }
|
||||
video: "7ABFKPK2hD4"
|
||||
sidebar:
|
||||
label: 'LRU Cache'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: ``
|
||||
- Output: ``
|
||||
@@ -110,7 +114,3 @@ class LRUCache:
|
||||
# param_1 = obj.get(key)
|
||||
# obj.put(key,value)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="7ABFKPK2hD4" title="LRU Cache - Twitch Interview Question - Leetcode 146" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '2. Add Two Numbers'
|
||||
description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Linked List", slug: "linked-list" }
|
||||
- { name: "Math", slug: "math" }
|
||||
- { name: "Recursion", slug: "recursion" }
|
||||
video: "wgFPrzTjm7s"
|
||||
sidebar:
|
||||
label: 'Add Two Numbers'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Linked List</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `l1 = [2,4,3], l2 = [5,6,4]`
|
||||
- Output: `[7,0,8]`
|
||||
@@ -70,7 +73,3 @@ class Solution:
|
||||
|
||||
return dummy.next
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="wgFPrzTjm7s" title="Add Two Numbers - Leetcode 2 - Python" />
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '141. Linked List Cycle'
|
||||
description: Given head, the head of a linked list, determine if the linked list has a cycle in it
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Linked List", slug: "linked-list" }
|
||||
- { name: "Two Pointers", slug: "two-pointers" }
|
||||
- { name: "Floyd's Cycle Finding Algorithm", slug: "floyds-cycle-finding-algorithm" }
|
||||
video: "gBTe7lFR3vc"
|
||||
sidebar:
|
||||
label: 'Linked List Cycle'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
::::warning
|
||||
Can you solve it using O(1) (i.e. constant) memory?
|
||||
::::
|
||||
@@ -69,7 +73,3 @@ class Solution:
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="gBTe7lFR3vc" title="Linked List Cycle - Floyd's Tortoise and Hare - Leetcode 141 - Python" />
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '206. Reverse Linked List'
|
||||
description: Given the head of a singly linked list, reverse the list, and return the reversed list
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Linked List", slug: "linked-list" }
|
||||
- { name: "Recursion", slug: "recursion" }
|
||||
video: "G0_I-ZF0S38"
|
||||
sidebar:
|
||||
label: 'Reverse Linked List'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Linked List</Badge>
|
||||
|
||||
::::warning
|
||||
A linked list can be reversed either iteratively or recursively. Could you implement both?
|
||||
::::
|
||||
@@ -62,7 +64,3 @@ class Solution:
|
||||
curr = next_
|
||||
return prev
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="G0_I-ZF0S38" title="Reverse Linked List - Iterative AND Recursive - Leetcode 206 - Python" />
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '21. Merge Two Sorted Lists'
|
||||
description: You are given the heads of two sorted linked lists list1 and list2
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Linked List", slug: "linked-list" }
|
||||
- { name: "Recursion", slug: "recursion" }
|
||||
video: "XIdigk956u0"
|
||||
sidebar:
|
||||
label: 'Merge Two Sorted Lists'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Linked List</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `list1 = [1,2,4], list2 = [1,3,4]`
|
||||
- Output: `[1,1,2,3,4,4]`
|
||||
@@ -82,7 +84,3 @@ class Solution:
|
||||
current.next = list1 or list2
|
||||
return head
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="XIdigk956u0" title="Merge Two Sorted Lists - Leetcode 21 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '48. Rotate Image'
|
||||
description: You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise)
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Math", slug: "math" }
|
||||
- { name: "Matrix", slug: "matrix" }
|
||||
video: "fMSJSS7eO1w"
|
||||
sidebar:
|
||||
label: 'Rotate Image'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
|
||||
- Output: `[[7,4,1],[8,5,2],[9,6,3]]`
|
||||
@@ -52,7 +55,3 @@ class Solution:
|
||||
for r in range(n):
|
||||
matrix[r].reverse()
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="fMSJSS7eO1w" title="Rotate Image - Matrix - Leetcode 48" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '54. Spiral Matrix'
|
||||
description: Given an m x n matrix, return all elements of the matrix in spiral order
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Matrix", slug: "matrix" }
|
||||
- { name: "Simulation", slug: "simulation" }
|
||||
video: "BJnMZNwUk1M"
|
||||
sidebar:
|
||||
label: 'Spiral Matrix'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
|
||||
- Output: `[1,2,3,6,9,8,7,4,5]`
|
||||
@@ -76,7 +79,3 @@ class Solution:
|
||||
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="BJnMZNwUk1M" title="Spiral Matrix - Microsoft Interview Question - Leetcode 54" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '73. Set Matrix Zeroes'
|
||||
description: Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0's
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Matrix", slug: "matrix" }
|
||||
video: "T41rL0L3Pnw"
|
||||
sidebar:
|
||||
label: 'Set Matrix Zeroes'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
::::warning
|
||||
You must do it in place.
|
||||
::::
|
||||
@@ -95,7 +98,3 @@ class Solution:
|
||||
for r in range(m):
|
||||
matrix[r][0] = 0
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="T41rL0L3Pnw" title="Set Matrix Zeroes - In-place - Leetcode 73" />
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 2: Nodal & Grid", order: 2 });
|
||||
export default defineMeta({ title: "2. Nodal & Grid", order: 2 });
|
||||
+6
-6
@@ -2,13 +2,17 @@
|
||||
title: '230. Kth Smallest Element in a BST'
|
||||
description: Given the root of a binary search tree, and an integer k, return the k^th smallest value (1-indexed) of all the values of the nodes in the tree
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Binary Search Tree", slug: "binary-search-tree" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
video: "5LUXSvjmGCw"
|
||||
sidebar:
|
||||
label: 'Kth Smallest Element in a BST'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
::::warning
|
||||
If the BST is modified often (i.e., we can do insert and delete operations) and you need to find the kth smallest frequently, how would you optimize?
|
||||
::::
|
||||
@@ -70,7 +74,3 @@ class Solution:
|
||||
return cur.val
|
||||
cur = cur.right
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="5LUXSvjmGCw" title="Kth Smallest Element in a BST - Leetcode 230 - Python" />
|
||||
+8
-6
@@ -2,13 +2,19 @@
|
||||
title: '235. Lowest Common Ancestor of a Binary Search Tree'
|
||||
description: Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Binary Search Tree", slug: "binary-search-tree" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
- { name: "Binary Lifting", slug: "binary-lifting" }
|
||||
- { name: "Lowest Common Ancestor", slug: "lowest-common-ancestor" }
|
||||
video: "gs2LMfuOR9k"
|
||||
sidebar:
|
||||
label: 'Lowest Common Ancestor of a Binary Search Tree'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8`
|
||||
- Output: `6`
|
||||
@@ -71,7 +77,3 @@ class Solution:
|
||||
else:
|
||||
return cur
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="gs2LMfuOR9k" title="Lowest Common Ancestor of a Binary Search Tree - Leetcode 235 - Python" />
|
||||
+6
-6
@@ -2,13 +2,17 @@
|
||||
title: '98. Validate Binary Search Tree'
|
||||
description: Given the root of a binary tree, determine if it is a valid binary search tree (BST)
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Binary Search Tree", slug: "binary-search-tree" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
video: "s6ATEkipzow"
|
||||
sidebar:
|
||||
label: 'Validate Binary Search Tree'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [2,1,3]`
|
||||
- Output: `true`
|
||||
@@ -62,7 +66,3 @@ class Solution:
|
||||
|
||||
return valid(root, float("-inf"), float("inf"))
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="s6ATEkipzow" title="Validate Binary Search Tree - Depth First Search - Leetcode 98" />
|
||||
+7
-6
@@ -2,13 +2,18 @@
|
||||
title: '215. Kth Largest Element in an Array'
|
||||
description: Given an integer array nums and an integer k, return the k^th largest element in the array
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
|
||||
- { name: "Quickselect", slug: "quickselect" }
|
||||
video: "XEmy13g1Qxc"
|
||||
sidebar:
|
||||
label: 'Kth Largest Element in an Array'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `nums = [3,2,1,5,6,4], k = 2`
|
||||
- Output: `5`
|
||||
@@ -54,7 +59,3 @@ class Solution:
|
||||
|
||||
return -heapq.heappop(nums)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="XEmy13g1Qxc" title="Kth Largest Element in an Array - Quick Select - Leetcode 215 - Python" />
|
||||
@@ -2,13 +2,18 @@
|
||||
title: '23. Merge k Sorted Lists'
|
||||
description: You are given an array of k linked-lists lists, each linked-list is sorted in ascending order
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Linked List", slug: "linked-list" }
|
||||
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
|
||||
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
|
||||
- { name: "Merge Sort", slug: "merge-sort" }
|
||||
- { name: "Tournament Sort", slug: "tournament-sort" }
|
||||
video: "q5a5OiGbT6Q"
|
||||
sidebar:
|
||||
label: 'Merge k Sorted Lists'
|
||||
badge: 'Hard'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Linked List</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `lists = [[1,4,5],[1,3,4],[2,6]]`
|
||||
- Output: `[1,1,2,3,4,4,5,6]`
|
||||
@@ -89,7 +94,3 @@ class Solution:
|
||||
# Space: O(n)
|
||||
return D.next
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="q5a5OiGbT6Q" title="Merge K Sorted Lists - Leetcode 23 - Python" />
|
||||
+10
-6
@@ -2,13 +2,21 @@
|
||||
title: '973. K Closest Points to Origin'
|
||||
description: Given an array of points where points[i] = [xi, yi] represents a point on the X-Y plane and an integer k, return the k closest points to the origin (0, 0)
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Math", slug: "math" }
|
||||
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
|
||||
- { name: "Geometry", slug: "geometry" }
|
||||
- { name: "Sorting", slug: "sorting" }
|
||||
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
|
||||
- { name: "Quickselect", slug: "quickselect" }
|
||||
- { name: "K-D Tree", slug: "k-d-tree" }
|
||||
video: "rI2EBUEMfTk"
|
||||
sidebar:
|
||||
label: 'K Closest Points to Origin'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `points = [[1,3],[-2,2]], k = 1`
|
||||
- Output: `[[-2,2]]`
|
||||
@@ -64,7 +72,3 @@ class Solution:
|
||||
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="rI2EBUEMfTk" title="K Closest Points to Origin - Heap / Priority Queue - Leetcode 973 - Python" />
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 3: Hierarchical", order: 3 });
|
||||
export default defineMeta({ title: "3. Hierarchical", order: 3 });
|
||||
+5
-6
@@ -2,13 +2,16 @@
|
||||
title: '102. Binary Tree Level Order Traversal'
|
||||
description: Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level)
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
video: "6ZnyEApgFYg"
|
||||
sidebar:
|
||||
label: 'Binary Tree Level Order Traversal'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [3,9,20,null,null,15,7]`
|
||||
- Output: `[[3],[9,20],[15,7]]`
|
||||
@@ -80,7 +83,3 @@ class Solution:
|
||||
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="6ZnyEApgFYg" title="Binary Tree Level Order Traversal - BFS - Leetcode 102" />
|
||||
+6
-6
@@ -2,13 +2,17 @@
|
||||
title: '1448. Count Good Nodes in Binary Tree'
|
||||
description: Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
video: "7cp5imvDzl4"
|
||||
sidebar:
|
||||
label: 'Count Good Nodes in Binary Tree'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [3,1,4,3,null,1,5]`
|
||||
- Output: `4`
|
||||
@@ -67,7 +71,3 @@ class Solution:
|
||||
|
||||
return dfs(root, root.val)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="7cp5imvDzl4" title="Microsoft's Most Asked Question 2021 - Count Good Nodes in a Binary Tree - Leetcode 1448 - Python" />
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '199. Binary Tree Right Side View'
|
||||
description: Given the root of a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
video: "d4zLyf32e3I"
|
||||
sidebar:
|
||||
label: 'Binary Tree Right Side View'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [1,2,3,null,5,null,4]`
|
||||
- Output: `[1,3,4]`
|
||||
@@ -83,7 +87,3 @@ class Solution:
|
||||
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="d4zLyf32e3I" title="Binary Tree Right Side View - Breadth First Search - Leetcode 199" />
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '104. Maximum Depth of Binary Tree'
|
||||
description: Given the root of a binary tree, return its maximum depth
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
video: "hTM3phVI6YQ"
|
||||
sidebar:
|
||||
label: 'Maximum Depth of Binary Tree'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [3,9,20,null,null,15,7]`
|
||||
- Output: `3`
|
||||
@@ -48,7 +52,3 @@ class Solution:
|
||||
|
||||
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="hTM3phVI6YQ" title="Maximum Depth of Binary Tree - 3 Solutions - Leetcode 104 - Python" />
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '226. Invert Binary Tree'
|
||||
description: Given the root of a binary tree, invert the tree, and return its root
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
video: "OnSn2XEQ4MY"
|
||||
sidebar:
|
||||
label: 'Invert Binary Tree'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [4,2,7,1,3,6,9]`
|
||||
- Output: `[4,7,2,9,6,3,1]`
|
||||
@@ -59,7 +63,3 @@ class Solution:
|
||||
|
||||
return root
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="OnSn2XEQ4MY" title="Invert Binary Tree - Depth First Search - Leetcode 226" />
|
||||
+6
-2
@@ -2,13 +2,17 @@
|
||||
title: '236. Lowest Common Ancestor of a Binary Tree'
|
||||
description: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Tree", slug: "tree" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Binary Tree", slug: "binary-tree" }
|
||||
- { name: "Binary Lifting", slug: "binary-lifting" }
|
||||
- { name: "Lowest Common Ancestor", slug: "lowest-common-ancestor" }
|
||||
sidebar:
|
||||
label: 'Lowest Common Ancestor of a Binary Tree'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Tree</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1`
|
||||
- Output: `3`
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '208. Implement Trie (Prefix Tree)'
|
||||
description: A trie (pronounced as "try") or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Design", slug: "design" }
|
||||
- { name: "Trie", slug: "trie" }
|
||||
video: "oobqoCJlHA0"
|
||||
sidebar:
|
||||
label: 'Implement Trie (Prefix Tree)'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: ``
|
||||
- Output: ``
|
||||
@@ -107,7 +111,3 @@ class Trie:
|
||||
# param_2 = obj.search(word)
|
||||
# param_3 = obj.startsWith(prefix)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="oobqoCJlHA0" title="Implement Trie (Prefix Tree) - Leetcode 208" />
|
||||
+6
-6
@@ -2,13 +2,17 @@
|
||||
title: '211. Design Add and Search Words Data Structure'
|
||||
description: Design a data structure that supports adding new words and finding if a string matches any previously added string
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Design", slug: "design" }
|
||||
- { name: "Trie", slug: "trie" }
|
||||
video: "BTf05gs_8iU"
|
||||
sidebar:
|
||||
label: 'Design Add and Search Words Data Structure'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">String</Badge>
|
||||
|
||||
### Constraints:
|
||||
|
||||
- `1 <= word.length <= 25`
|
||||
@@ -101,7 +105,3 @@ class WordDictionary:
|
||||
# obj.addWord(word)
|
||||
# param_3 = obj.search(word)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="BTf05gs_8iU" title="Design Add and Search Words Data Structure - Leetcode 211 - Python" />
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '127. Word Ladder'
|
||||
description: 'A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that:'
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "String", slug: "string" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Bidirectional Search", slug: "bidirectional-search" }
|
||||
sidebar:
|
||||
label: 'Word Ladder'
|
||||
badge: 'Hard'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]`
|
||||
- Output: `5`
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '994. Rotting Oranges'
|
||||
description: 'You are given an m x n grid where each cell can have one of three values:'
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Matrix", slug: "matrix" }
|
||||
sidebar:
|
||||
label: 'Rotting Oranges'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `grid = [[2,1,1],[1,1,0],[0,1,1]]`
|
||||
- Output: `4`
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '133. Clone Graph'
|
||||
description: Given a reference of a node in a connected undirected graph
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Hash Table", slug: "hash-table" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Graph Theory", slug: "graph" }
|
||||
sidebar:
|
||||
label: 'Clone Graph'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hash Table</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `adjList = [[2,4],[1,3],[2,4],[1,3]]`
|
||||
- Output: `[[2,4],[1,3],[2,4],[1,3]]`
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '200. Number of Islands'
|
||||
description: Given an m x n 2D binary grid grid which represents a map of '1's (land) and '0's (water), return the number of islands
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Union-Find", slug: "union-find" }
|
||||
- { name: "Matrix", slug: "matrix" }
|
||||
sidebar:
|
||||
label: 'Number of Islands'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ]`
|
||||
- Output: `1`
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '417. Pacific Atlantic Water Flow'
|
||||
description: There is an m x n rectangular island that borders both the Pacific Ocean and Atlantic Ocean. The Pacific Ocean touches the island's left and top edges, and the Atlantic Ocean touches the island's right and bottom edges
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Matrix", slug: "matrix" }
|
||||
sidebar:
|
||||
label: 'Pacific Atlantic Water Flow'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]`
|
||||
- Output: `[[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]`
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 4: Relational", order: 4 });
|
||||
export default defineMeta({ title: "4. Relational", order: 4 });
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '207. Course Schedule'
|
||||
description: There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Graph Theory", slug: "graph" }
|
||||
- { name: "Topological Sort", slug: "topological-sort" }
|
||||
- { name: "Directed Acyclic Graph", slug: "directed-acyclic-graph" }
|
||||
sidebar:
|
||||
label: 'Course Schedule'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Depth-First Search</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `numCourses = 2, prerequisites = [[1,0]]`
|
||||
- Output: `true`
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '210. Course Schedule II'
|
||||
description: There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Graph Theory", slug: "graph" }
|
||||
- { name: "Topological Sort", slug: "topological-sort" }
|
||||
sidebar:
|
||||
label: 'Course Schedule II'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Depth-First Search</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `numCourses = 2, prerequisites = [[1,0]]`
|
||||
- Output: `[0,1]`
|
||||
|
||||
+5
-2
@@ -2,13 +2,16 @@
|
||||
title: '323. Number of Connected Components in an Undirected Graph'
|
||||
description: You have a graph of n nodes. You are given an integer n and an array edges where edges[i] = [ai, bi] indicates that there is an edge between ai and bi in the graph
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Union-Find", slug: "union-find" }
|
||||
- { name: "Graph Theory", slug: "graph" }
|
||||
sidebar:
|
||||
label: 'Number of Connected Components in an Undirected Graph'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Depth-First Search</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `n = 5, edges = [[0,1],[1,2],[3,4]]`
|
||||
- Output: `2`
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
title: '684. Redundant Connection'
|
||||
description: In this problem, a tree is an undirected graph that is connected and has no cycles
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Depth-First Search", slug: "depth-first-search" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Union-Find", slug: "union-find" }
|
||||
- { name: "Graph Theory", slug: "graph" }
|
||||
sidebar:
|
||||
label: 'Redundant Connection'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Depth-First Search</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `edges = [[1,2],[1,3],[2,3]]`
|
||||
- Output: `[2,3]`
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
title: '198. House Robber'
|
||||
description: You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Dynamic Programming", slug: "dynamic-programming" }
|
||||
sidebar:
|
||||
label: 'House Robber'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `nums = [1,2,3,1]`
|
||||
- Output: `4`
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
title: '322. Coin Change'
|
||||
description: You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Dynamic Programming", slug: "dynamic-programming" }
|
||||
- { name: "Breadth-First Search", slug: "breadth-first-search" }
|
||||
- { name: "Knapsack Problem", slug: "knapsack-problem" }
|
||||
- { name: "Complete Knapsack", slug: "complete-knapsack" }
|
||||
sidebar:
|
||||
label: 'Coin Change'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `coins = [1,2,5], amount = 11`
|
||||
- Output: `3`
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
title: '70. Climbing Stairs'
|
||||
description: You are climbing a staircase. It takes n steps to reach the top
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Math", slug: "math" }
|
||||
- { name: "Dynamic Programming", slug: "dynamic-programming" }
|
||||
- { name: "Memoization", slug: "memoization" }
|
||||
sidebar:
|
||||
label: 'Climbing Stairs'
|
||||
badge: 'Easy'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Math</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `n = 2`
|
||||
- Output: `2`
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
title: '39. Combination Sum'
|
||||
description: Given an array of distinct integers candidates and a target integer target, return a list of all unique combinations of candidates where the chosen numbers sum to target. You may return the combinations in any order
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Backtracking", slug: "backtracking" }
|
||||
sidebar:
|
||||
label: 'Combination Sum'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Array</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: `candidates = [2,3,6,7], target = 7`
|
||||
- Output: `[[2,2,3],[7]]`
|
||||
|
||||
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user