Compare commits

...
Author SHA1 Message Date
Prad Nukala cd23c619df build(docs): generate Blume site artifacts 2026-10-17 20:38:18 -04:00
Prad Nukala 7f8461e692 build(docs): add Astro and content collection configuration 2026-10-17 20:23:40 -04:00
Prad Nukala fadfa7ff9f build(deps): migrate docs scripts and add Astro dependencies 2026-10-17 20:20:34 -04:00
Prad Nukala 83cb284e87 docs(agents): fix bullet formatting for sync script entry 2026-10-14 16:03:38 -04:00
Prad Nukala f33ded37ec docs(appendix): unify terms and companies entries, add LeetCode links, refine description and navigation rendering 2026-10-14 16:03:07 -04:00
Prad Nukala ddf5dc483c docs(agents): document companies field in sync script 2026-10-14 15:52:26 -04:00
Prad Nukala e3d915513d feat(work): add Partition List solution stub 2026-10-14 15:52:06 -04:00
Prad Nukala f6c580b14b docs: add companies frontmatter, schema, and rendering in badges & appendix pages 2026-10-14 15:52:02 -04:00
Prad Nukala c753937852 feat(cli): add company frequency frontmatter to docs sync 2026-10-14 15:51:56 -04:00
Prad Nukala 8e07899157 docs(cli): clarify test and submit command behavior for uncommitted changes 2026-10-14 11:04:22 -04:00
Prad Nukala 886024c4f0 feat(work): implement reorder list solution for problem 143 2026-10-14 11:04:19 -04:00
Prad Nukala 37a2d39269 docs(docs): add 143 Reorder List documentation 2026-10-14 11:04:16 -04:00
Prad Nukala a7ab989d57 feat(cli): prioritize uncommitted solutions in picker ordering 2026-10-14 11:04:13 -04:00
Prad Nukala 0ccd9794f0 docs(docs): delete Reorder List documentation 2026-10-14 10:48:34 -04:00
Prad Nukala b922205bd0 feat(work): add Reorder List solution 2026-10-14 10:47:47 -04:00
Prad Nukala def494d524 docs(docs): add Reorder List page, rename phase titles, update redirects 2026-10-14 10:47:44 -04:00
Prad Nukala 0a4b8d8b49 feat(work): add LeetCode solutions for problems 380, 82, 92 2026-10-13 16:51:41 -04:00
Prad Nukala f1a952f2bd docs: add new problem documentation pages and update redirects 2026-10-13 16:51:38 -04:00
Prad Nukala ea44a99c40 feat(work): add solution for LeetCode 36 Valid Sudoku 2026-10-13 15:34:15 -04:00
Prad Nukala 00fa322a14 feat(work): add solution for LeetCode 19 Remove Nth Node From End of List 2026-10-13 15:14:50 -04:00
Prad Nukala bf56e170a4 chore: remove sd 2026-10-13 14:29:10 -04:00
Prad Nukala 4cf976f683 feat(work): add solution for LeetCode 876 Middle of the Linked List 2026-10-13 14:25:36 -04:00
Prad Nukala ace1944786 docs: clarify /progress page header tab and fix apps/docs table formatting 2026-10-13 09:16:41 -04:00
Prad Nukala 13734e2e27 docs: add /progress page with live stats cards and update navigation and component imports 2026-10-13 09:16:30 -04:00
Prad Nukala cb4a03b6fa feat: integrate system design 2026-10-07 14:18:14 -04:00
Prad Nukala e14432d32a docs(docs): improve system design guide and simplify custom page tabs 2026-10-07 14:17:38 -04:00
Prad Nukala 8843cb776c docs(AGENTS): fix apps/docs table row formatting 2026-10-07 13:45:26 -04:00
Prad Nukala 0e58b63d4d docs(docs): add header tabs for Changelog and Appendix pages 2026-10-07 13:45:21 -04:00
Prad Nukala b5c3f1a159 docs(readme): fix formatting of apps/docs entry in AGENTS.md 2026-10-07 13:00:01 -04:00
Prad Nukala 37d3d9ceaf feat(work): add Minimum Window Substring & Basic Calculator solutions and implement StockSpanner algorithm 2026-10-07 12:59:02 -04:00
Prad Nukala 5627d0ca64 docs(docs): add new problem documentation pages and source selector component 2026-10-07 12:58:59 -04:00
Prad Nukala 598688977a feat(work): add 901 online stock span solution and implement removeDuplicates algorithm 2026-10-06 13:47:23 -04:00
Prad Nukala a3e846c606 docs(docs): add documentation for Next Greater Element I and Remove Duplicates from Sorted Array II, update redirects 2026-10-06 13:47:20 -04:00
Prad Nukala 52d9654ade feat(array): add solution for problem 80 remove duplicates from sorted array ii 2026-10-06 13:16:50 -04:00
Prad Nukala f202c3a0a1 docs(docs): add two‑pointer guide with decision tree and illustrations 2026-10-06 13:16:45 -04:00
Prad Nukala 030887c7df docs(docs): add detailed stack overview with operation examples and SVG illustrations 2026-10-06 12:58:52 -04:00
Prad Nukala 733c71d364 feat(work): add solution for Next Greater Element I 2026-10-06 12:53:56 -04:00
Prad Nukala 115b901fbe docs(docs): add YouTube video and detailed monotonic stack explanation with Python example 2026-10-06 12:53:26 -04:00
Prad Nukala ceaa9a2e75 feat(work): implement binary search solution for median of two sorted arrays 2026-10-05 15:47:05 -04:00
Prad Nukala 7c7019ec0c docs: restructure problem paths, change section display to groups, and improve documentation content 2026-10-05 15:47:02 -04:00
Prad Nukala cd5f416939 style(cli): reformat sync script for readability and consistency 2026-10-05 15:46:55 -04:00
Prad Nukala 6247915ae5 feat: refactored docs structure 2026-10-05 14:22:28 -04:00
Prad Nukala 31a1fcfdbd docs(docs): add MDX pages for Median of Two Sorted Arrays and Largest Rectangle in Histogram and update site redirects 2026-10-05 14:01:30 -04:00
296 changed files with 18753 additions and 917 deletions

No files matched your search

+7 -9
View File
@@ -1,11 +1,11 @@
---
name: problem-approach
description: Write the `## Approach` section — a Mermaid flowchart that traces the submitted solution — for every docs solution page under docs/ that lacks one, using the repo's exact placement and node conventions. Use when asked to add/implement/fill in missing approaches, draw the approach diagram for a problem page, or when `/problem-approach` is run.
description: Write the `## Approach` section — a Mermaid flowchart that traces the submitted solution — for every docs solution page under apps/docs/content/ that lacks one, using the repo's exact placement and node conventions. Use when asked to add/implement/fill in missing approaches, draw the approach diagram for a problem page, or when `/problem-approach` is run.
---
# Problem approach diagrams
Every solution page (`docs/(<phase>)/<topic>/(problems)/<num>_<slug>.mdx`) has a
Every solution page (`apps/docs/content/(<phase>)/<topic>/(problems)/<num>_<slug>.mdx`) has a
hand-owned section before the script-owned one:
```
@@ -23,7 +23,7 @@ next `work/` push.
1. **Find the pages** (numbered pages with no `## Approach` heading):
```sh
grep -rL '^## Approach' docs --include='[0-9]*_*.mdx'
grep -L '^## Approach' apps/docs/content/*/*.mdx
```
2. **For each page**, read `## Solution` and write the diagram from *that code* — the
@@ -47,13 +47,11 @@ next `work/` push.
````
4. **Check** each diagram by hand against the syntax rules below (the docs build does
not parse Mermaid — a broken diagram fails only in the browser). The site is built
from prdlk/cs, which reads `docs/` over HTTP, so preview unpushed pages there
against this checkout (its `README` names the local-content switch), then build
once at the end:
not parse Mermaid — a broken diagram fails only in the browser), then run the
docs build once at the end and review the pages in `bun run dev`:
```sh
cd ../cs && bunx blume build --isolated && rm -rf .blume-verify
cd apps/docs && bunx blume build --isolated && rm -rf .blume-verify
```
Pages with two languages (`<CodeGroup>`) get **one** diagram, traced from the
@@ -61,7 +59,7 @@ Python version. Pages with only JavaScript trace the JavaScript.
## Diagram conventions
Reference page: `docs/(01-linear-structures)/two-pointers/(problems)/15_3sum.mdx`. Every existing diagram
Reference page: `apps/docs/content/(array)/15-3sum.mdx`. Every existing diagram
follows this shape:
```mermaid
+38 -4
View File
@@ -5,19 +5,53 @@ on:
branches: [main]
workflow_dispatch:
# The Worker only. The docs site is built and published from prdlk/cs, which
# reads docs/ from this repo over HTTP (Blume's mdxRemote()), so this repo has
# no Pages build.
permissions:
contents: read
pages: write
id-token: write
# The Worker deploy is idempotent (wrangler uploads the whole bundle), so on a
# burst of pushes only the last one matters: cancel-in-progress is correct.
# burst of pushes only the last one matters: cancel-in-progress is correct for
# both the Pages build and the Worker job.
concurrency:
group: deploy
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22 # Blume requires 22.12+
- uses: oven-sh/setup-bun@v2 # skip if you use npm/pnpm
- run: bun install --frozen-lockfile
# pages/changelog.astro reads this repo's GitHub releases at build time
# (apps/docs/lib/releases.ts). The token authenticates that fetch —
# required if the repo is private, and it lifts the shared runner's
# unauthenticated API rate limit. `contents: read` covers it.
- run: bun run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-pages-artifact@v3
with:
path: apps/docs/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
# 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),
+3
View File
@@ -37,6 +37,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
/CLAUDE.md
.blume/
/apps/docs/docs/.wrangler/state/v3
.blume-verify/
# api worker
apps/api/.dev.vars
+19 -17
View File
@@ -2,7 +2,7 @@
## Project Overview
LeetCode interview-prep campaign (2026-08-17 → Nov 3: eight weeks of topics, then the Review-issue phase; the end is the last date in `apps/api/data/schedule.json` — `CAMPAIGN_END`/`CAMPAIGN_DAYS` in `apps/api/src/srs.ts` are derived from it, never declared) run as a repo: solution files under `work/`, the docs content under `docs/` (MDX only — the Blume site that publishes it lives in [prdlk/cs](https://github.com/prdlk/cs) and reads `docs/` over HTTP), GitHub Actions that reconcile issues, and a Cloudflare Worker (`apps/api/`) that runs the spaced-repetition system (SRS) — daily digest email, one-tap logging, weekly review issues, live charts. Issues model the curriculum: ~24 `topic` issues (`Topic NN — Name`) own ~161 `problem` sub-issues titled `LC <num> · <Name>`, with difficulty and set carried **only** by labels (`set:core|optional|deferred`, `diff:*`) — never by the title; milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
LeetCode interview-prep campaign (2026-08-17 → Nov 3: eight weeks of topics, then the Review-issue phase; the end is the last date in `apps/api/data/schedule.json` — `CAMPAIGN_END`/`CAMPAIGN_DAYS` in `apps/api/src/srs.ts` are derived from it, never declared) run as a repo: solution files under `work/`, a Blume docs site under `apps/docs/`, GitHub Actions that reconcile issues and publish docs, and a Cloudflare Worker (`apps/api/`) that runs the spaced-repetition system (SRS) — daily digest email, one-tap logging, weekly review issues, live charts. Issues model the curriculum: ~24 `topic` issues (`Topic NN — Name`) own ~161 `problem` sub-issues titled `LC <num> · <Name>`, with difficulty and set carried **only** by labels (`set:core|optional|deferred`, `diff:*`) — never by the title; milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
## Architecture & Data Flow
@@ -10,8 +10,7 @@ Three owners, one direction of truth:
```mermaid
flowchart LR
work[work/ solutions] -->|bun run sync| docs[docs/*.mdx + docs/site.json]
docs -->|mdxRemote + site.json fetch| Site[prdlk/cs docs site]
work[work/ solutions] -->|bun run sync| docs[apps/docs/content/*.mdx]
work -->|close-solved.yml| PI[problem issues]
work -->|close-solved.yml POST /admin/solved| D1[(D1 = SRS state)]
PI -->|close-topics.yml| TI[topic + Review N issues]
@@ -39,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, then the problems closed on the finish day / earlier, with solution links at the tag — problems still open at the finish are left out). 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 site's `/changelog` (prdlk/cs `pages/changelog.astro` + `lib/releases.ts`) fetches this repo's releases at its build, dates each by its timever tag, not `published_at` (which the API cannot backdate), tags it `Topic` or `Review` from the release name, and lists the rows of its `### Closed …` tables — so the tag format and that table shape are its cross-repo contract with `render()`.
- **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, then the problems closed on the finish day / earlier, with solution links at the tag — problems still open at the finish are left out). 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 ONE custom page, `apps/docs/pages/changelog.astro` (a NeonUI-style timeline: date left, a card per release right, no per-release subpages, so nothing of it enters the sidebar or prev/next), linked from the header by a `navigation.tabs` entry (beside `/appendix`'s). `apps/docs/lib/releases.ts` fetches the releases at build (`GITHUB_TOKEN` in `deploy.yml`; a failed fetch fails the build), dates each by its timever tag, not `published_at` (which the API cannot backdate), tags it `Topic` or `Review` from the release name, and lists the rows of its `### Closed …` tables — so the tag format and that table shape are the page's contract with `render()`. There is no changelog RSS feed.
- **Review issues re-parent problems; topics still own them.** The transfer phase's 20 `Review <N> — <Phase>` issues (label `review`, milestone `Review — Transfer & Mocks`; not the Worker's `Review — Week N` gate issues) hold open optional/deferred problems as sub-issues in solve order, moved there with `replace_parent`. `apps/api/src/reviews.ts` is the ONE recovery of ownership — `readReviews()` (one GraphQL request per 50 `review` issues, cheap enough for the Worker's cron subrequest budget) reads each moved problem's topic off its last `parent_issue_removed` event naming a topic, `movedFromTopics()` groups them — shared by the Worker's catalog reconcile (a moved problem's D1 row keeps syncing its labels and `topic_issue`) and `apps/cli/topics.ts` (`readProblems()` returns a topic's sub-issues plus its moved problems, so the finished rule and existing release bodies do not change when a problem moves). A Review issue finishes by `completion()` in `topics.ts` — every sub-issue closed, any set — which `close-topics.ts` closes it on and `release-topics.ts` releases it on, exactly like a topic: timever of the last close, same `<!-- release-topics: #N -->` marker (issue numbers never collide), body fields from the review's `**Scheduled:**`/`**Phase:**`/`**Load:**` lines.
- **Review days are plan days.** `schedule.json` books each Review issue on its own date — Mon/Tue/Thu/Fri only, Oct 1 → Nov 3 — as `review: { issue, title, problems }` (solve order). The digest renders it as a linked "Today's review" section with the issue's day rules (`REVIEW_RULES` in `email.tsx`, one sentence for HTML and text), its problems join `RESERVED` so drills and sampled gates never draw them early, and `gate: []` skips a Saturday's checkpoint (Oct 3). The issue's sub-issues stay the truth: the catalog reconcile compares every booked review's title and problem list with GitHub and returns any mismatch as `reviewDrift` (also logged), so a re-arranged Review issue shows up the next sync instead of silently disagreeing with the email. Move a review by editing its `schedule.json` date AND the issue's `**Scheduled:**` line (the release body reads it).
@@ -49,10 +48,10 @@ 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 |
| `docs/` | The docs content, and nothing of the site: no `blume.config.ts`, no `meta.ts`, no components. prdlk/cs publishes it with Blume's `mdxRemote()` source, which fetches `.md`/`.mdx` files only. `docs/(0N-<phase>)/<topic>/(problems)/<num>_<slug>.mdx` is generated by sync; `(0N-<phase>)/<topic>/index.mdx` (`hidden: true`) is a topic's hand-written landing page, and `index.mdx`/`keywords.mdx`/`progress.mdx` sit at the root. 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, and `(problems)` is a second `(group)` inside it, so `tree-dfs/(problems)/226_invert-binary-tree.mdx` routes to `/tree-dfs/invert-binary-tree` (Blume sorts by the `<num>_` prefix and strips it from the route). `docs/site.json` is sync's generated manifest and the site's ONE other input: `folders` (path under `docs/` → `{ title, order, icon?, display? }`, exactly what a `meta.ts` in that folder would say — every topic `display: "page"`, so it is one sidebar row whose panel holds its landing page and a "Problems" section) and `redirects` (the `/<num>` short links). Images are root-absolute (`![](/topo_intro_1.png)`, `<CarouselImage src='/binary_search.001.png' />`) and served from the site's `public/` — a page-relative path cannot resolve once the MDX is fetched over HTTP. Frontmatter keys beyond Blume's (`topics:`, `video:`, `issue:`) and the MDX components pages use (`<CodeGroup>`, `<Carousel>`/`<CarouselImage>`, the islands) are the site's to declare — add a new one there before a page uses it |
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `pages/`, `components/` + `components.ts`, `islands/`, `public/`). `content/(0N-<phase>)/<topic>/(problems)/<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, and `(problems)` is a second `(group)` inside it, so `tree-dfs/(problems)/226_invert-binary-tree.mdx` routes to `/tree-dfs/invert-binary-tree` (Blume sorts by the `<num>_` prefix and strips it from the route). The `meta.ts` files carry the exact labels, phase/topic numbers and icons so the sidebar reads in curriculum order; every topic meta is `display: "page"`, so a topic is one sidebar row whose panel holds its landing page and a "Problems" section. 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 card on the Appendix glossary `/appendix#<slug>` (outlined; LeetCode's problem list if the tag has no term, 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 (a landing's: the topic `meta.ts` icon read off `data.navigation`; an Appendix term's: its `icon` in `appendix.ts`) and the tag's own colour from `lib/tag-colors.ts` — unique per tag site-wide, assigned greedily over every tag with a landing or a term 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 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. The sidebar is Blume's default nav tree (no `Sidebar` slot override). The custom pages `/progress`, `/changelog` and `/appendix` are header `navigation.tabs` (on mobile, Blume lists them at the top of the drawer); never `navigation.featured`, which Blume renders above the nav tree. 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 and the Appendix page (so badges, backlinks and term cards cannot disagree) and the map by sync. Every other tag a problem carries is ONE hand-written table, `apps/docs/appendix.ts` (`APPENDIX`: slug → `{ name, icon, category, definition }`), rendered by ONE custom page, `pages/appendix.astro` — a glossary with an A–Z letter index and a filter box (an `<appendix-filter>` custom element over static markup; no JS, the full list), one card per term (icon tinted the tag's colour, category, definition, LeetCode list link, and a `<details>` "Solved" list of the problem pages whose badge `tagTarget()` sends there, paged `PAGE_SIZE` (5) rows at a time by numbered buttons under it — a `<solved-pages>` custom element; without JS every row shows and the buttons stay hidden), each card `id`'d by slug and highlighted as `:target`. It is no content folder, so it enters neither the sidebar tree nor prev/next: its header tab is its link. Sync writes nothing for it — it warns about a carried tag with no term, a term whose tag a topic now teaches (delete it), and a term `icon` that is a `TOPIC_ICONS` value or another term's. The former per-term pages `/appendix/<slug>` are exact redirects to `/appendix#<slug>` in `blume.config.ts`, generated from `APPENDIX`'s keys. 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; `DependencySpine` — the README topic map as data, zoom/pan, nodes link to whatever route Blume's `/api/docs/navigaLine truncated
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (the plan: `date → { topics | problems | review | gate | sweep }`, human-edited, bundled at deploy), `migrations/` (append-only; `0002` is the +3/+7 ladder rebuild) |
| `.github/workflows/` | deploy (Worker → Cloudflare with the `OPENROUTER_API_KEY` Actions secret put into it first, on every main push; the docs site deploys from prdlk/cs), 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/` | `bun install` points `core.hooksPath` here (root `prepare` script, no husky). Its `pre-commit` hook (run `bun run sync`, stage only what sync changed — pages and `docs/site.json`) was removed in `e63d517`, so nothing runs on commit today: run `bun run sync` yourself before committing a solution, or its page and short link ship a commit later |
| `.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
@@ -69,14 +68,16 @@ bun run stats # terminal dashboard over GET /api/stats — four panes
# r refetches, q quits. Outside a TTY it prints every pane once.
# SRS_API points it at a local Worker like close-solved does.
bun run test # run ONE solution against LeetCode's judge (not a test suite;
# most recently tested first, stamped in the db on exit 0)
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
# hidden; last_submitted stamped in the db on exit 0)
bun run sync # work/1 → docs/ pages + docs/site.json (folder meta + /<num> short
# files git sees as modified/untracked first, so Enter runs the
# one in progress; then most recently tested; stamped on exit 0)
bun run submit # submit ONE solution to LeetCode (uncommitted first, then newest
# scaffold, nothing hidden; last_submitted stamped 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 `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
bun run release-topics -- --dry-run # prints every would-be topic release body
@@ -113,20 +114,21 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
- `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/<CAMPAIGN_DAYS>) LeetCode Daily Digest` — 79 with the plan ending Nov 3; booking a later day in `schedule.json` lengthens it, and the heatmaps and `/api/stats` `heat` with it.
- `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 frontmatter `icon:` and the `## Solution` section are regenerated, and `topics:` (LeetCode's `topicTags` as `{ name, slug }`, fetched by GraphQL) and `issue:` (the URL of the problem's `problem`-labelled GitHub issue, `LC <num> · …`, listed via `github()` — rendered as the neutral `Issue #N` badge after `Explanation`) are filled once — on create, or on any run that finds them missing — never refreshed (delete a key to refetch; offline or without `gh` auth, the page is written without it and the next sync retries; a sync with nothing missing makes no request). Only a problem the README plan lists gets `issue:` — an unplanned warm-up has no catalog issue, so it is never looked up. `## 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`): `docs/(0N-<phase>)/<topic>/`, with every problem page in the topic's `(problems)/` group as `<num>_<slug>.mdx` (a page is found by its frontmatter title, wherever it sits, so a layout change is one sync of `rename`s). `docs/site.json` is wholly script-owned and rewritten whenever it differs, because `mdxRemote()` reads no `meta.ts`: its `folders` are what a `meta.ts` per phase and topic folder would carry — a phase `N. Name` at `order: N`, a topic its exact label, curriculum order, `TOPIC_ICONS` icon and `display: "page"` — all derived from the README, so a rename there is a rename on the site; its `redirects` are the `/<num>` → `/<topic>/<slug>` short links. Which tags the site teaches as topics (`topic-landings.ts`) and its Appendix glossary (`appendix.ts`) are the site's, in prdlk/cs: sync no longer checks them. 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 through an emptied `(problems)` and topic into an emptied phase folder) unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one). 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/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) and `issue:` (the URL of the problem's `problem`-labelled GitHub issue, `LC <num> · …`, listed via `github()` — rendered as the neutral `Issue #N` badge after `Explanation`) are filled once — on create, or on any run that finds them missing — never refreshed (delete a key to refetch; offline or without `gh` auth, the page is written without it and the next sync retries; a sync with nothing missing makes no request). Only a problem the README plan lists gets `issue:` — an unplanned warm-up has no catalog issue, so it is never looked up. `companies:` (`{ name, period, frequency }`: every company that asked the problem in the last six months, at the most recent of the `Thirty Days`/`Three Months`/`Six Months` windows it appears in and its frequency there, most recent first — the vocabulary is ONE module, `apps/docs/companies.ts`, shared by sync, the schema and both readers) is the exception that IS rewritten every run, since recency ages: it comes from the local DuckDB company-frequency database (`~/.local/share/duckdb/data/leetcode-company-frequency.db`, built by that project's `mise run lc:build`) via one `duckdb -readonly -json` query, and where the database or the `duckdb` CLI is missing (CI) the step is skipped and pages keep the key as it is. `ProblemBadges.astro` renders it as a TypeTable-style table under the badge row (company pill · asked within · frequency bar), each pill linking to `companyHref()` — the company's Appendix card, filed A–Z among the glossary terms (category `Company`, no definition, no hand-written table: derived from every page's `companies:`), whose Solved backlinks are the problems it asked, each with how recently. `## 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>/`, with every problem page in the topic's `(problems)/` group as `<num>_<slug>.mdx` (a page is found by its frontmatter title, wherever it sits, so a layout change is one sync of `rename`s). The topic `meta.ts` is script-owned and rewritten every run (label, curriculum order, `TOPIC_ICONS` icon, `display: "page"`); 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 through an emptied `(problems)` and topic 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, `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 site's `/progress` islands (prdlk/cs `lib/stats.ts`) and `bun run stats`, so its shape is a cross-repo 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 campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`, `PHASE_ALIASES`). `/api/stats` and `/ask` reflect a requesting origin only when it is in the `DOCS_ORIGIN` var (`apps/api/wrangler.jsonc`, comma-separated: `https://lc.prd.lk,https://prdlk.github.io`) — a new host for the docs site must be added there.
- `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 a custom page, `apps/docs/pages/progress.astro` (no content folder, so outside the sidebar and prev/next; its header tab is its link), mounting five LeetCode-style React cards from `components/progress/` with `client:load` — imported by the page, never used from MDX, so they are not in `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 cards hydrate. shadcn sources live outside `islands/` (every `.tsx` there becomes an island) and import `lib/utils` relatively; `components.json` + `tsconfig.json` exist for the shadcn CLI and `tsc`, so after `shadcn add` rewrite `@/…` imports to relative ones (house style) — and check `package.json`: the CLI has twice installed an unrelated `cn` package for `lib/utils` and skipped real deps (`class-variance-authority`). Blume turns every tsconfig `paths` target into a Vite alias AND a dev dep-scan glob (`<target>/**/*.astro`…), so `paths` maps only `@/components`, `@/lib`, `@/hooks` — each bare and `/*`, since `components.json` names the bare forms — never `@/*` → `./*`: that glob reaches through `node_modules/blume`, where Vite's scanner reads a `<script>` inside a `RootLayout.astro` comment as a tag and `blume dev` skips dependency pre-bundling. 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`.
- **Near-zero runtime npm dependencies** — SVG, PNG, terminal ANSI, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. One exception, 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`. Do not extend it to any other module. (The docs site's own dependencies are prdlk/cs's.)
- **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. 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
- Repo-wide: no test framework — `bun run test` submits one solution to LeetCode's judge. Exception: `apps/api/src/srs.test.ts` (`bun:test`) proves the DST cron guards with fixed dates; run via `bun run api:test`.
- QA is dry-runs + local state: Worker logic against `wrangler dev` + local D1 with `?dry=1&date=`; reconcilers with `--dry-run`; idempotency check = run twice, `cmp` output.
- Docs changes: the site builds in prdlk/cs, which must pass `bunx blume build --isolated` with no new warnings against this repo's `docs/`; after sync, review only *created* pages (prose transforms are heuristic) and `docs/site.json`'s diff.
- Docs changes: `bunx blume build --isolated` (from `apps/docs/`) must pass with no new warnings (then `rm -rf apps/docs/.blume-verify`); after sync, review only *created* pages (prose transforms are heuristic).
+4 -4
View File
@@ -21,14 +21,14 @@
* tagged commit — the last main commit before the finish. Problems still open
* at the finish are left out: the release records what finished, and an open
* optional problem is tracked by its issue (or its Review day), not here.
* The docs site (prdlk/cs `lib/releases.ts`) reads these `### Closed …`
* tables back for its `/changelog` timeline, so their row shape is its contract.
* apps/docs/lib/releases.ts reads these `### Closed …` tables back for the
* docs `/changelog` timeline, so their row shape is its contract.
*
* 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), so the docs changelog dates each entry from
* this timever tag instead (prdlk/cs `lib/releases.ts`)
* this timever tag instead (apps/docs/lib/releases.ts)
* — the tag format is that contract.
*
* Reconciles instead of reacting: every finished topic or review without a
@@ -59,7 +59,7 @@ const DRY =
process.env.DRY_RUN === "1" ||
process.env.DRY_RUN === "true";
/** The docs site (prdlk/cs `blume.config.ts` `deployment.site`); it publishes sync's `/<num>` short links. */
/** The docs site (apps/docs/blume.config.ts `deployment.site`); sync publishes `/<num>` short links. */
const DOCS_SITE = "https://lc.prd.lk";
const TIMEVER = "https://gist.github.com/twolfson/de1b004dd22536b8e668";
+38 -5
View File
@@ -9,10 +9,12 @@
*
* Every run backfills the db's solutions table with created_at (from the
* file's birthtime, mtime when the filesystem has none) so ordering works for
* files that predate the db. Ordering is per command — test lists the most
* recently tested first (never-tested after, by name); submit lists the most
* recently created first. Nothing is ever hidden. Exit 0 of leetcode-cli
* stamps last_tested / last_submitted.
* files that predate the db. Files git reports as modified, added or
* untracked come first, newest edit first — the problem being worked on is
* the focused option, so Enter runs it without a search. The rest are ordered
* per command — test lists the most recently tested first (never-tested
* after, by name); submit lists the most recently created first. Nothing is
* ever hidden. Exit 0 of leetcode-cli stamps last_tested / last_submitted.
*
* Library: side-effect-free on import — nothing scans the filesystem until
* runOnSolution() is called.
@@ -29,6 +31,29 @@ function lcNumber(rel: string): number | undefined {
return m ? Number(m[1]) : undefined;
}
/**
* work/-relative paths of solutions git sees as modified, added or untracked,
* mapped to their mtime. Empty outside a git checkout.
*/
function dirtySolutions(scanned: Set<string>): Map<string, number> {
const git = Bun.spawnSync(
["git", "status", "--porcelain", "-z", "--untracked-files=all", "--", WORK_DIR],
{ cwd: ROOT },
);
const dirty = new Map<string, number>();
if (!git.success) return dirty;
const entries = git.stdout.toString().split("\0");
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]!;
// renames/copies carry their source path as the next entry; skip it
if (/[RC]/.test(entry.slice(0, 2))) i++;
const rel = relative(WORK_DIR, join(ROOT, entry.slice(3)));
// deletions and non-solution files are not in the scan
if (scanned.has(rel)) dirty.set(rel, statSync(join(WORK_DIR, rel)).mtimeMs);
}
return dirty;
}
/** Fuzzy-pick a solution under work/ and run `leetcode <command>` on it. */
export async function runOnSolution(
command: "test" | "submit",
@@ -61,7 +86,15 @@ export async function runOnSolution(
return (command === "test" ? row?.lastTested : row?.createdAt) ?? undefined;
};
const dirty = dirtySolutions(new Set(scanned));
const files = scanned.sort((a, b) => {
const ma = dirty.get(a);
const mb = dirty.get(b);
if (ma !== mb) {
if (ma === undefined) return 1;
if (mb === undefined) return -1;
return mb - ma;
}
const ka = sortKey(a);
const kb = sortKey(b);
if (ka !== kb) {
@@ -91,7 +124,7 @@ export async function runOnSolution(
value: f,
label: basename(f),
hint: parsed
? `${stage} · ${parsed.difficulty} · ${parsed.category}`
? `${dirty.has(f) ? "uncommitted · " : ""}${stage} · ${parsed.difficulty} · ${parsed.category}`
: f,
};
}),
+369 -109
View File
@@ -1,13 +1,6 @@
#!/usr/bin/env bun
/**
* Sync work/1 leetcode solutions into docs/ pages, plus docs/site.json.
*
* docs/ is content only — MDX pages and one generated JSON manifest, no Blume
* config. The docs site itself (config, components, islands, the Appendix)
* lives in prdlk/cs and reads docs/ over HTTP with Blume's `mdxRemote()`
* source, which fetches `.md`/`.mdx` and nothing else: no `meta.ts`. So the
* sidebar meta a folder `meta.ts` used to carry, and the `/<num>` short links,
* travel in `docs/site.json` instead, which the site fetches at the same ref.
* Sync work/1 leetcode solutions into apps/docs/content/ pages.
*
* - New problems get a full page (frontmatter, warning, examples,
* constraints, solution) in the gold-standard format.
@@ -22,9 +15,9 @@
* `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 site's MDX `wrapper` (prdlk/cs
* `components/ProblemBadges.astro`) turns `topics:` and the `video:` key
* `videos.ts` writes into the badge row under the description.
* 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 planned page (a problem the README plan lists) carries its GitHub
* problem issue as frontmatter `issue:` (the URL of the `problem`-labelled
* issue titled `LC <num> · …`), rendered by the same wrapper as a GitHub
@@ -34,11 +27,27 @@
* (never on a sync with nothing new); offline, or with no `gh` auth, the
* page is written without it and the next sync retries. Delete the key to
* refetch it.
* - Every page carries the companies that asked it in the last six months as
* frontmatter `companies:` (`{ name, period, frequency }`, the most recent
* window each company falls in and its frequency there, most recent first;
* `apps/docs/companies.ts`), read from the local DuckDB
* company-frequency database (`COMPANY_DB`, built by its own `mise run
* lc:build`) with the `duckdb` CLI. Unlike the keys above it is rewritten
* every run, since recency ages and a rebuilt database should show; a
* problem no company asked lately carries no key. Where the database or the
* CLI is missing (CI), the step is skipped and pages keep the key as is.
* - Every tag a page carries that no campaign topic teaches (not a key of
* `apps/docs/topic-landings.ts`) belongs in the Appendix glossary,
* `apps/docs/appendix.ts`, which is hand-written: sync only checks it,
* warning about a carried tag with no term (its badge falls back to
* LeetCode's list), a term whose tag a topic now teaches, and a term icon
* that is missing, a topic's (`TOPIC_ICONS`), or another term's — every
* badge linking to the term shows it.
*
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`).
* 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.
* 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 adding no route
@@ -49,33 +58,34 @@
* `tree-dfs/(problems)/226_invert-binary-tree.mdx`: no route segment either,
* and Blume strips the `<num>_` prefix from the route while sorting by it, so
* the URL is still `/tree-dfs/invert-binary-tree`. 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 docs/ root.
*
* `docs/site.json` is wholly script-owned and rewritten whenever it differs:
* - `folders`: Blume folder meta keyed by folder path under docs/ — exactly
* the key a `meta.ts` in that folder would have. A phase is `N. Name` in
* phase order; a topic is its exact name (the folder slug loses `/` and
* case), curriculum order, `TOPIC_ICONS` icon and `display: "page"`, so it
* is one sidebar row drilling into its own panel. `(problems)` has none:
* Blume's humanized "Problems" is the label.
* - `redirects`: one `{ from: "/<num>", to: "/<topic>/<slug>" }` per page, in
* number order — exact root-relative paths; Blume prepends the deployment
* base itself.
* 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
* there would otherwise wipe the page's `## Solution`.
*
* Every page also gets a short link: `/<num>` → `/<topic>/<slug>` (so
* `/leetcode/217` opens Contains Duplicate). The rules live in the generated
* `apps/docs/redirects.ts`, which `blume.config.ts` imports — Blume takes exact
* root-relative paths and prepends the deployment base itself.
*/
import { mkdir, readdir, rename, rmdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { APPENDIX } from "../docs/appendix.ts";
import { RECENCY, type Company } from "../docs/companies.ts";
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
import { github } from "./github.ts";
const ROOT = join(import.meta.dir, "..", "..");
const WORK = join(ROOT, "work", "1");
const DOCS = join(ROOT, "docs");
const MANIFEST = join(DOCS, "site.json");
const DOCS = join(ROOT, "apps", "docs", "content");
const REDIRECTS = join(ROOT, "apps", "docs", "redirects.ts");
type Lang = "js" | "py";
@@ -106,11 +116,15 @@ interface Solution {
// ── work/ parsing ────────────────────────────────────────────────
/** Split a source file into (header comment text, code). */
function splitSource(src: string, lang: Lang): { header: string; code: string } {
function splitSource(
src: string,
lang: Lang,
): { header: string; code: string } {
let header: string, code: string;
if (lang === "js") {
const end = src.indexOf("*/");
if (!src.trimStart().startsWith("/*") || end === -1) throw new Error("missing /* header */");
if (!src.trimStart().startsWith("/*") || end === -1)
throw new Error("missing /* header */");
header = src
.slice(src.indexOf("/*") + 2, end)
.split("\n")
@@ -142,7 +156,11 @@ function paragraphs(lines: string[]): string[] {
flush();
continue;
}
if (/^([•-] |Input:|Output:|Explanation:|Example \d+:|Constraints:|Follow[- ]?ups?:)/.test(text)) {
if (
/^([•-] |Input:|Output:|Explanation:|Example \d+:|Constraints:|Follow[- ]?ups?:)/.test(
text,
)
) {
flush();
cur = text;
} else {
@@ -160,7 +178,10 @@ function parseHeader(header: string): Header {
const num = Number.parseInt(titleLine, 10);
const title = titleLine.replace(/^\d+\.\s*/, "");
const difficulty =
lines.find((l) => l.trim().startsWith("Difficulty:"))?.split(":")[1]?.trim() ?? "";
lines
.find((l) => l.trim().startsWith("Difficulty:"))
?.split(":")[1]
?.trim() ?? "";
// Body: everything after the ───── separator.
const sep = lines.findIndex((l) => /^[─]{3,}/.test(l.trim()));
@@ -290,10 +311,18 @@ async function readmeTopics(): Promise<Map<number, Topic>> {
const p = line.match(/^## Phase ([IVX]+) — (.+?) · /);
if (p) {
const name = p[2]!.replaceAll("&amp;", "&");
phase = { order: ROMAN.indexOf(p[1]!) + 1, name: PHASE_ALIASES[name] ?? name };
phase = {
order: ROMAN.indexOf(p[1]!) + 1,
name: PHASE_ALIASES[name] ?? name,
};
}
const t = line.match(/<strong>(\d+)\.\s*([^<]+)<\/strong>/);
if (t) topic = { order: Number(t[1]), name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim(), phase };
if (t)
topic = {
order: Number(t[1]),
name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim(),
phase,
};
if (!topic) continue;
const n = line.match(/<td align="center">(\d+)<\/td>/);
if (n && !map.has(Number(n[1]))) map.set(Number(n[1]), topic);
@@ -329,34 +358,38 @@ function phaseDir(phase: Phase): string {
return `(${String(phase.order).padStart(2, "0")}-${slugify(phase.name)})`;
}
/** A sidebar group's Blume folder meta, as `docs/site.json` carries it. */
interface FolderMeta {
title: string;
order: number;
icon?: string;
display?: "page";
/**
* 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`;
}
/**
* Folder meta for a phase `(group)`: the overview page's heading, `N. Name`,
* in phase order. Derived like everything in the manifest, so a README rename
* renames the heading — and lands in a new folder (`phaseDir`) anyway.
* Blume folder meta for a topic: exact label (the folder slug loses `/` and
* case), curriculum order, the sidebar icon, and `display: "page"` so the
* topic is one row under its phase heading that drills into its own panel
* (the landing page, then its `(problems)` list). An unplanned category group
* (warm-ups) has no README number and sorts at 0, ahead of phase 1, so the
* sidebar opens on the basics.
*/
function phaseMeta(phase: Phase): FolderMeta {
return { title: `${phase.order}. ${phase.name}`, order: phase.order };
}
/**
* Folder meta for a topic: exact label, curriculum order, the sidebar icon,
* and `display: "page"` so the topic is one row under its phase heading that
* drills into its own panel (the landing page, then its `(problems)` list).
* An unplanned category group (warm-ups) has no README number and sorts at 0,
* ahead of phase 1, so the sidebar opens on the basics.
*/
function groupMeta(topic: Topic): FolderMeta {
function groupMeta(topic: Topic): string {
const icon = TOPIC_ICONS[topic.name];
if (!icon) console.warn(`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`);
return { title: topic.name, order: topic.order ?? 0, ...(icon ? { icon } : {}), display: "page" };
if (!icon)
console.warn(
`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`,
);
const fields = [
`title: ${JSON.stringify(topic.name)}`,
`order: ${topic.order ?? 0}`,
];
if (icon) fields.push(`icon: ${JSON.stringify(icon)}`);
fields.push(`display: "group"`);
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
}
// ── LeetCode topic tags ──────────────────────────────────────────
@@ -382,17 +415,24 @@ async function fetchTopicTags(sol: Solution): Promise<TopicTag[] | undefined> {
referer: `https://leetcode.com/problems/${sol.slug}/`,
},
body: JSON.stringify({
query: "query topicTags($titleSlug: String!) { question(titleSlug: $titleSlug) { topicTags { name slug } } }",
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 } };
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");
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}`);
console.warn(
`no topics for ${sol.num}.${sol.slug}: ${err instanceof Error ? err.message : err}`,
);
return undefined;
}
}
@@ -400,7 +440,30 @@ async function fetchTopicTags(sol: Solution): Promise<TopicTag[] | 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");
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",
);
}
// ── GitHub problem issues ────────────────────────────────────────
@@ -418,7 +481,9 @@ function problemIssues(): Promise<Map<number, string> | undefined> {
try {
const gh = await github();
const urls = new Map<number, string>();
for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=problem&state=all`)) {
for await (const raw of gh.list(
`/repos/${gh.repo}/issues?labels=problem&state=all`,
)) {
if (!raw || typeof raw !== "object" || "pull_request" in raw) continue;
if (!("title" in raw) || typeof raw.title !== "string") continue;
if (!("html_url" in raw) || typeof raw.html_url !== "string") continue;
@@ -427,7 +492,9 @@ function problemIssues(): Promise<Map<number, string> | undefined> {
}
return urls;
} catch (err) {
console.warn(`no problem issues: ${err instanceof Error ? err.message : err}`);
console.warn(
`no problem issues: ${err instanceof Error ? err.message : err}`,
);
return undefined;
}
})();
@@ -444,23 +511,84 @@ async function issueUrl(sol: Solution): Promise<string | undefined> {
if (!topics.has(sol.num)) return undefined;
const urls = await problemIssues();
const url = urls?.get(sol.num);
if (urls && !url) console.warn(`no problem issue titled "LC ${sol.num} · …" for ${sol.num}.${sol.slug}`);
if (urls && !url)
console.warn(
`no problem issue titled "LC ${sol.num} · …" for ${sol.num}.${sol.slug}`,
);
return url;
}
// ── company frequency ────────────────────────────────────────────
// The windows and the key's shape are `apps/docs/companies.ts`, shared with
// the schema and the readers; `More Than Six Months` and `All` are not recent.
const COMPANY_DB = join(
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
"duckdb",
"data",
"leetcode-company-frequency.db",
);
/**
* LC number → the companies that asked it in the last six months, each at the
* most recent window it appears in (the windows nest), ordered most recent
* first, then by that window's frequency. Undefined when this machine has no
* database or no `duckdb` CLI — the step is skipped — or the query fails
* (warned). One query for every solved problem.
*/
function recentCompanies(nums: number[]): Map<number, Company[]> | undefined {
if (!existsSync(COMPANY_DB) || !Bun.which("duckdb")) return undefined;
const order = `list_position([${RECENCY.map((p) => `'${p}'`).join(", ")}], period)`;
const sql = `SELECT question_id, company, period, frequency FROM company_frequency
WHERE question_id IN (${nums.join(", ")}) AND ${order} IS NOT NULL
QUALIFY row_number() OVER (PARTITION BY question_id, company ORDER BY ${order}) = 1
ORDER BY question_id, ${order}, frequency DESC, company`;
const proc = Bun.spawnSync(["duckdb", "-readonly", "-json", COMPANY_DB, "-c", sql]);
if (!proc.success) {
console.warn(`no companies: duckdb -> ${proc.stderr.toString().trim()}`);
return undefined;
}
const out = proc.stdout.toString().trim();
const rows: unknown = out ? JSON.parse(out) : [];
const companies = new Map<number, Company[]>(nums.map((n) => [n, []]));
for (const row of Array.isArray(rows) ? rows : []) {
if (!row || typeof row !== "object") continue;
if (!("question_id" in row) || typeof row.question_id !== "number") continue;
if (!("company" in row) || typeof row.company !== "string") continue;
if (!("period" in row) || !("frequency" in row) || typeof row.frequency !== "number") continue;
const period = RECENCY.find((p) => p === row.period);
if (period)
companies.get(row.question_id)?.push({ name: row.company, period, frequency: row.frequency });
}
return companies;
}
/** The frontmatter `companies:` block, one flow mapping per company (JSON strings are valid YAML). */
function companiesYaml(list: Company[]): string {
return [
"companies:",
...list.map(
(c) =>
` - { name: ${JSON.stringify(c.name)}, period: ${JSON.stringify(c.period)}, frequency: ${c.frequency} }`,
),
].join("\n");
}
// ── inline-code formatting heuristics ────────────────────────────
/** Identifiers worth backticking in prose, harvested from the code. */
function identifiers(sol: Solution): Set<string> {
const ids = new Set<string>(["n", "m", "k"]);
for (const code of Object.values(sol.code)) {
for (const m of code.matchAll(/@param\s*\{[^}]*\}\s*(\w+)/g)) ids.add(m[1]!);
for (const m of code.matchAll(/@param\s*\{[^}]*\}\s*(\w+)/g))
ids.add(m[1]!);
for (const m of code.matchAll(/def \w+\(self,?\s*([^)]*)\)/g))
for (const arg of m[1]!.split(","))
if (arg.trim()) ids.add(arg.split(":")[0]!.trim());
for (const m of code.matchAll(/(?:var|const|let) (\w+) = function\s*\(([^)]*)\)/g))
for (const arg of m[2]!.split(","))
if (arg.trim()) ids.add(arg.trim());
for (const m of code.matchAll(
/(?:var|const|let) (\w+) = function\s*\(([^)]*)\)/g,
))
for (const arg of m[2]!.split(",")) if (arg.trim()) ids.add(arg.trim());
}
return ids;
}
@@ -517,7 +645,11 @@ function backtickify(text: string, ids: Set<string>): string {
while (j > i && /^[+\-*/=%]$/.test(toks[j]!.core)) j--;
const span = toks
.slice(i, j + 1)
.map((tok, idx, arr) => (idx === arr.length - 1 ? `${tok.pre}${tok.core}` : `${tok.pre}${tok.core}${tok.post}`))
.map((tok, idx, arr) =>
idx === arr.length - 1
? `${tok.pre}${tok.core}`
: `${tok.pre}${tok.core}${tok.post}`,
)
.join(" ");
out.push(`\`${span}\`${toks[j]!.post}`);
i = j + 1;
@@ -545,14 +677,23 @@ function warningText(h: Header): string {
return (
h.statement
.slice(1)
.find((p) => /^(You must|Your solution must|Your algorithm|Notice that|Could you)/.test(p)) ?? ""
.find((p) =>
/^(You must|Your solution must|Your algorithm|Notice that|Could you)/.test(
p,
),
) ?? ""
);
}
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
const PAGE_ICON = "dot";
function renderPage(sol: Solution, tags: TopicTag[] | undefined, issue: string | undefined): string {
function renderPage(
sol: Solution,
tags: TopicTag[] | undefined,
issue: string | undefined,
companies: Company[] | undefined,
): string {
const h = sol.header;
const ids = identifiers(sol);
const description = h.statement[0]?.replace(/\.\s*$/, "") ?? "";
@@ -561,10 +702,13 @@ function renderPage(sol: Solution, tags: TopicTag[] | undefined, issue: string |
parts.push(
"---",
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
/[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`,
/[:#]/.test(description)
? `description: '${description.replaceAll("'", "''")}'`
: `description: ${description}`,
`icon: ${PAGE_ICON}`,
...(tags ? [topicsYaml(tags)] : []),
...(issue ? [`issue: ${JSON.stringify(issue)}`] : []),
...(companies?.length ? [companiesYaml(companies)] : []),
"sidebar:",
` label: '${h.title.replaceAll("'", "''")}'`,
` badge: '${h.difficulty}'`,
@@ -583,7 +727,8 @@ function renderPage(sol: Solution, tags: TopicTag[] | undefined, issue: string |
parts.push(`- Explanation: ${backtickify(ex.explanation[0]!, ids)}`);
else if (ex.explanation.length > 1) {
parts.push("- Explanation:");
for (const item of ex.explanation) parts.push(` - ${backtickify(item, ids)}`);
for (const item of ex.explanation)
parts.push(` - ${backtickify(item, ids)}`);
}
parts.push("");
});
@@ -608,7 +753,10 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
}
const num = Number(m[1]);
const slug = m[2]!;
const { header, code } = splitSource(await Bun.file(join(WORK, rel)).text(), lang);
const { header, code } = splitSource(
await Bun.file(join(WORK, rel)).text(),
lang,
);
const parsed = parseHeader(header);
const existing = solutions.get(num);
if (existing) {
@@ -626,6 +774,7 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
}
const topics = await readmeTopics();
const companies = recentCompanies([...solutions.keys()]);
// An unplanned problem whose work/ category is also a README topic name (a
// `Hash Table` warm-up) files under that topic, in its phase, not a second
// root-level `hash-table/`.
@@ -657,7 +806,10 @@ async function ensureTopics(page: string, sol: Solution): Promise<string> {
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);
return (
fm[0].replace(/^sidebar:$/m, `${topicsYaml(tags)}\nsidebar:`) +
page.slice(fm[0].length)
);
}
/** Insert `issue:` before `sidebar:` when the frontmatter lacks it; an existing URL is kept as is. */
@@ -666,7 +818,27 @@ async function ensureIssue(page: string, sol: Solution): Promise<string> {
if (!fm || /^issue:/m.test(fm[0])) return page;
const url = await issueUrl(sol);
if (!url) return page;
return fm[0].replace(/^sidebar:$/m, `issue: ${JSON.stringify(url)}\nsidebar:`) + page.slice(fm[0].length);
return (
fm[0].replace(/^sidebar:$/m, `issue: ${JSON.stringify(url)}\nsidebar:`) +
page.slice(fm[0].length)
);
}
/**
* Replace the frontmatter's `companies:` with this run's list, inserted before
* `sidebar:` (dropped when no company asked lately). Without the database the
* page is returned untouched, so an environment lacking it never strips the key.
*/
function ensureCompanies(page: string, sol: Solution): string {
if (!companies) return page;
const fm = page.match(/^---\n[\s\S]*?\n---/);
if (!fm) return page;
const list = companies.get(sol.num) ?? [];
const without = fm[0].replace(/^companies:.*\n(?: {2}- .*\n)*/m, "");
const next = list.length
? without.replace(/^sidebar:$/m, `${companiesYaml(list)}\nsidebar:`)
: without;
return next + page.slice(fm[0].length);
}
/**
@@ -689,7 +861,9 @@ function solutionEnd(page: string, from: number): number {
// Existing docs pages, keyed by leetcode number from the frontmatter title.
const pages = new Map<number, string>(); // num -> path relative to DOCS
for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).sort()) {
for (const rel of (
await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))
).sort()) {
const text = await Bun.file(join(DOCS, rel)).text();
const t = text.match(/^title: '(\d+)\./m);
if (t) pages.set(Number(t[1]), rel);
@@ -697,9 +871,11 @@ for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).s
const report: [string, string, string][] = [];
const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
const folders = new Map<string, FolderMeta>(); // folder under docs/ -> its sidebar meta, for site.json
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: { from: string; to: string }[] = []; // one per page, numeric order
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
for (const sol of ordered) {
const langs = Object.keys(sol.code).sort().join("+");
@@ -707,14 +883,16 @@ for (const sol of ordered) {
// (0N-<phase>)/<topic>/(problems)/ folder (the topic is a real route segment,
// so its index.mdx is the landing page; `(problems)` adds none) +
// leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
const topic = topics.get(sol.num) ?? topicByName.get(sol.category) ?? { name: sol.category };
const topic = topics.get(sol.num) ??
topicByName.get(sol.category) ?? { name: sol.category };
const dir = topicDir(topic);
if (!folders.has(dir)) folders.set(dir, groupMeta(topic));
if (topic.phase) folders.set(phaseDir(topic.phase), phaseMeta(topic.phase));
const name = `${dir}/(problems)/${sol.num}_${sol.slug}.mdx`;
groups.set(dir, topic);
if (topic.phase) phases.set(phaseDir(topic.phase), topic.phase);
const name = `${dir}/${sol.num}_${sol.slug}.mdx`;
const existing = pages.get(sol.num);
await mkdir(join(DOCS, dirname(name)), { 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");
@@ -723,8 +901,15 @@ for (const sol of ordered) {
continue;
}
const tail = page.slice(solutionEnd(page, at));
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
const next = await ensureIssue(await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol), sol);
const body =
page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
const next = ensureCompanies(
await ensureIssue(
await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol),
sol,
),
sol,
);
const actions: string[] = [];
if (existing !== name) {
await rename(join(DOCS, existing), join(DOCS, name));
@@ -736,41 +921,115 @@ 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, await fetchTopicTags(sol), await issueUrl(sol)));
const page = renderPage(
sol,
await fetchTopicTags(sol),
await issueUrl(sol),
companies?.get(sol.num),
);
await Bun.write(join(DOCS, name), page);
report.push([workRef, name, "created"]);
written = page;
}
// The route drops the phase `(group)`, `(problems)` and the number prefix.
redirects.push({ from: `/${sol.num}`, to: `/${slugify(topic.name)}/${sol.slug}` });
for (const tag of frontmatterTopics(written)) tags.set(tag.slug, tag);
// 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}`)} },`,
);
}
// The site's one generated input besides the pages; script-owned, rewritten
// only when it differs. Folders sorted by path so a diff reads by location.
// Script-owned short links, one per page; imported by blume.config.ts.
{
const manifest = `${JSON.stringify(
{ folders: Object.fromEntries([...folders].sort(([a], [b]) => (a < b ? -1 : 1))), redirects },
null,
2,
)}\n`;
if (!(await Bun.file(MANIFEST).exists()) || (await Bun.file(MANIFEST).text()) !== manifest) {
await Bun.write(MANIFEST, manifest);
report.push([`${folders.size} folders, ${redirects.length} redirects`, "docs/site.json", "written"]);
const module = [
"// Generated by `bun run sync` (apps/cli/sync.ts) — do not edit.",
"// `/leetcode/<num>` → that problem's page; exact paths, base added by Blume.",
"export default [",
...redirects,
"];",
"",
].join("\n");
if (
!(await Bun.file(REDIRECTS).exists()) ||
(await Bun.file(REDIRECTS).text()) !== module
) {
await Bun.write(REDIRECTS, module);
report.push([
`${redirects.length} redirects`,
"apps/docs/redirects.ts",
"written",
]);
}
}
// A folder every page left behind is dead: drop it, then the folders above it
// that emptied too (`(problems)`, its topic, the phase). A folder that still
// holds anything (a hand-written index.mdx, another topic) stops the climb.
// Every carried tag no campaign topic teaches is an Appendix term, filed by
// hand in apps/docs/appendix.ts: warn about the gaps and the dead weight. A
// term's icon is also the icon on every problem-page badge linking to its
// card, so it must not be a topic group's (`TOPIC_ICONS`), so a term badge
// never reads as a topic's, nor another term's.
for (const tag of [...tags.values()].sort((a, b) =>
a.slug.localeCompare(b.slug),
)) {
if (!TOPIC_LANDINGS[tag.slug] && !APPENDIX[tag.slug])
console.warn(
`tag ${tag.slug}: no Appendix term; add ${JSON.stringify(tag.name)} to apps/docs/appendix.ts`,
);
}
const topicIcons = new Set(Object.values(TOPIC_ICONS));
const termIcons = new Map<string, string>(); // icon -> the term that took it first
for (const [slug, term] of Object.entries(APPENDIX)) {
if (TOPIC_LANDINGS[slug])
console.warn(
`appendix ${slug}: a campaign topic teaches it now; delete its term`,
);
if (topicIcons.has(term.icon))
console.warn(
`appendix ${slug}: icon ${JSON.stringify(term.icon)} is a topic's (TOPIC_ICONS); pick another`,
);
else if (termIcons.has(term.icon))
console.warn(
`appendix ${slug}: icon ${JSON.stringify(term.icon)} is ${termIcons.get(term.icon)}'s too; pick another`,
);
else termIcons.set(term.icon, slug);
}
// 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 — rewritten only when
// they differ.
const metas: [label: string, dir: string, meta: string][] = [...groups].map(
([dir, topic]) => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)],
);
for (const [label, dir, meta] of metas) {
const path = join(DOCS, dir, "meta.ts");
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta)
continue;
await Bun.write(path, meta);
report.push([label, `${dir}/meta.ts`, "written"]);
}
// A folder every page left behind is dead: drop its meta and the folder itself,
// then the phase folder above it if that emptied too. A folder that still holds
// anything else (a hand-written index.mdx, another topic) stops the climb.
for (const start of vacated) {
for (let dir = start; dir !== "."; dir = dirname(dir)) {
if (folders.has(dir)) break;
if (groups.has(dir) || phases.has(dir)) break;
let left: string[];
try {
left = await readdir(join(DOCS, dir));
} catch {
break; // already removed via a sibling's climb
}
if (left.length) break;
if (left.some((f) => f !== "meta.ts")) break;
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
await rmdir(join(DOCS, dir));
report.push(["", dir, "removed (empty)"]);
}
@@ -780,4 +1039,5 @@ const w0 = Math.max(...report.map((r) => r[0].length), 9);
const w1 = Math.max(...report.map((r) => r[1].length), 9);
console.log(`${"work file".padEnd(w0)} ${"docs page".padEnd(w1)} status`);
console.log(`${"─".repeat(w0)} ${"─".repeat(w1)} ${"─".repeat(20)}`);
for (const [a, b, c] of report) console.log(`${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`);
for (const [a, b, c] of report)
console.log(`${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`);
+3 -3
View File
@@ -3,8 +3,8 @@
* Find the NeetCode explanation video for every docs page that has none.
*
* A finished solution page carries frontmatter `video: <YouTube id>` —
* NeetCode's walkthrough of the problem — which the site's MDX `wrapper`
* (prdlk/cs `components/ProblemBadges.astro`) renders as the red
* 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
@@ -25,7 +25,7 @@ import { join } from "node:path";
import { printTable } from "./report.ts";
import { ROOT } from "./work.ts";
const DOCS = join(ROOT, "docs");
const DOCS = join(ROOT, "apps", "docs", "content");
const WRITE = process.argv.includes("--write");
/** Channel handles searched in order; the first exact match wins. */
+1
View File
@@ -0,0 +1 @@
.blume-verify/
+232
View File
@@ -0,0 +1,232 @@
/**
* The Appendix glossary: one term per LeetCode topic tag that problem pages
* carry but no campaign topic teaches (not a key of `topic-landings.ts`).
* `pages/appendix.astro` renders each as a card — icon, category, definition,
* and the solved problems carrying the tag — anchored at `/appendix#<slug>`,
* which is where `tagTarget()` sends that tag's badge on a problem page.
*
* Hand-written: `apps/cli/sync.ts` only checks it, warning about a carried
* tag with no entry (its card renders bare), an entry whose tag a campaign
* topic now teaches (dead weight, delete it), and an icon that is a topic's
* (`TOPIC_ICONS`) or another term's — the icon is also the one on every
* badge linking here, so it must not read as anything else. Plain data, no
* Astro imports: sync imports this file under Bun.
*/
export type AppendixCategory = "Data Structures" | "Algorithms" | "Techniques";
export interface AppendixTerm {
/** LeetCode's tag name. */
name: string;
/** Lucide icon, unique across terms and topics. */
icon: string;
category: AppendixCategory;
/** One sentence: what the term means, the way a solution uses it. */
definition: string;
}
export const APPENDIX: Record<string, AppendixTerm> = {
// ── data structures ──────────────────────────────────────────────
array: {
name: "Array",
icon: "list-ordered",
category: "Data Structures",
definition: "A contiguous, index-addressed sequence: O(1) access by position, O(n) to insert or delete in the middle.",
},
"binary-tree": {
name: "Binary Tree",
icon: "git-branch",
category: "Data Structures",
definition: "A rooted tree in which every node has at most two children, left and right.",
},
"directed-acyclic-graph": {
name: "Directed Acyclic Graph",
icon: "route",
category: "Data Structures",
definition: "A directed graph with no cycles, which is exactly what lets its nodes be put in a topological order.",
},
"doubly-linked-list": {
name: "Doubly-Linked List",
icon: "link-2",
category: "Data Structures",
definition: "A linked list whose nodes point to both neighbours, so a node you already hold is unlinked in O(1).",
},
graph: {
name: "Graph Theory",
icon: "share-2",
category: "Data Structures",
definition: "Nodes joined by edges, directed or not, weighted or not, stored as an adjacency list or matrix.",
},
"k-d-tree": {
name: "K-D Tree",
icon: "axis-3d",
category: "Data Structures",
definition: "A binary tree that splits k-dimensional points on alternating axes, for nearest-neighbour and range queries.",
},
queue: {
name: "Queue",
icon: "list-end",
category: "Data Structures",
definition: "First in, first out: push at the back, pop from the front, both O(1).",
},
string: {
name: "String",
icon: "quote",
category: "Data Structures",
definition: "A sequence of characters; solutions lean on its small alphabet, its substrings, and its immutability.",
},
tree: {
name: "Tree",
icon: "tree-deciduous",
category: "Data Structures",
definition: "A connected acyclic graph, usually rooted, in which every node but the root has exactly one parent.",
},
// ── algorithms ───────────────────────────────────────────────────
"bidirectional-search": {
name: "Bidirectional Search",
icon: "move-horizontal",
category: "Algorithms",
definition: "Search from the start and the goal at once and stop where the frontiers meet, about 2·b^(d/2) states instead of b^d.",
},
"binary-lifting": {
name: "Binary Lifting",
icon: "chevrons-up",
category: "Algorithms",
definition: "Precompute every node's 2^k-th ancestor so any k-th ancestor, and so the LCA, takes O(log n) jumps.",
},
"bit-manipulation": {
name: "Bit Manipulation",
icon: "binary",
category: "Algorithms",
definition: "Treat integers as bit vectors: masks, shifts, and XOR in place of arithmetic or extra memory.",
},
"bracket-sequences": {
name: "Bracket Sequences",
icon: "parentheses",
category: "Algorithms",
definition: "Strings of opening and closing brackets; balanced means the running depth never dips below zero and ends at zero.",
},
"bucket-sort": {
name: "Bucket Sort",
icon: "container",
category: "Algorithms",
definition: "Scatter values into buckets by range or frequency, then read the buckets in order: linear when the key range is bounded.",
},
"complete-knapsack": {
name: "Complete Knapsack",
icon: "package-plus",
category: "Algorithms",
definition: "Knapsack in which every item may be taken any number of times, so the DP walks capacity upward and reuses the row.",
},
"divide-and-conquer": {
name: "Divide and Conquer",
icon: "split",
category: "Algorithms",
definition: "Split the input into independent parts, solve each recursively, and combine their answers.",
},
"floyds-cycle-finding-algorithm": {
name: "Floyd's Cycle Finding Algorithm",
icon: "repeat",
category: "Algorithms",
definition: "A slow and a fast pointer meet inside any cycle; restarting one from the head finds the cycle's entry, in O(1) space.",
},
"knapsack-problem": {
name: "Knapsack Problem",
icon: "backpack",
category: "Algorithms",
definition: "Pick items with weights and values to maximise value under a capacity; the 0/1 version is an O(n·W) DP.",
},
"longest-common-subsequence": {
name: "Longest Common Subsequence",
icon: "diff",
category: "Algorithms",
definition: "The longest sequence that appears in order, not necessarily contiguously, in two strings: the classic O(n·m) 2-D DP.",
},
"lowest-common-ancestor": {
name: "Lowest Common Ancestor",
icon: "git-merge",
category: "Algorithms",
definition: "The deepest node of a rooted tree that is an ancestor of both given nodes.",
},
memoization: {
name: "Memoization",
icon: "database",
category: "Algorithms",
definition: "Cache a pure function's result per argument so every subproblem is solved once: top-down dynamic programming.",
},
"merge-sort": {
name: "Merge Sort",
icon: "blend",
category: "Algorithms",
definition: "Sort each half recursively, then merge the two sorted halves: O(n log n), stable, O(n) extra space.",
},
quickselect: {
name: "Quickselect",
icon: "crosshair",
category: "Algorithms",
definition: "Partition around a pivot and recurse into one side only, finding the k-th smallest element in O(n) on average.",
},
quicksort: {
name: "Quicksort",
icon: "shuffle",
category: "Algorithms",
definition: "Partition around a pivot, then sort each side in place: O(n log n) on average, O(n²) at worst.",
},
recursion: {
name: "Recursion",
icon: "iteration-ccw",
category: "Algorithms",
definition: "Solve a problem by calling the same function on smaller inputs until a base case answers directly.",
},
simulation: {
name: "Simulation",
icon: "monitor-play",
category: "Algorithms",
definition: "Carry out the process the statement describes, step by step; the work is modelling the state faithfully.",
},
sorting: {
name: "Sorting",
icon: "arrow-down-wide-narrow",
category: "Algorithms",
definition: "Order the input first so a later step, such as two pointers, a greedy choice, or binary search, becomes possible.",
},
"tournament-sort": {
name: "Tournament Sort",
icon: "trophy",
category: "Algorithms",
definition: "A winner tree of pairwise comparisons: the root is the extreme element, and replaying one path yields the next in O(log n).",
},
// ── techniques ───────────────────────────────────────────────────
combinatorics: {
name: "Combinatorics",
icon: "dices",
category: "Techniques",
definition: "Count arrangements and selections (permutations, combinations, Catalan numbers), usually modulo a prime.",
},
counting: {
name: "Counting",
icon: "tally-5",
category: "Techniques",
definition: "Tally occurrences in a hash map or fixed array, then answer from the counts instead of the raw input.",
},
design: {
name: "Design",
icon: "pencil-ruler",
category: "Techniques",
definition: "Implement a class to an API spec, choosing the internals so every operation meets its time bound.",
},
geometry: {
name: "Geometry",
icon: "shapes",
category: "Techniques",
definition: "Points, lines, and areas on a plane or grid: distances, slopes, overlaps, and orientation by cross product.",
},
math: {
name: "Math",
icon: "calculator",
category: "Techniques",
definition: "An arithmetic or number-theory insight (modular arithmetic, GCD, digit manipulation, a closed form) in place of brute force.",
},
};
+137
View File
@@ -0,0 +1,137 @@
// Written by `blume eject`. This is your Astro config now: edit it freely.
import { availableParallelism } from "node:os";
import { fileURLToPath } from "node:url";
import { defineConfig, fontProviders } from "astro/config";
import mdx from "@astrojs/mdx";
import tailwindcss from "@tailwindcss/vite";
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
import react from "@astrojs/react";
import { blumeIntegration, includeHmrPlugin, mdxSourceErrorsPlugin, prerenderDepsPlugin } from "blume/astro";
export default defineConfig({
root: ".",
srcDir: "./src",
outDir: "./dist",
publicDir: "./public",
output: "static",
site: "https://lc.prd.lk",
redirects: {"/1":{"destination":"/hash-table/two-sum","status":301},"/1.md":{"destination":"/hash-table/two-sum.md","status":301},"/1.mdx":{"destination":"/hash-table/two-sum.mdx","status":301},"/2":{"destination":"/hybrid-structures/add-two-numbers","status":301},"/2.md":{"destination":"/hybrid-structures/add-two-numbers.md","status":301},"/2.mdx":{"destination":"/hybrid-structures/add-two-numbers.mdx","status":301},"/3":{"destination":"/sliding-window/longest-substring-without-repeating-characters","status":301},"/3.md":{"destination":"/sliding-window/longest-substring-without-repeating-characters.md","status":301},"/3.mdx":{"destination":"/sliding-window/longest-substring-without-repeating-characters.mdx","status":301},"/4":{"destination":"/binary-search/median-of-two-sorted-arrays","status":301},"/4.md":{"destination":"/binary-search/median-of-two-sorted-arrays.md","status":301},"/4.mdx":{"destination":"/binary-search/median-of-two-sorted-arrays.mdx","status":301},"/11":{"destination":"/two-pointers/container-with-most-water","status":301},"/11.md":{"destination":"/two-pointers/container-with-most-water.md","status":301},"/11.mdx":{"destination":"/two-pointers/container-with-most-water.mdx","status":301},"/15":{"destination":"/two-pointers/3sum","status":301},"/15.md":{"destination":"/two-pointers/3sum.md","status":301},"/15.mdx":{"destination":"/two-pointers/3sum.mdx","status":301},"/19":{"destination":"/linked-list/remove-nth-node-from-end-of-list","status":301},"/19.md":{"destination":"/linked-list/remove-nth-node-from-end-of-list.md","status":301},"/19.mdx":{"destination":"/linked-list/remove-nth-node-from-end-of-list.mdx","status":301},"/20":{"destination":"/stack/valid-parentheses","status":301},"/20.md":{"destination":"/stack/valid-parentheses.md","status":301},"/20.mdx":{"destination":"/stack/valid-parentheses.mdx","status":301},"/21":{"destination":"/linked-list/merge-two-sorted-lists","status":301},"/21.md":{"destination":"/linked-list/merge-two-sorted-lists.md","status":301},"/21.mdx":{"destination":"/linked-list/merge-two-sorted-lists.mdx","status":301},"/23":{"destination":"/heap-priority-queue/merge-k-sorted-lists","status":301},"/23.md":{"destination":"/heap-priority-queue/merge-k-sorted-lists.md","status":301},"/23.mdx":{"destination":"/heap-priority-queue/merge-k-sorted-lists.mdx","status":301},"/33":{"destination":"/binary-search/search-in-rotated-sorted-array","status":301},"/33.md":{"destination":"/binary-search/search-in-rotated-sorted-array.md","status":301},"/33.mdx":{"destination":"/binary-search/search-in-rotated-sorted-array.mdx","status":301},"/34":{"destination":"/binary-search/find-first-and-last-position-of-element-in-sorted-array","status":301},"/34.md":{"destination":"/binary-search/find-first-and-last-position-of-element-in-sorted-array.md","status":301},"/34.mdx":{"destination":"/binary-search/find-first-and-last-position-of-element-in-sorted-array.mdx","status":301},"/35":{"destination":"/binary-search/search-insert-position","status":301},"/35.md":{"destination":"/binary-search/search-insert-position.md","status":301},"/35.mdx":{"destination":"/binary-search/search-insert-position.mdx","status":301},"/36":{"destination":"/matrix-index-math/valid-sudoku","status":301},"/36.md":{"destination":"/matrix-index-math/valid-sudoku.md","status":301},"/36.mdx":{"destination":"/matrix-index-math/valid-sudoku.mdx","status":301},"/39":{"destination":"/backtracking/combination-sum","status":301},"/39.md":{"destination":"/backtracking/combination-sum.md","status":301},"/39.mdx":{"destination":"/backtracking/combination-sum.mdx","status":301},"/42":{"destination":"/two-pointers/trapping-rain-water","status":301},"/42.md":{"destination":"/two-pointers/trapping-rain-water.md","status":301},"/42.mdx":{"destination":"/two-pointers/trapping-rain-water.mdx","status":301},"/46":{"destination":"/backtracking/permutations","status":301},"/46.md":{"destination":"/backtracking/permutations.md","status":301},"/46.mdx":{"destination":"/backtracking/permutations.mdx","status":301},"/48":{"destination":"/matrix-index-math/rotate-image","status":301},"/48.md":{"destination":"/matrix-index-math/rotate-image.md","status":301},"/48.mdx":{"destination":"/matrix-index-math/rotate-image.mdx","status":301},"/49":{"destination":"/hash-table/group-anagrams","status":301},"/49.md":{"destination":"/hash-table/group-anagrams.md","status":301},"/49.mdx":{"destination":"/hash-table/group-anagrams.mdx","status":301},"/53":{"destination":"/greedy/maximum-subarray","status":301},"/53.md":{"destination":"/greedy/maximum-subarray.md","status":301},"/53.mdx":{"destination":"/greedy/maximum-subarray.mdx","status":301},"/54":{"destination":"/matrix-index-math/spiral-matrix","status":301},"/54.md":{"destination":"/matrix-index-math/spiral-matrix.md","status":301},"/54.mdx":{"destination":"/matrix-index-math/spiral-matrix.mdx","status":301},"/55":{"destination":"/greedy/jump-game","status":301},"/55.md":{"destination":"Line truncated
fonts: [{ provider: fontProviders.local(), name: "Inter", cssVariable: "--blume-ff-inter", fallbacks: ["ui-sans-serif","system-ui","sans-serif"], options: { variants: [{"weight":"100..900","src":["node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2"]}] } }, { provider: fontProviders.local(), name: "IBM Plex Mono", cssVariable: "--blume-ff-ibm-plex-mono", fallbacks: ["ui-monospace","SF Mono","Menlo","monospace"], options: { variants: [{"weight":400,"src":["node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2"]},{"weight":500,"src":["node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2"]}] } }],
integrations: [mdx({ processor: blumeMdxProcessor({ ...{"basePath":"","codeThemes":{"dark":"github-dark","light":"github-light"},"contentRoot":"content","deployBase":"","externalLinks":false,"headingAnchors":true,"variables":{}}, dataFile: fileURLToPath(new URL("./src/generated/data.json", import.meta.url)) }) }), react({ exclude: [/\/\.cache\/vite\//] }), blumeIntegration({"pages":[{"entrypoint":"pages/appendix.astro","pattern":"/appendix"},{"entrypoint":"pages/changelog.astro","pattern":"/changelog"},{"entrypoint":"pages/progress.astro","pattern":"/progress"}],"poweredBy":true,"redirects":[["^/1/?$","/hash-table/two-sum",301],["^/1\\.md/?$","/hash-table/two-sum.md",301],["^/1\\.mdx/?$","/hash-table/two-sum.mdx",301],["^/2/?$","/hybrid-structures/add-two-numbers",301],["^/2\\.md/?$","/hybrid-structures/add-two-numbers.md",301],["^/2\\.mdx/?$","/hybrid-structures/add-two-numbers.mdx",301],["^/3/?$","/sliding-window/longest-substring-without-repeating-characters",301],["^/3\\.md/?$","/sliding-window/longest-substring-without-repeating-characters.md",301],["^/3\\.mdx/?$","/sliding-window/longest-substring-without-repeating-characters.mdx",301],["^/4/?$","/binary-search/median-of-two-sorted-arrays",301],["^/4\\.md/?$","/binary-search/median-of-two-sorted-arrays.md",301],["^/4\\.mdx/?$","/binary-search/median-of-two-sorted-arrays.mdx",301],["^/11/?$","/two-pointers/container-with-most-water",301],["^/11\\.md/?$","/two-pointers/container-with-most-water.md",301],["^/11\\.mdx/?$","/two-pointers/container-with-most-water.mdx",301],["^/15/?$","/two-pointers/3sum",301],["^/15\\.md/?$","/two-pointers/3sum.md",301],["^/15\\.mdx/?$","/two-pointers/3sum.mdx",301],["^/19/?$","/linked-list/remove-nth-node-from-end-of-list",301],["^/19\\.md/?$","/linked-list/remove-nth-node-from-end-of-list.md",301],["^/19\\.mdx/?$","/linked-list/remove-nth-node-from-end-of-list.mdx",301],["^/20/?$","/stack/valid-parentheses",301],["^/20\\.md/?$","/stack/valid-parentheses.md",301],["^/20\\.mdx/?$","/stack/valid-parentheses.mdx",301],["^/21/?$","/linked-list/merge-two-sorted-lists",301],["^/21\\.md/?$","/linked-list/merge-two-sorted-lists.md",301],["^/21\\.mdx/?$","/linked-list/merge-two-sorted-lists.mdx",301],["^/23/?$","/heap-priority-queue/merge-k-sorted-lists",301],["^/23\\.md/?$","/heap-priority-queue/merge-k-sorted-lists.md",301],["^/23\\.mdx/?$","/heap-priority-queue/merge-k-sorted-lists.mdx",301],["^/33/?$","/binary-search/search-in-rotated-sorted-array",301],["^/33\\.md/?$","/binary-search/search-in-rotated-sorted-array.md",301],["^/33\\.mdx/?$","/binary-search/search-in-rotated-sorted-array.mdx",301],["^/34/?$","/binary-search/find-first-and-last-position-of-element-in-sorted-array",301],["^/34\\.md/?$","/binary-search/find-first-and-last-position-of-element-in-sorted-array.md",301],["^/34\\.mdx/?$","/binary-search/find-first-and-last-position-of-element-in-sorted-array.mdx",301],["^/35/?$","/binary-search/search-insert-position",301],["^/35\\.md/?$","/binary-search/search-insert-position.md",301],["^/35\\.mdx/?$","/binary-search/search-insert-position.mdx",301],["^/36/?$","/matrix-index-math/valid-sudoku",301],["^/36\\.md/?$","/matrix-index-math/valid-sudoku.md",301],["^/36\\.mdx/?$","/matrix-index-math/valid-sudoku.mdx",301],["^/39/?$","/backtracking/combination-sum",301],["^/39\\.md/?$","/backtracking/combination-sum.md",301],["^/39\\.mdx/?$","/backtracking/combination-sum.mdx",301],["^/42/?$","/two-pointers/trapping-rain-water",301],["^/42\\.md/?$","/two-pointers/trapping-rain-water.md",301],["^/42\\.mdx/?$","/two-pointers/trapping-rain-water.mdx",301],["^/46/?$","/backtracking/permutations",301],["^/46\\.md/?$","/backtracking/permutations.md",301],["^/46\\.mdx/?$","/backtracking/permutations.mdx",301],["^/48/?$","/matrix-index-math/rotate-image",301],["^/48\\.md/?$","/matrix-index-math/rotate-image.md",301],["^/48\\.mdx/?$","/matrix-index-math/rotate-image.mdx",301],["^/49/?$","/hash-table/group-anagrams",301],["^/49\\.md/?$","/hash-table/group-anagrams.md",301],["^/49\\.mdx/?$","/hash-table/group-anagrams.mdx",301],["^/53/?$","/greedy/maximum-subarray",301],["^/53\\.md/?$","/greedy/maximum-subarray.md",301],["^/53\\.mdx/?$","/greedy/maximum-subarray.mdx",301],["^/54/?$","/matrix-index-math/spiral-matrix",301],["^/54\\.md/?$","/matrix-index-math/spiral-matrix.md",301],["^/54\\.mdx/?$","/matrix-index-math/spiral-matrix.mdx",301],["^/55/?$","/greedy/jump-game",301],["^/55\\.md/?$","/greedy/jump-game.md",301],["^/55\\.mdx/?$","/greedy/jump-game.mdx",301],["^/56/?$","/intervals/merge-intervals",301],["^/56\\.md/?$","/intervals/merge-intervals.md",301],["^/56\\.mdx/?$","/intervals/merge-intervals.mdx",301],["^/57/?$","/intervals/insert-interval",301],["^/57\\.md/?$","/intervals/insert-interval.md",301],["^/57\\.mdx/?$","/intervals/insert-interval.mdx",301],["^/62/?$","/multi-d-grid-dp/unique-paths",301],["^/62\\.md/?$","/multi-d-grid-dp/unique-paths.md",301],["^/62\\.mdx/?$","/multi-d-grid-dp/unique-paths.mdx",301],["^/64/?$","Line truncated
markdown: {
processor: blumeMarkdownProcessor({ ...{"basePath":"","codeThemes":{"dark":"github-dark","light":"github-light"},"contentRoot":"content","deployBase":"","externalLinks":false,"headingAnchors":true,"variables":{}}, dataFile: fileURLToPath(new URL("./src/generated/data.json", import.meta.url)) }),
shikiConfig: {
themes: {
light: "github-light",
dark: "github-dark",
},
defaultColor: false,
transformers: [blumeTwoslashTransformer(), ...blumeShikiTransformers({"icons":true})],
},
},
devToolbar: { enabled: false },
// One canonical URL per page: canonicals, the sitemap, and hreflang all use
// the slashless form, so the slashed spelling is not a second address. Astro
// applies this itself — an on-demand route redirects, and the integration
// has the dev server redirect too, ahead of Astro's own 404 — and the Vercel
// adapter turns it into the platform's 308 route, so the Build Output config
// needs no hand-spliced redirect (see deploy/vercel-negotiation.ts). Static hosts
// serve the `index.html` directory layout as they always did.
trailingSlash: "never",
// The layouts render Astro's <ClientRouter />, and its in-place swaps read
// from the prefetch cache — fetching every link on hover/viewport hides the
// request latency behind the user's intent, so most navigations swap
// instantly.
prefetch: { prefetchAll: true },
// Prerender several pages at once. Rendering is single-threaded, but a
// page's OG card renders on a native thread and its HTML is written
// asynchronously, so the main thread would otherwise idle behind each
// page's off-thread work. Capped at 8: the gain flattens there and beyond it
// the overlap only adds memory.
build: { concurrency: Math.min(8, availableParallelism()) },
vite: {
plugins: [tailwindcss(), includeHmrPlugin(fileURLToPath(new URL("./src/generated/includes.json", import.meta.url))), mdxSourceErrorsPlugin(), prerenderDepsPlugin()],
build: {
chunkSizeWarningLimit: 2048,
rolldownOptions: {
onLog(level, log, handler) {
if (log.code === "MODULE_LEVEL_DIRECTIVE" && String(log.message).includes("use astro:head-inject")) {
return;
}
handler(level, log);
},
},
},
// Everything hydration can reach must be part of the dev dep optimizer's
// FIRST run. The Vite root is the generated runtime, so user pages,
// islands, and aliased components live outside it and are only crawled
// when first requested — and `react/compiler-runtime` is injected by the
// React Compiler, so no source scan can ever see it. A dependency discovered after
// hydration begins triggers a mid-session re-optimization whose new
// generation imports React through new `?v=` URLs; the browser then
// evaluates a second React copy and every island tears down with
// "Invalid hook call" (#157). `entries` points the startup scanner at
// the user's files (the scanner follows their imports, so their deps land
// in the initial optimization); the compiler runtime rides the include
// list because only the transform pipeline knows it exists.
//
// The mermaid/epub includes fix CJS interop instead: both lazy client-side
// imports land on CJS/UMD files (mermaid statically imports dayjs as CJS,
// epub-gen-memory's browser bundle is a browserified UMD) that break when
// served as raw ESM — mermaid throws on load and the EPUB export throws
// `epub is not a function`. They resolve through the `blume` package
// (they aren't direct deps of the generated project), so the nested
// `blume > x` form is required, and epub-gen-memory must name the
// `/bundle` subpath that is actually imported: optimizing the package
// root leaves that entry out. Production (Rollup) already handles the
// interop, so all of this only affects dev.
//
// The search adapter's client library rides the list for the same reason
// as the compiler runtime: the search dialog lazy-imports it on first
// open, and discovering it then re-optimizes and reloads the page.
optimizeDeps: {
entries: ["./pages/**/*.astro","./islands/**/*.{jsx,svelte,tsx,vue}"],
include: ["blume > mermaid","blume > @orama/orama"],
},
// Blume's render-time deps are forced external on both build environments so
// native bindings resolve at runtime and isolated linkers don't bundle
// symlinked store copies (which would surface their children as unresolvable
// imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
//
// The SSR externals go through the legacy `ssr.external` key rather than
// `environments.ssr`: defining a user-owned `environments.ssr` block
// collides with the internal environment Astro 7 builds the server under and
// detaches the adapter's server entrypoint from the rolldown input, so the
// SSR entry is emitted as `index.mjs` instead of the `entry.mjs` the
// Vercel adapter's `astro:build:done` hook then fails to find. `prerender`
// is Astro-only and has no legacy equivalent, so it stays under `environments`.
ssr: { external: ["@astrojs/markdown-satteri","@pierre/diffs","@shikijs/transformers","@takumi-rs/core","@takumi-rs/helpers","@takumi-rs/wasm","github-slugger","katex","shiki","simple-icons","takumi-js","takumi-js/helpers","zod"] },
environments: {
prerender: { resolve: { external: ["@astrojs/markdown-satteri","@pierre/diffs","@shikijs/transformers","@takumi-rs/core","@takumi-rs/helpers","@takumi-rs/wasm","github-slugger","katex","shiki","simple-icons","takumi-js","takumi-js/helpers","zod"] } },
},
resolve: {
alias: {
"blume:ask": fileURLToPath(new URL("./src/generated/Ask.astro", import.meta.url)),
"blume:consent-client": fileURLToPath(new URL("./src/generated/consent-client.ts", import.meta.url)),
"blume:examples": fileURLToPath(new URL("./src/generated/examples.ts", import.meta.url)),
"blume:examples-theme": fileURLToPath(new URL("./src/generated/examples.css", import.meta.url)),
"blume:features": fileURLToPath(new URL("./src/generated/features.ts", import.meta.url)),
"blume:search-client": fileURLToPath(new URL("./src/generated/search-client.ts", import.meta.url)),
"blume:theme": fileURLToPath(new URL("./src/generated/app.css", import.meta.url)),
"blume:ask-data": fileURLToPath(new URL("./src/generated/ask-data.json", import.meta.url)),
"blume:content-assets": fileURLToPath(new URL("./src/generated/content-assets.json", import.meta.url)),
"blume:data": fileURLToPath(new URL("./src/generated/data.json", import.meta.url)),
"blume:mcp-data": fileURLToPath(new URL("./src/generated/mcp-data.json", import.meta.url)),
"blume:openapi": fileURLToPath(new URL("./src/generated/openapi.json", import.meta.url)),
"blume:raw-markdown": fileURLToPath(new URL("./src/generated/raw-markdown.json", import.meta.url)),
"blume:rss": fileURLToPath(new URL("./src/generated/rss.json", import.meta.url)),
"blume:search-index": fileURLToPath(new URL("./src/generated/search.json", import.meta.url)),
},
},
server: {
fs: {
allow: ["."],
},
},
},
});
+168
View File
@@ -0,0 +1,168 @@
import { defineConfig } from "blume";
import type { StandardSchema } from "blume/core/standard-schema.ts";
import { filesystem } from "blume/sources";
import { APPENDIX } from "./appendix";
import { RECENCY, type Company } from "./companies";
import redirects from "./redirects";
// 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
// local files, keep the build network-free. Blume resolves `src` from this
// directory; the workspace links these packages under ./node_modules.
const inter = {
name: "Inter",
variants: [
{
src: "./node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2",
weight: "100..900",
},
],
};
const plexMono = {
name: "IBM Plex Mono",
variants: [
{
src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2",
weight: 400,
},
{
src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2",
weight: 500,
},
],
};
/**
* Custom frontmatter, rendered between the description and the body by
* components/ProblemBadges.astro. Problem pages: `topics:` is LeetCode's topic
* tags, `issue:` the problem's GitHub issue URL and `companies:` the companies
* that asked it in the last six months (`companies.ts`; the table under the
* badge row) — all three written by apps/cli/sync.ts — and `video:` the
* NeetCode walkthrough's YouTube id (apps/cli/videos.ts). 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 issueSchema = optional(
"expected a https://github.com/<owner>/<repo>/issues/<n> URL",
(value): value is string =>
typeof value === "string" &&
/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/issues\/\d+$/.test(value),
);
const companiesSchema = optional(
`expected a list of { name, period, frequency } with period one of ${RECENCY.join(" | ")}`,
(value): value is Company[] =>
Array.isArray(value) &&
value.every(
(c) =>
typeof c?.name === "string" &&
RECENCY.includes(c?.period) &&
typeof c?.frequency === "number",
),
);
export default defineConfig({
title: "prdlk",
description: "Documentation for my LeetCode solutions",
logo: "./public/logo.svg",
deployment: {
site: "https://lc.prd.lk",
},
github: {
owner: "prdlk",
repo: "leetcode",
},
lastModified: "git",
// Sync's `/<num>` short links, then the Appendix's former per-term pages
// (`/appendix/<slug>`, published until the glossary became one page) onto
// their cards, so old links and search results still land.
redirects: [
...redirects,
...Object.keys(APPENDIX).map((slug) => ({
from: `/appendix/${slug}`,
to: `/appendix#${slug}`,
})),
],
frontmatter: {
extend: {
topics: topicsSchema,
video: videoSchema,
issue: issueSchema,
companies: companiesSchema,
},
},
seo: {
og: { enabled: true },
// No `rss`: the changelog is one custom page (pages/changelog.astro), not
// `type: changelog` entries, so a changelog feed would have nothing in it.
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 },
},
// The only content source. `/changelog`, `/appendix` and `/progress` are
// custom pages (pages/*.astro): `/changelog` reads the GitHub releases
// itself — one timeline, no per-release pages — `/appendix` is one glossary
// page built from appendix.ts, and `/progress` is the live stats cards, so
// none enters the sidebar tree or prev/next.
content: {
sources: [filesystem({ root: "content" })],
},
theme: {
fonts: { display: inter, body: inter, mono: plexMono },
},
navigation: {
repo: "https://github.com/prdlk/leetcode",
sidebar: {
display: "flat", // "flat" | "group" | "page"
},
// The custom pages (pages/*.astro) as header tabs. None is a content
// folder, so the tabs scope nothing out of the sidebar tree.
tabs: [
{ label: "Changelog", path: "/changelog" },
{ label: "Progress", path: "/progress" },
{ label: "Appendix", path: "/appendix" },
],
},
});
+36
View File
@@ -0,0 +1,36 @@
/**
* The `companies:` frontmatter vocabulary, shared by its writer
* (`apps/cli/sync.ts`, from the local DuckDB company-frequency database), its
* schema (`blume.config.ts`) and its two readers: the company table under a
* problem page's badge row (`components/ProblemBadges.astro`) and the
* Companies section of the Appendix (`pages/appendix.astro`), where each
* company's card — anchored at `companyHref()` — lists the problems it asked.
* Plain data, no Astro imports: sync imports this file under Bun.
*/
/** The database's recency windows, most recent first; they nest, so a company is filed under the first it appears in. */
export const RECENCY = ["Thirty Days", "Three Months", "Six Months"] as const;
export type Recency = (typeof RECENCY)[number];
/** Read under an "Asked within" heading (the company table) or as a row chip (the Appendix card). */
export const RECENCY_LABEL: Record<Recency, string> = {
"Thirty Days": "30 days",
"Three Months": "3 months",
"Six Months": "6 months",
};
export interface Company {
name: string;
/** The most recent window the company asked the problem in. */
period: Recency;
/** 5–100, relative within the company and window: 100 is its most-asked problem there. */
frequency: number;
}
/** The company's card on the Appendix; `company-` keeps it clear of the tag cards' `#<slug>` ids. */
export function companyHref(name: string): string {
return `/appendix#company-${name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")}`;
}
+21
View File
@@ -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"
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* 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";
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 pages' "what links here": every problem pointing at the page.
PageFooter: "./components/BacklinksList.astro",
},
});
+95
View File
@@ -0,0 +1,95 @@
---
/**
* The `PageFooter` slot: an Obsidian-style backlinks list under a topic landing
* page — every problem page that points at it, as `<num> <name>` rows in
* LeetCode-number order. Pages nothing points at (problem pages, the
* overview) render nothing; a tag no topic teaches lists its problems on its
* Appendix card instead (`pages/appendix.astro`), from the same answer.
*
* 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>
)
}
+170
View File
@@ -0,0 +1,170 @@
---
/**
* A horizontal carousel of <CarouselImage> slides, built the way Blume's own
* content components are: static markup, a custom element for the behaviour,
* no framework. The track is a CSS scroll-snap strip (so it also swipes on
* touch and the browser does the animation); prev/next sit outside it and
* the play toggle sits below with a "n / N" counter.
*
* `time` is seconds per slide while playing (default 1.25); `autoplay` starts
* playing on load. Play wraps around, pauses whenever the tab is hidden, and
* never starts on its own for a reader who prefers reduced motion — the
* button still works for them. Manual navigation (buttons, arrow keys, swipe)
* does not stop playback; it only restarts the interval so a step is never
* doubled.
*/
import Icon from "blume/components/Icon.astro";
interface Props {
/** Seconds each slide stays while playing. */
time?: number;
autoplay?: boolean;
}
const { time = 1.25, autoplay = false } = Astro.props;
const buttonClass =
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border bg-background text-muted-foreground transition-colors hover:border-foreground hover:text-foreground disabled:cursor-default disabled:opacity-40";
---
<blume-carousel
class="not-prose my-6 block"
data-time={String(time)}
data-autoplay={autoplay ? "" : undefined}
role="region"
aria-roledescription="carousel"
>
<div class="flex items-center gap-3">
<button class={buttonClass} data-prev type="button" aria-label="Previous slide">
<Icon class="rtl:-scale-x-100" name="chevron-left" size={18} />
</button>
<div
class="flex min-w-0 flex-1 snap-x snap-mandatory overflow-x-auto scroll-smooth rounded-blume [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
data-track
tabindex="0"
aria-live="polite"
>
<slot />
</div>
<button class={buttonClass} data-next type="button" aria-label="Next slide">
<Icon class="rtl:-scale-x-100" name="chevron-right" size={18} />
</button>
</div>
<div class="mt-3 flex items-center justify-center gap-3">
<button class={buttonClass} data-play type="button" aria-label="Play" aria-pressed="false">
<Icon class="[[aria-pressed=true]_&]:hidden" name="play" size={16} />
<Icon class="[[aria-pressed=false]_&]:hidden" name="pause" size={16} />
</button>
<span class="text-muted-foreground text-xs tabular-nums leading-5" data-count></span>
</div>
</blume-carousel>
<script>
class BlumeCarousel extends HTMLElement {
#track!: HTMLElement;
#play!: HTMLButtonElement;
#count!: HTMLElement;
#timer: number | null = null;
#index = 0;
connectedCallback() {
this.#track = this.querySelector("[data-track]")!;
this.#play = this.querySelector("[data-play]")!;
this.#count = this.querySelector("[data-count]")!;
const prev = this.querySelector<HTMLButtonElement>("[data-prev]")!;
const next = this.querySelector<HTMLButtonElement>("[data-next]")!;
const slides = this.#slides().length;
if (slides < 2) {
for (const b of [prev, next, this.#play]) b.disabled = true;
}
this.#render();
prev.addEventListener("click", () => this.#step(-1));
next.addEventListener("click", () => this.#step(1));
this.#play.addEventListener("click", () => (this.#timer === null ? this.#start() : this.#stop()));
this.addEventListener("keydown", (event) => {
if (event.key === "ArrowLeft") this.#step(-1);
else if (event.key === "ArrowRight") this.#step(1);
else return;
event.preventDefault();
});
// Swipes and trackpad scrolls move the track without #step; follow them
// once the scroll settles. Settle-only, not per event: a long smooth
// scroll from #step passes over intermediate slides and must not be
// read back as the reader's position mid-flight.
let settle: number | undefined;
this.#track.addEventListener(
"scroll",
() => {
window.clearTimeout(settle);
settle = window.setTimeout(() => this.#syncFromScroll(), 150);
},
{ passive: true }
);
document.addEventListener("visibilitychange", this.#onVisibility);
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
if (this.hasAttribute("data-autoplay") && slides > 1 && !reduced) this.#start();
}
disconnectedCallback() {
this.#stop();
document.removeEventListener("visibilitychange", this.#onVisibility);
}
#slides(): HTMLElement[] {
return [...this.#track.querySelectorAll<HTMLElement>(":scope > blume-carousel-image")];
}
#step(delta: number) {
const slides = this.#slides();
this.#index = (this.#index + delta + slides.length) % slides.length;
slides[this.#index]!.scrollIntoView({ block: "nearest", inline: "start", behavior: "smooth" });
this.#render();
if (this.#timer !== null) this.#start(); // restart the interval from this slide
}
#syncFromScroll() {
const at = Math.round(this.#track.scrollLeft / this.#track.clientWidth);
if (at !== this.#index && Number.isFinite(at)) {
this.#index = Math.min(Math.max(at, 0), this.#slides().length - 1);
this.#render();
}
}
#start() {
this.#stop();
const ms = Number(this.dataset.time) * 1000;
this.#timer = window.setInterval(() => this.#step(1), ms > 0 ? ms : 1250);
this.#play.setAttribute("aria-pressed", "true");
this.#play.setAttribute("aria-label", "Pause");
}
#stop() {
if (this.#timer !== null) window.clearInterval(this.#timer);
this.#timer = null;
this.#play.setAttribute("aria-pressed", "false");
this.#play.setAttribute("aria-label", "Play");
}
// A background tab keeps its timers but not its layout; resume on return.
#wasPlaying = false;
#onVisibility = () => {
if (document.hidden) {
this.#wasPlaying = this.#timer !== null;
this.#stop();
} else if (this.#wasPlaying) this.#start();
};
#render() {
const slides = this.#slides();
this.#count.textContent = `${this.#index + 1} / ${slides.length}`;
slides.forEach((s, i) => s.setAttribute("aria-hidden", i === this.#index ? "false" : "true"));
}
}
if (!customElements.get("blume-carousel")) {
customElements.define("blume-carousel", BlumeCarousel);
}
</script>
+45
View File
@@ -0,0 +1,45 @@
---
/**
* One slide of a <Carousel>: Blume's own <Frame> around an <img>, so a slide
* is pixel-identical to a framed image elsewhere on the site — `alt` doubles
* as the frame's hint line and `caption` is Markdown, exactly as in Frame.
*
* `src` accepts what MDX authors already write for images: a URL, a
* root-relative `/file.png` (served from public/, prefixed with the deploy
* base), or the page-relative `../../public/file.png` form. A relative path
* that does not pass through public/ has nothing to serve it, so it is a
* build error rather than a silent 404.
*/
import Frame from "blume/components/content/Frame.astro";
import { withBase } from "blume/components/islands/base-path.ts";
interface Props {
src: string;
alt?: string;
caption?: string;
}
const { src, alt, caption } = Astro.props;
const resolveSrc = (value: string): string => {
if (URL.canParse(value)) return value;
if (value.startsWith("/")) return withBase(value);
const at = value.indexOf("public/");
if (at === -1) {
throw new Error(
`<CarouselImage src="${value}">: a relative src must point into public/ (or use /file.png)`
);
}
return withBase(`/${value.slice(at + "public/".length)}`);
};
---
<blume-carousel-image
class="block w-full shrink-0 snap-center [&_blume-frame]:my-0"
role="group"
aria-roledescription="slide"
>
<Frame caption={caption} hint={alt}>
<img alt={alt ?? ""} src={resolveSrc(src)} loading="lazy" draggable="false" />
</Frame>
</blume-carousel-image>
+175
View File
@@ -0,0 +1,175 @@
---
/**
* The MDX `wrapper`: the badge row (and, below it, the company table) between
* a page's description and its body.
* Blume's page template renders the title and description itself and then
* the MDX content, with no slot in between, so wrapping the content is the
* only way to land right after the description. MDX hands a wrapper no
* route, so it is read off the URL, and the page's frontmatter is then
* looked up through the route manifest — the same `getEntry` call Blume's own
* page template makes. Pages with none of the keys below (landing pages, the
* changelog) render only their content.
*
* - `topics:` (problem pages, written by `apps/cli/sync.ts`) is LeetCode's own
* topic tags. A tag the campaign teaches (`topic-landings.ts`) links to that
* topic's landing page and is filled; any other tag links to its card on the
* Appendix glossary, `/appendix#<slug>` (`appendix.ts`), and is outlined.
* `tagTarget()` picks the route (and between two landings, e.g. DFS); the
* `PageFooter` backlinks list and the Appendix page read the same answer.
* Each tag badge is a pill carrying the icon its target shows (a topic's
* `meta.ts` icon in the sidebar, a term's `icon` on its card) and the tag's
* own colour (`lib/tag-colors.ts`), assigned over every tag the site has a
* page or card for — no two tags share one, and tags that share problem
* pages are spread furthest apart. A tag with neither (the LeetCode-list
* fallback) stays gray and iconless.
* - `video:` (problem pages, written by `apps/cli/videos.ts`) is the NeetCode
* walkthrough's YouTube id, the red "Explanation" badge.
* - `issue:` (problem pages, written by sync) is the problem's GitHub issue
* URL, a neutral "Issue #N" badge with the GitHub mark, right after
* "Explanation" at the end of the row.
* - `companies:` (problem pages, written by sync; `companies.ts`) is the
* companies that asked the problem in the last six months, a table laid
* out like Blume's TypeTable under the badge row: each company a pill
* linking to its card in the Appendix's Companies section, the most recent
* window it asked in, and its frequency there (relative within that
* company and window, so a bar of 100 is its most-asked problem).
*/
import { getEntry } from "astro:content";
import data from "blume:data";
import Badge from "blume/components/content/Badge.astro";
import { stripBase, withBase } from "blume/components/islands/base-path.ts";
import { APPENDIX } from "../appendix.ts";
import { RECENCY_LABEL, companyHref, type Company } from "../companies.ts";
import { tagColors } from "../lib/tag-colors.ts";
import { TOPIC_LANDINGS, tagTarget, type TopicTag } from "../topic-landings.ts";
// `/sub/tree-dfs/invert-binary-tree/` (or `.../index.html` in a static build)
// -> `/tree-dfs/invert-binary-tree`, the base-less shape of `data.routes`.
const route =
stripBase(import.meta.env.BASE_URL ?? "/", Astro.url.pathname)
.replace(/(\/index)?\.html$/u, "")
.replace(/(.)\/+$/u, "$1");
const at = data.routes.find((r: { path: string }) => r.path === route);
const entry = at ? await getEntry(at.collection, at.entryId) : undefined;
const tags: TopicTag[] = entry?.data.topics ?? [];
const video: string | undefined = entry?.data.video;
const issue: string | undefined = entry?.data.issue;
const companies: Company[] = entry?.data.companies ?? [];
const served = new Set<string>(data.routes.map((r: { path: string }) => r.path));
// route → the icon its sidebar row shows: a page's own, or a topic group's
// (from its `meta.ts`) for the landing page the group row links to.
interface NavNode {
route?: string;
icon?: string;
children?: NavNode[];
}
const icons = new Map<string, string>();
const walk = (nodes: NavNode[]) => {
for (const node of nodes) {
if (node.route && node.icon && !icons.has(node.route)) icons.set(node.route, node.icon);
if (node.children) walk(node.children);
}
};
walk(data.navigation.sidebar);
// Every tag with a landing or an Appendix card, coloured against how problem
// pages (`<num>. <name>` titles, the shape sync writes) combine them. Only a
// page with tags needs the answer.
const colors = tags.length
? tagColors(
[...Object.keys(TOPIC_LANDINGS), ...Object.keys(APPENDIX)],
await Promise.all(
data.routes
.filter((r: { title: string }) => /^\d+\.\s/u.test(r.title))
.map(async (r: { collection: string; entryId: string }) => {
const page = await getEntry(r.collection, r.entryId);
return (page?.data.topics ?? []).map((t: TopicTag) => t.slug);
}),
),
)
: new Map<string, string>();
const badges = tags.map((tag) => {
const target = tagTarget(tag, { route, tags }, served);
const external = target.kind === "external";
return {
name: tag.name,
href: external ? target.href : withBase(target.href),
external,
icon: target.kind === "appendix" ? APPENDIX[tag.slug]?.icon : external ? undefined : icons.get(target.href),
color: external ? undefined : colors.get(tag.slug),
stroke: target.kind !== "landing",
};
});
const linkClass = "no-underline transition-opacity hover:opacity-80";
// TypeTable's column track, so the header and every row align.
const columns = "grid grid-cols-[2fr_1fr_auto] items-center gap-4";
---
{
(badges.length > 0 || video || issue) && (
<div class="not-prose mb-6 flex flex-wrap gap-1.5">
{badges.map((b) => (
<a
href={b.href}
target={b.external ? "_blank" : undefined}
rel={b.external ? "noopener" : undefined}
class={linkClass}
>
<Badge color={b.color} icon={b.icon} shape="pill" stroke={b.stroke}>
{b.name}
</Badge>
</a>
))}
{video && (
<a href={`https://www.youtube.com/watch?v=${video}`} target="_blank" rel="noopener" class={linkClass}>
<Badge variant="danger" icon="youtube" shape="pill">
Explanation
</Badge>
</a>
)}
{issue && (
<a href={issue} target="_blank" rel="noopener" class={linkClass}>
<Badge icon="github" shape="pill">
Issue #{issue.slice(issue.lastIndexOf("/") + 1)}
</Badge>
</a>
)}
</div>
)
}
{
companies.length > 0 && (
<div class="not-prose mb-6 rounded-blume border border-border bg-muted/30 p-1.5" data-blume-narration="skip">
<div class={`${columns} px-3 py-2 font-medium text-muted-foreground text-xs`}>
<span>Company</span>
<span>Asked within</span>
<span title="Relative within the company and window: 100 is its most-asked problem there">Frequency</span>
</div>
<div class="mt-1 flex flex-col">
{companies.map((c) => (
<div class={`${columns} border-border border-t px-3 py-2 text-xs`}>
<a href={withBase(companyHref(c.name))} class={linkClass}>
<Badge icon="building-2" shape="pill" stroke>
{c.name}
</Badge>
</a>
<span class="whitespace-nowrap text-foreground">{RECENCY_LABEL[c.period]}</span>
<span class="flex items-center gap-2">
<span class="h-1.5 w-16 overflow-hidden rounded-full bg-muted" aria-hidden="true">
<span class="block h-full rounded-full bg-accent" style={`width: ${c.frequency}%`} />
</span>
<code class="w-8 text-right font-mono text-foreground tabular-nums">{Math.round(c.frequency)}</code>
</span>
</div>
))}
</div>
</div>
)
}
<slot />
@@ -0,0 +1,79 @@
/**
* The ten most recently solved problems — LeetCode's "Recent AC" list. Each
* row is dated by the problem's first pass (an ET calendar date: the Worker
* logs days, not times), read relative to today in ET with the full date on
* hover. A row links to the problem's docs page when one exists: the
* sync-generated short links (`redirects.ts`) are exactly the pages built.
*/
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { useStats, utcDate } from "../../lib/stats";
import redirects from "../../redirects";
import { StatsPending } from "./shared";
const PAGES = new Map(redirects.map((r) => [r.from, r.to]));
const ET_TODAY = new Intl.DateTimeFormat("en-CA", { timeZone: "America/New_York" });
const RELATIVE = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const FULL = new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeZone: "UTC" });
const DAY_MS = 86_400_000;
export default function RecentSolves() {
const load = useStats();
if (load.phase !== "ready") {
return (
<Card>
<CardHeader>
<CardTitle>Recently solved</CardTitle>
</CardHeader>
<CardContent>
<StatsPending load={load} className="h-80 w-full" />
</CardContent>
</Card>
);
}
const today = utcDate(ET_TODAY.format(new Date())).getTime();
return (
<Card>
<CardHeader>
<CardTitle>Recently solved</CardTitle>
</CardHeader>
<CardContent>
{load.stats.recent.length === 0 ? (
<p className="my-0! text-muted-foreground text-sm">Nothing solved yet.</p>
) : (
<ol className="my-0! flex flex-col">
{load.stats.recent.map((p) => {
const date = utcDate(p.date);
const page = PAGES.get(`/${p.lc}`);
const name = (
<>
<span className="text-muted-foreground tabular-nums">{p.lc}.</span> {p.title}
</>
);
return (
<li
key={p.lc}
className="flex items-center justify-between gap-4 rounded-lg px-4 py-3 text-sm odd:bg-muted"
>
{page ? (
<a href={page} className="truncate font-normal! text-foreground no-underline! hover:underline!">
{name}
</a>
) : (
<span className="truncate">{name}</span>
)}
<time dateTime={p.date} title={FULL.format(date)} className="shrink-0 text-muted-foreground">
{RELATIVE.format(Math.round((date.getTime() - today) / DAY_MS), "day")}
</time>
</li>
);
})}
</ol>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,143 @@
/**
* Solved-by-difficulty ring — LeetCode's profile gauge. A 270° arc split into
* one segment per difficulty, each sized by that difficulty's share of the
* catalog; the faint track is the whole set, the solid arc the solved part.
* The centre reads the total, or one difficulty while its segment (or its
* stat box) is hovered. "Attempting" is the open-miss count: problems whose
* latest attempt failed.
*/
import { useState } from "react";
import { Pie, PieChart } from "recharts";
import { Card, CardContent } from "../ui/card";
import { type ChartConfig, ChartContainer } from "../ui/chart";
import { DIFFICULTIES, type Difficulty, useStats } from "../../lib/stats";
import { cn } from "../../lib/utils";
import { StatsPending } from "./shared";
const chartConfig = {
easy: { label: "Easy", color: "var(--chart-1)" },
medium: { label: "Med.", color: "var(--chart-2)" },
hard: { label: "Hard", color: "var(--chart-3)" },
} satisfies ChartConfig;
// Static class names, so Tailwind's scanner sees every one (the chart's
// --color-* variables exist only inside ChartContainer).
const DIFFICULTY_TEXT: Record<Difficulty, string> = {
easy: "text-chart-1",
medium: "text-chart-2",
hard: "text-chart-3",
};
// The gauge opens at the bottom: from 225° (lower left) clockwise to -45°
// (lower right), in Recharts' counter-clockwise-from-3-o'clock degrees.
const START = 225;
const SWEEP = 270;
const GAP = 8;
export default function SolvedRing() {
const load = useStats();
const [focus, setFocus] = useState<Difficulty | null>(null);
if (load.phase !== "ready") {
return (
<Card>
<CardContent>
<StatsPending load={load} className="h-52 w-full" />
</CardContent>
</Card>
);
}
const rows = DIFFICULTIES.map(
(difficulty) => load.stats.difficulty.find((d) => d.difficulty === difficulty) ?? { difficulty, total: 0, done: 0 },
);
const total = rows.reduce((n, r) => n + r.total, 0);
const done = rows.reduce((n, r) => n + r.done, 0);
const span = SWEEP - GAP * (rows.length - 1);
const segments: ((typeof rows)[number] & { from: number; to: number; solvedTo: number })[] = [];
let cursor = START;
for (const r of rows) {
const size = total > 0 ? (span * r.total) / total : 0;
const solved = r.total > 0 ? size * (r.done / r.total) : 0;
segments.push({ ...r, from: cursor, to: cursor - size, solvedTo: cursor - solved });
cursor -= size + GAP;
}
const focused = focus ? rows.find((r) => r.difficulty === focus) : undefined;
const arc = { innerRadius: "80%", outerRadius: "88%", cornerRadius: 99, stroke: "none", dataKey: "value" } as const;
return (
<Card>
<CardContent className="flex flex-col items-center gap-6 sm:flex-row">
<div className="relative w-full max-w-56 shrink-0">
<ChartContainer config={chartConfig} className="aspect-square w-full">
<PieChart>
{segments.map((s) => (
<Pie
key={`${s.difficulty}-track`}
{...arc}
data={[{ value: 1 }]}
startAngle={s.from}
endAngle={s.to}
fill={`var(--color-${s.difficulty})`}
fillOpacity={focus && focus !== s.difficulty ? 0.12 : 0.25}
isAnimationActive={false}
onMouseEnter={() => setFocus(s.difficulty)}
onMouseLeave={() => setFocus(null)}
/>
))}
{segments
.filter((s) => s.done > 0)
.map((s) => (
<Pie
key={`${s.difficulty}-solved`}
{...arc}
data={[{ value: 1 }]}
startAngle={s.from}
endAngle={s.solvedTo}
fill={`var(--color-${s.difficulty})`}
fillOpacity={focus && focus !== s.difficulty ? 0.35 : 1}
onMouseEnter={() => setFocus(s.difficulty)}
onMouseLeave={() => setFocus(null)}
/>
))}
</PieChart>
</ChartContainer>
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-1">
<div className="flex items-baseline">
<span className="font-semibold text-4xl tabular-nums">{focused ? focused.done : done}</span>
<span className="text-muted-foreground tabular-nums">/{focused ? focused.total : total}</span>
</div>
<span className={cn("text-sm", focused ? DIFFICULTY_TEXT[focused.difficulty] : "text-muted-foreground")}>
{focused ? chartConfig[focused.difficulty].label : "Solved"}
</span>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-3 text-center text-muted-foreground text-xs">
<span className="font-semibold text-foreground tabular-nums">{load.stats.attempting}</span> Attempting
</div>
</div>
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-1">
{rows.map((r) => (
<div
key={r.difficulty}
className={cn(
"flex flex-col items-center gap-0.5 rounded-lg bg-muted px-4 py-2 transition-opacity",
focus && focus !== r.difficulty && "opacity-50",
)}
onPointerEnter={() => setFocus(r.difficulty)}
onPointerLeave={() => setFocus(null)}
>
<span className={cn("font-medium text-sm", DIFFICULTY_TEXT[r.difficulty])}>
{chartConfig[r.difficulty].label}
</span>
<span className="text-sm tabular-nums">
{r.done}/{r.total}
</span>
</div>
))}
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,220 @@
/**
* This calendar year by difficulty — LeetCode's solved/submissions bar chart,
* sharing its row with the heatmap. Stacked Easy / Med. / Hard bars per month
* or per week (Monday-start, like the heatmap rows); "Solved" counts each
* problem once, on its first-pass date (the date `recent` uses), while
* "Submissions" counts every logged attempt. The totals under the chart are
* the year's, in the selected mode. Buckets are cut from `year`, which always
* covers January 1 to today.
*/
import { useState } from "react";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { Card, CardAction, CardContent, CardFooter, CardHeader } from "../ui/card";
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart";
import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
import { DIFFICULTIES, type Day, type Difficulty, useStats, utcDate } from "../../lib/stats";
import { StatsPending } from "./shared";
const chartConfig = {
easy: { label: "Easy", color: "var(--chart-1)" },
medium: { label: "Med.", color: "var(--chart-2)" },
hard: { label: "Hard", color: "var(--chart-3)" },
} satisfies ChartConfig;
// Static class names, so Tailwind's scanner sees every one.
const DIFFICULTY_TEXT: Record<Difficulty, string> = {
easy: "text-chart-1",
medium: "text-chart-2",
hard: "text-chart-3",
};
type Mode = "solved" | "attempts";
type Grain = "month" | "week";
const DAY_MS = 86_400_000;
const MONTH = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" });
const MONTH_YEAR = new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric", timeZone: "UTC" });
const SHORT = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
interface Bucket extends Record<Difficulty, number> {
index: number;
/** Axis tick: the month the bucket starts in. */
tick: string;
/** Tooltip heading. */
label: string;
}
/** The year's buckets, every one present (future ones empty), filled from `days`. */
function buckets(days: Day[], year: number, grain: Grain, mode: Mode): Bucket[] {
const jan1 = Date.UTC(year, 0, 1);
const lead = (new Date(jan1).getUTCDay() + 6) % 7; // Jan 1's Monday-based weekday
const length = Math.round((Date.UTC(year + 1, 0, 1) - jan1) / DAY_MS);
const out: Bucket[] = [];
if (grain === "month") {
for (let m = 0; m < 12; m++) {
const start = new Date(Date.UTC(year, m, 1));
out.push({ index: m, tick: MONTH.format(start), label: MONTH_YEAR.format(start), easy: 0, medium: 0, hard: 0 });
}
} else {
const weeks = Math.floor((length - 1 + lead) / 7) + 1;
for (let w = 0; w < weeks; w++) {
const start = new Date(Math.max(jan1, jan1 + (w * 7 - lead) * DAY_MS));
const end = new Date(Math.min(jan1 + (length - 1) * DAY_MS, jan1 + (w * 7 - lead + 6) * DAY_MS));
out.push({
index: w,
tick: MONTH.format(start),
label: `${SHORT.format(start)} – ${SHORT.format(end)}`,
easy: 0,
medium: 0,
hard: 0,
});
}
}
for (const day of days) {
const date = utcDate(day.date);
if (date.getUTCFullYear() !== year) continue;
const i = grain === "month" ? date.getUTCMonth() : Math.floor(((date.getTime() - jan1) / DAY_MS + lead) / 7);
const bucket = out[i];
if (!bucket) continue;
for (const d of DIFFICULTIES) bucket[d] += day[mode][d];
}
return out;
}
export default function SubmissionChart() {
const load = useStats();
const [mode, setMode] = useState<Mode>("solved");
const [grain, setGrain] = useState<Grain>("month");
if (load.phase !== "ready") {
return (
<Card>
<CardContent>
<StatsPending load={load} className="h-40 w-full" />
</CardContent>
</Card>
);
}
const year = utcDate(load.stats.year.at(-1)?.date ?? "1970-01-01").getUTCFullYear();
const data = buckets(load.stats.year, year, grain, mode);
const totals = Object.fromEntries(
DIFFICULTIES.map((d) => [d, data.reduce((n, b) => n + b[d], 0)]),
);
const peak = Math.max(...data.map((b) => b.easy + b.medium + b.hard));
// A round ceiling for the one labelled gridline, as LeetCode draws it.
const top = peak <= 5 ? 5 : Math.ceil(peak / 10) * 10;
return (
<Card>
<CardHeader className="flex flex-wrap items-center justify-between gap-2">
<ToggleGroup
type="single"
size="sm"
spacing={1}
value={mode}
onValueChange={(value: string) => value && setMode(value as Mode)}
className="rounded-full bg-muted p-0.5"
aria-label="Count"
>
<ToggleGroupItem value="solved" className="h-7 rounded-full px-2.5 text-xs">
Solved
</ToggleGroupItem>
<ToggleGroupItem value="attempts" className="h-7 rounded-full px-2.5 text-xs">
Submissions
</ToggleGroupItem>
</ToggleGroup>
<div className="flex items-center">
<ToggleGroup
type="single"
size="sm"
spacing={1}
value={grain}
onValueChange={(value: string) => value && setGrain(value as Grain)}
className="rounded-full bg-muted p-0.5"
aria-label="Bucket size"
>
<ToggleGroupItem value="month" className="h-7 rounded-full px-2.5 text-xs" aria-label="Per month">
M
</ToggleGroupItem>
<ToggleGroupItem value="week" className="h-7 rounded-full px-2.5 text-xs" aria-label="Per week">
W
</ToggleGroupItem>
</ToggleGroup>
</div>
</CardHeader>
<CardContent className="flex-1">
<ChartContainer config={chartConfig} className="aspect-auto h-40 w-full">
<BarChart accessibilityLayer data={data} margin={{ top: 8, right: 0, bottom: 0, left: 0 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="index"
type="category"
tickLine={false}
axisLine={false}
tickMargin={6}
ticks={[0, data.length - 1]}
interval={0}
// First label anchored at its start, last at its end, so neither
// is clipped when a week band is only a few pixels wide.
tick={({ x, y, payload }) => (
<text
x={x}
y={y}
dy={10}
textAnchor={payload.value === 0 ? "start" : "end"}
className="fill-muted-foreground"
>
{data[Number(payload.value)]?.tick}
</text>
)}
/>
<YAxis
orientation="right"
domain={[0, top]}
ticks={[0, top]}
tickLine={false}
axisLine={false}
width={28}
allowDecimals={false}
/>
<ChartTooltip
cursor={false}
// Recharts applies itemSorter only in its default tooltip; this
// content gets the stack's bottom-up order, so flip it to read
// top-down, Easy first, like the bars.
content={({ active, label, payload }) => (
<ChartTooltipContent
active={active}
label={label}
payload={[...(payload ?? [])].reverse()}
labelFormatter={(_, items) => items[0]?.payload?.label}
indicator="dot"
/>
)}
/>
{(["hard", "medium", "easy"] as const).map((d) => (
<Bar
key={d}
dataKey={d}
stackId="solved"
fill={`var(--color-${d})`}
radius={2}
maxBarSize={grain === "month" ? 10 : 5}
/>
))}
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="gap-4 text-sm">
{DIFFICULTIES.map((d) => (
<span key={d} className="flex items-baseline gap-1">
<span className={DIFFICULTY_TEXT[d]}>{chartConfig[d].label}</span>
<span className="text-muted-foreground tabular-nums">{totals[d]}</span>
</span>
))}
<span className="ml-auto text-muted-foreground tabular-nums">{year}</span>
</CardFooter>
</Card>
);
}
@@ -0,0 +1,163 @@
/**
* The last six months of attempts — LeetCode's calendar heatmap, halved to
* share its row with the submissions chart: one cell per day, weeks as
* columns, each month its own block, and the window's attempt count, active
* days and longest streak above it. The figures are counted from exactly the
* cells drawn, so they cannot disagree with the grid. The colour ramp is the
* README/email heatmap's (theme.css `--heat-0..4`, level = attempts capped at
* 4) and rows start on Monday like it, so the three heatmaps read the same.
* Drawn as SVG in a viewBox, so it scales to its column instead of scrolling.
*/
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { type Day, useStats, utcDate } from "../../lib/stats";
import { StatsPending, TipRow, useHoverTip } from "./shared";
const MONTHS = 6;
const CELL = 10;
const STEP = 13; // cell + gap
const MONTH_GAP = 8;
const LABEL_H = 16;
const HEAT = ["var(--heat-0)", "var(--heat-1)", "var(--heat-2)", "var(--heat-3)", "var(--heat-4)"];
const MONTH = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" });
const DAY = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
interface Cell {
x: number;
y: number;
date: Date;
attempts: number;
passes: number;
}
interface Block {
label: string;
x: number;
width: number;
cells: Cell[];
}
/**
* Month blocks laid left to right over the last MONTHS calendar months. Within
* a block a day sits at row = weekday (Mon = 0) and column = its week counted
* from the 1st, so a month starting on a Thursday opens with a short column.
*/
function layout(year: Day[]): { blocks: Block[]; width: number } {
const last = utcDate(year.at(-1)?.date ?? "1970-01-01");
const from = Date.UTC(last.getUTCFullYear(), last.getUTCMonth() - (MONTHS - 1), 1);
const blocks: Block[] = [];
let x = 0;
for (const day of year) {
const date = utcDate(day.date);
if (date.getTime() < from) continue;
let block = blocks.at(-1);
if (date.getUTCDate() === 1 || !block) {
if (block) x += block.width + MONTH_GAP;
block = { label: MONTH.format(date), x, width: 0, cells: [] };
blocks.push(block);
}
const first = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
const index = (first.getUTCDay() + 6) % 7 + date.getUTCDate() - 1;
const col = Math.floor(index / 7);
block.cells.push({
x: x + col * STEP,
y: (index % 7) * STEP,
date,
attempts: day.attempts.easy + day.attempts.medium + day.attempts.hard,
passes: day.passes,
});
block.width = col * STEP + CELL;
}
const end = blocks.at(-1);
return { blocks, width: end ? end.x + end.width : 0 };
}
export default function SubmissionHeatmap() {
const load = useStats();
const { ref, show, hide, node } = useHoverTip<HTMLDivElement>();
if (load.phase !== "ready") {
return (
<Card>
<CardContent>
<StatsPending load={load} className="h-40 w-full" />
</CardContent>
</Card>
);
}
const { blocks, width } = layout(load.stats.year);
const cells = blocks.flatMap((b) => b.cells);
let run = 0;
let streak = 0;
for (const c of cells) {
run = c.attempts > 0 ? run + 1 : 0;
streak = Math.max(streak, run);
}
const height = 7 * STEP - (STEP - CELL) + LABEL_H;
return (
<Card>
<CardHeader>
<CardTitle className="flex items-baseline gap-2">
<span className="font-semibold text-2xl tabular-nums">{cells.reduce((n, c) => n + c.attempts, 0)}</span>
<span className="font-normal text-muted-foreground text-sm">attempts in the past {MONTHS} months</span>
</CardTitle>
<CardDescription className="flex gap-4">
<span>
Active days:{" "}
<span className="text-foreground tabular-nums">{cells.filter((c) => c.attempts > 0).length}</span>
</span>
<span>
Max streak: <span className="text-foreground tabular-nums">{streak}</span>
</span>
</CardDescription>
</CardHeader>
<CardContent className="flex flex-1 items-center">
<div ref={ref} className="relative w-full" onPointerLeave={hide}>
<svg
viewBox={`0 0 ${width} ${height}`}
className="h-auto w-full"
role="img"
aria-label={`Attempts per day over the past ${MONTHS} months`}
>
{blocks.map((block) => (
<g key={`${block.label}-${block.x}`}>
{block.cells.map((cell) => (
<rect
key={cell.x * 1000 + cell.y}
x={cell.x}
y={cell.y}
width={CELL}
height={CELL}
rx={2}
fill={HEAT[Math.min(cell.attempts, 4)]}
onPointerMove={(event) =>
show(
event,
<>
<div className="font-medium">{DAY.format(cell.date)}</div>
<TipRow color={HEAT[Math.min(cell.attempts, 4)]!} label="Attempts" value={cell.attempts} />
<TipRow color="var(--chart-4)" label="Passed" value={cell.passes} />
</>,
)
}
/>
))}
<text
x={block.x + block.width / 2}
y={height - 3}
textAnchor="middle"
className="fill-muted-foreground text-[9px]"
>
{block.label}
</text>
</g>
))}
</svg>
{node}
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,137 @@
/**
* The campaign's topics as packed bubbles — LeetCode's "All Topics" view,
* over our curriculum instead of LeetCode's tags. A bubble's area is the
* topic's problem count; it fills from the bottom like a glass as problems
* are solved, and turns solid once every one is. A topic the schedule has not
* reached yet is dimmed. Packing is d3-hierarchy's front-chain (packSiblings,
* largest first, so the layout is deterministic), drawn as SVG in a viewBox:
* Recharts has no packed layout.
*/
import { packEnclose, packSiblings } from "d3-hierarchy";
import { useId } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { type Stats, useStats } from "../../lib/stats";
import { cn } from "../../lib/utils";
import { StatsPending, TipRow, useHoverTip } from "./shared";
const PAD = 2; // gap between bubbles, in layout units
const UNIT = 12; // radius of a one-problem bubble
type Topic = Stats["topics"][number];
type Bubble = Topic & { x: number; y: number; r: number };
/** Split a name across lines of at most ~10 characters, never breaking a word. */
function lines(name: string): string[] {
const out: string[] = [];
for (const word of name.split(" ")) {
const last = out.at(-1);
if (last !== undefined && `${last} ${word}`.length <= 10) out[out.length - 1] = `${last} ${word}`;
else out.push(word);
}
return out;
}
export default function TopicBubbles() {
const load = useStats();
const { ref, show, hide, node } = useHoverTip<HTMLDivElement>();
const clip = useId().replace(/:/g, "");
if (load.phase !== "ready") {
return (
<Card>
<CardHeader>
<CardTitle>Topics</CardTitle>
</CardHeader>
<CardContent>
<StatsPending load={load} className="aspect-square w-full" />
</CardContent>
</Card>
);
}
const topics = load.stats.topics.filter((t) => t.total > 0);
const circles: Bubble[] = topics
.slice()
.sort((a, b) => b.total - a.total || a.issue - b.issue)
.map((t) => ({ ...t, x: 0, y: 0, r: UNIT * Math.sqrt(t.total) + PAD }));
packSiblings(circles);
const bound = packEnclose(circles) ?? { x: 0, y: 0, r: 1 };
const solved = topics.reduce((n, t) => n + t.done, 0);
const total = topics.reduce((n, t) => n + t.total, 0);
return (
<Card>
<CardHeader>
<CardTitle>Topics</CardTitle>
<CardDescription>
{solved} of {total} problems across {topics.length} topics · size is problem count, fill is solved
</CardDescription>
</CardHeader>
<CardContent>
<div ref={ref} className="relative mx-auto w-full max-w-xl" onPointerLeave={hide}>
<svg
viewBox={`${bound.x - bound.r} ${bound.y - bound.r} ${bound.r * 2} ${bound.r * 2}`}
className="h-auto w-full"
role="img"
aria-label="Campaign topics sized by problem count and filled by problems solved"
>
{circles.map((c) => {
const r = c.r - PAD;
const ratio = c.total > 0 ? c.done / c.total : 0;
const complete = c.done >= c.total;
const words = lines(c.name);
const size = Math.min(r / 3.2, (1.7 * r) / Math.max(...words.map((w) => w.length)));
const id = `${clip}-${c.issue}`;
return (
<g
key={c.issue}
className={cn("transition-opacity hover:opacity-90", !c.taught && "opacity-50")}
onPointerMove={(event) =>
show(
event,
<>
<div className="font-medium">{c.name}</div>
<TipRow color="var(--chart-4)" label="Solved" value={`${c.done}/${c.total}`} />
{!c.taught && <div className="text-muted-foreground">Not reached yet</div>}
</>,
)
}
>
<clipPath id={id}>
<circle cx={c.x} cy={c.y} r={r} />
</clipPath>
<circle cx={c.x} cy={c.y} r={r} className="fill-muted stroke-border" strokeWidth={0.5} />
<rect
x={c.x - r}
y={c.y + r - 2 * r * ratio}
width={2 * r}
height={2 * r * ratio}
clipPath={`url(#${id})`}
fill="var(--chart-4)"
fillOpacity={complete ? 1 : 0.35}
/>
<text
x={c.x}
y={c.y - ((words.length - 1) * size * 1.15) / 2}
textAnchor="middle"
dominantBaseline="central"
clipPath={`url(#${id})`}
className={cn("pointer-events-none select-none", complete ? "fill-background" : "fill-foreground")}
style={{ fontSize: size }}
>
{words.map((w, i) => (
<tspan key={`${i}-${w}`} x={c.x} dy={i === 0 ? 0 : size * 1.15}>
{w}
</tspan>
))}
</text>
</g>
);
})}
</svg>
{node}
</div>
</CardContent>
</Card>
);
}
+67
View File
@@ -0,0 +1,67 @@
/**
* What the progress cards 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>
);
}
+91
View File
@@ -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,
}
+372
View File
@@ -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,
}
+13
View File
@@ -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 }
+83
View File
@@ -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 }
+47
View File
@@ -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 }
@@ -7,6 +7,13 @@ topics:
- { name: "Binary Search", slug: "binary-search" }
video: "nIVW4P8b1VA"
issue: "https://github.com/prdlk/leetcode/issues/62"
companies:
- { name: "Ola Cabs", period: "Thirty Days", frequency: 65.6 }
- { name: "Amazon", period: "Thirty Days", frequency: 48.9 }
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "tcs", period: "Six Months", frequency: 43.5 }
- { name: "Bloomberg", period: "Six Months", frequency: 40.6 }
- { name: "Meta", period: "Six Months", frequency: 32.7 }
sidebar:
label: 'Find Minimum in Rotated Sorted Array'
badge: 'Medium'
@@ -6,6 +6,15 @@ topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
issue: "https://github.com/prdlk/leetcode/issues/64"
companies:
- { name: "Infosys", period: "Thirty Days", frequency: 68.6 }
- { name: "tcs", period: "Thirty Days", frequency: 67.6 }
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "Meta", period: "Three Months", frequency: 54.8 }
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Bloomberg", period: "Three Months", frequency: 36.8 }
- { name: "Amazon", period: "Three Months", frequency: 34.6 }
- { name: "Uber", period: "Six Months", frequency: 67.2 }
sidebar:
label: 'Find Peak Element'
badge: 'Medium'
@@ -7,6 +7,18 @@ topics:
- { name: "Binary Search", slug: "binary-search" }
video: "U8XENwh8Oy8"
issue: "https://github.com/prdlk/leetcode/issues/60"
companies:
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Google", period: "Thirty Days", frequency: 36.7 }
- { name: "Oracle", period: "Three Months", frequency: 100 }
- { name: "Bloomberg", period: "Three Months", frequency: 68.3 }
- { name: "Microsoft", period: "Three Months", frequency: 33.4 }
- { name: "TikTok", period: "Six Months", frequency: 64.9 }
- { name: "LinkedIn", period: "Six Months", frequency: 63.3 }
- { name: "Yandex", period: "Six Months", frequency: 59.6 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 53.1 }
- { name: "Apple", period: "Six Months", frequency: 48.1 }
sidebar:
label: 'Search in Rotated Sorted Array'
badge: 'Medium'
@@ -6,6 +6,17 @@ topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
issue: "https://github.com/prdlk/leetcode/issues/65"
companies:
- { name: "Microsoft", period: "Thirty Days", frequency: 57.2 }
- { name: "Amazon", period: "Thirty Days", frequency: 48.9 }
- { name: "Google", period: "Thirty Days", frequency: 36.7 }
- { name: "TikTok", period: "Three Months", frequency: 70.9 }
- { name: "tcs", period: "Three Months", frequency: 47.7 }
- { name: "Meta", period: "Three Months", frequency: 43.3 }
- { name: "Bloomberg", period: "Three Months", frequency: 36.8 }
- { name: "Instacart", period: "Six Months", frequency: 100 }
- { name: "Apple", period: "Six Months", frequency: 70.2 }
- { name: "Infosys", period: "Six Months", frequency: 56.2 }
sidebar:
label: 'Find First and Last Position of Element in Sorted Array'
badge: 'Medium'
@@ -7,6 +7,15 @@ topics:
- { name: "Binary Search", slug: "binary-search" }
video: "K-RYzDZkzCI"
issue: "https://github.com/prdlk/leetcode/issues/63"
companies:
- { name: "Google", period: "Thirty Days", frequency: 50.2 }
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Grammarly", period: "Three Months", frequency: 84.8 }
- { name: "Bloomberg", period: "Three Months", frequency: 54 }
- { name: "Meta", period: "Three Months", frequency: 49.8 }
- { name: "tcs", period: "Three Months", frequency: 47.7 }
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Infosys", period: "Six Months", frequency: 56.2 }
sidebar:
label: 'Search Insert Position'
badge: 'Easy'
@@ -0,0 +1,76 @@
---
title: '4. Median of Two Sorted Arrays'
description: Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
issue: "https://github.com/prdlk/leetcode/issues/66"
companies:
- { name: "Rippling", period: "Thirty Days", frequency: 93 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 69.6 }
- { name: "Meta", period: "Thirty Days", frequency: 63.8 }
- { name: "Google", period: "Thirty Days", frequency: 62 }
- { name: "Microsoft", period: "Thirty Days", frequency: 57.2 }
- { name: "eBay", period: "Three Months", frequency: 82.2 }
- { name: "Goldman Sachs", period: "Three Months", frequency: 57 }
- { name: "tcs", period: "Three Months", frequency: 47.7 }
- { name: "Zoho", period: "Six Months", frequency: 79.6 }
- { name: "DE Shaw", period: "Six Months", frequency: 74.2 }
- { name: "Apple", period: "Six Months", frequency: 48.1 }
sidebar:
label: 'Median of Two Sorted Arrays'
badge: 'Hard'
---
### Example 1:
- Input: `nums1 = [1,3], nums2 = [2]`
- Output: `2.00000`
- Explanation: merged array = `[1,2,3]` and median is `2`.
### Example 2:
- Input: `nums1 = [1,2], nums2 = [3,4]`
- Output: `2.50000`
- Explanation: merged array = `[1,2,3,4]` and median is `(2 + 3`) / `2 = 2.5`.
### Constraints:
- `nums1.length == m`
- `nums2.length == n`
- `0 <= m <= 1000`
- `0 <= n <= 1000`
- `1 <= m + n <= 2000`
- `-10^6 <= nums1[i], nums2[i] <= 10^6`
## Solution
```py
class Solution:
def findMedianSortedArrays(self, nums1: list[int], nums2: list[int]) -> float:
if len(nums1) > len(nums2):
return self.findMedianSortedArrays(nums2, nums1)
m, n = len(nums1), len(nums2)
left, right = 0, m
while left <= right:
partA = (left + right) // 2
partB = (m + n + 1) // 2 - partA
maxLeftA = float("-inf") if partA == 0 else nums1[partA - 1]
minRightA = float("inf") if partA == m else nums1[partA]
maxLeftB = float("-inf") if partB == 0 else nums2[partB - 1]
minRightB = float("inf") if partB == n else nums2[partB]
if maxLeftA <= minRightB and maxLeftB <= minRightA:
if (m + n) % 2 == 0:
return (max(maxLeftA, maxLeftB) + min(minRightA, minRightB)) / 2
else:
return max(maxLeftA, maxLeftB)
elif maxLeftA > minRightB:
right = partA - 1
else:
left = partA + 1
```
@@ -7,6 +7,12 @@ topics:
- { name: "Binary Search", slug: "binary-search" }
video: "s4DPM8ct1pI"
issue: "https://github.com/prdlk/leetcode/issues/58"
companies:
- { name: "Google", period: "Thirty Days", frequency: 53.2 }
- { name: "Bloomberg", period: "Three Months", frequency: 54 }
- { name: "Meta", period: "Three Months", frequency: 49.8 }
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Amazon", period: "Three Months", frequency: 39.1 }
sidebar:
label: 'Binary Search'
badge: 'Easy'
@@ -8,6 +8,15 @@ topics:
- { name: "Matrix", slug: "matrix" }
video: "Ber2pi2C0j0"
issue: "https://github.com/prdlk/leetcode/issues/61"
companies:
- { name: "Goldman Sachs", period: "Thirty Days", frequency: 62.9 }
- { name: "Microsoft", period: "Three Months", frequency: 57.2 }
- { name: "Amazon", period: "Three Months", frequency: 34.6 }
- { name: "Meta", period: "Three Months", frequency: 34.5 }
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "SAP", period: "Six Months", frequency: 100 }
- { name: "tcs", period: "Six Months", frequency: 43.5 }
- { name: "Bloomberg", period: "Six Months", frequency: 25.5 }
sidebar:
label: 'Search a 2D Matrix'
badge: 'Medium'
@@ -7,6 +7,18 @@ topics:
- { name: "Binary Search", slug: "binary-search" }
video: "U2SozAs9RzA"
issue: "https://github.com/prdlk/leetcode/issues/59"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 69.6 }
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "Nvidia", period: "Three Months", frequency: 100 }
- { name: "Infosys", period: "Three Months", frequency: 87.4 }
- { name: "Meta", period: "Three Months", frequency: 62.3 }
- { name: "Bloomberg", period: "Three Months", frequency: 54 }
- { name: "Coupang", period: "Six Months", frequency: 100 }
- { name: "Swiggy", period: "Six Months", frequency: 100 }
- { name: "Accenture", period: "Six Months", frequency: 75 }
- { name: "Microsoft", period: "Six Months", frequency: 56.9 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 44.8 }
sidebar:
label: 'Koko Eating Bananas'
badge: 'Medium'
@@ -26,14 +26,14 @@ Time Complexity: `O(log(n))`
<Carousel>
<CarouselImage src='/binary_search.001.png' />
<CarouselImage src='/binary_search.002.png' />
<CarouselImage src='/binary_search.003.png' />
<CarouselImage src='/binary_search.004.png' />
<CarouselImage src='/binary_search.005.png' />
<CarouselImage src='/binary_search.006.png' />
<CarouselImage src='/binary_search.007.png' />
<CarouselImage src='/binary_search.008.png' />
<CarouselImage src='../../../public/binary_search.001.png' />
<CarouselImage src='../../../public/binary_search.002.png' />
<CarouselImage src='../../../public/binary_search.003.png' />
<CarouselImage src='../../../public/binary_search.004.png' />
<CarouselImage src='../../../public/binary_search.005.png' />
<CarouselImage src='../../../public/binary_search.006.png' />
<CarouselImage src='../../../public/binary_search.007.png' />
<CarouselImage src='../../../public/binary_search.008.png' />
</Carousel>
```python
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Binary Search", order: 5, icon: "search", display: "group" });
@@ -7,6 +7,18 @@ topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "Union-Find", slug: "union-find" }
issue: "https://github.com/prdlk/leetcode/issues/34"
companies:
- { name: "tcs", period: "Thirty Days", frequency: 74.3 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 65.8 }
- { name: "Microsoft", period: "Thirty Days", frequency: 57.2 }
- { name: "Google", period: "Thirty Days", frequency: 53.2 }
- { name: "Apple", period: "Three Months", frequency: 69 }
- { name: "Meta", period: "Three Months", frequency: 34.5 }
- { name: "Paytm", period: "Six Months", frequency: 100 }
- { name: "Nvidia", period: "Six Months", frequency: 91.1 }
- { name: "TikTok", period: "Six Months", frequency: 55.2 }
- { name: "Salesforce", period: "Six Months", frequency: 50.2 }
sidebar:
label: 'Longest Consecutive Sequence'
badge: 'Medium'
@@ -5,6 +5,10 @@ icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
companies:
- { name: "Bloomberg", period: "Six Months", frequency: 34.2 }
- { name: "Amazon", period: "Six Months", frequency: 14.3 }
- { name: "Google", period: "Six Months", frequency: 11.8 }
sidebar:
label: 'Check if the Sentence Is Pangram'
badge: 'Easy'
@@ -0,0 +1,94 @@
---
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"
issue: "https://github.com/prdlk/leetcode/issues/29"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 100 }
- { name: "Google", period: "Thirty Days", frequency: 100 }
- { name: "Meta", period: "Thirty Days", frequency: 100 }
- { name: "Microsoft", period: "Thirty Days", frequency: 100 }
- { name: "tcs", period: "Thirty Days", frequency: 79.6 }
- { name: "Ola Cabs", period: "Thirty Days", frequency: 76.7 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Infosys", period: "Thirty Days", frequency: 68.6 }
- { name: "Altimetrik", period: "Three Months", frequency: 100 }
- { name: "Capgemini", period: "Three Months", frequency: 100 }
- { name: "Microstrategy", period: "Three Months", frequency: 100 }
- { name: "MongoDB", period: "Three Months", frequency: 100 }
- { name: "Cognizant", period: "Three Months", frequency: 88.7 }
- { name: "Apple", period: "Three Months", frequency: 80.4 }
- { name: "Barclays", period: "Six Months", frequency: 100 }
- { name: "Cisco", period: "Six Months", frequency: 100 }
- { name: "Deloitte", period: "Six Months", frequency: 100 }
- { name: "Expedia", period: "Six Months", frequency: 100 }
- { name: "NetApp", period: "Six Months", frequency: 100 }
- { name: "Samsung", period: "Six Months", frequency: 100 }
- { name: "Sony", period: "Six Months", frequency: 100 }
- { name: "Oracle", period: "Six Months", frequency: 91.2 }
- { name: "Qualcomm", period: "Six Months", frequency: 80.7 }
- { name: "IBM", period: "Six Months", frequency: 77.2 }
- { name: "eBay", period: "Six Months", frequency: 75.2 }
- { name: "Walmart Labs", period: "Six Months", frequency: 70.5 }
- { name: "Flipkart", period: "Six Months", frequency: 57.1 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 44.8 }
sidebar:
label: 'Two Sum'
badge: 'Easy'
---
::::warning
Can you come up with an algorithm that is less than O(n^2) time complexity?
::::
### Example 1:
- Input: `nums = [2,7,11,15], target = 9`
- Output: `[0,1]`
- Explanation: Because `nums[0] + nums[1]` == `9`, we return [0, 1].
### Example 2:
- Input: `nums = [3,2,4], target = 6`
- Output: `[1,2]`
### Example 3:
- Input: `nums = [3,3], target = 6`
- Output: `[0,1]`
### Constraints:
- `2 <= nums.length <= 10^4`
- `-10^9 <= nums[i] <= 10^9`
- `-10^9 <= target <= 10^9`
- Only one valid answer exists.
## Approach
```mermaid
flowchart TD
S(["twoSum(nums, target)"]) --> I["seen = {} — value to index"]
I --> L{"more (i, num) in nums?"}
L -- no --> E(["return []"])
L -- yes --> C["complement = target - num"]
C --> H{"complement in seen?"}
H -- yes --> R(["return [seen[complement], i]"])
H -- no --> W["seen[num] = i"]
W --> L
```
## Solution
```py
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
```
@@ -6,6 +6,13 @@ topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
issue: "https://github.com/prdlk/leetcode/issues/33"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Google", period: "Thirty Days", frequency: 36.7 }
- { name: "Bloomberg", period: "Three Months", frequency: 54 }
- { name: "tcs", period: "Three Months", frequency: 47.7 }
- { name: "Microsoft", period: "Three Months", frequency: 33.4 }
- { name: "Meta", period: "Six Months", frequency: 24.5 }
sidebar:
label: 'Isomorphic Strings'
badge: 'Easy'
@@ -8,6 +8,12 @@ topics:
- { name: "Sorting", slug: "sorting" }
video: "3OamzN90kPg"
issue: "https://github.com/prdlk/leetcode/issues/27"
companies:
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Google", period: "Thirty Days", frequency: 50.2 }
- { name: "Amazon", period: "Thirty Days", frequency: 48.9 }
- { name: "Microsoft", period: "Three Months", frequency: 48.3 }
- { name: "Bloomberg", period: "Three Months", frequency: 36.8 }
sidebar:
label: 'Contains Duplicate'
badge: 'Easy'
@@ -8,6 +8,16 @@ topics:
- { name: "Sorting", slug: "sorting" }
video: "9UtInBqnCgA"
issue: "https://github.com/prdlk/leetcode/issues/28"
companies:
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "tcs", period: "Thirty Days", frequency: 58.2 }
- { name: "Google", period: "Thirty Days", frequency: 53.2 }
- { name: "Amazon", period: "Thirty Days", frequency: 48.9 }
- { name: "IBM", period: "Three Months", frequency: 100 }
- { name: "Infosys", period: "Three Months", frequency: 66 }
- { name: "Bloomberg", period: "Three Months", frequency: 64.4 }
- { name: "Meta", period: "Three Months", frequency: 58.9 }
- { name: "Deloitte", period: "Six Months", frequency: 81 }
sidebar:
label: 'Valid Anagram'
badge: 'Easy'
@@ -6,6 +6,10 @@ topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
issue: "https://github.com/prdlk/leetcode/issues/35"
companies:
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "Microsoft", period: "Six Months", frequency: 24.1 }
- { name: "Amazon", period: "Six Months", frequency: 20.9 }
sidebar:
label: 'Word Pattern'
badge: 'Easy'
@@ -13,6 +13,23 @@ topics:
- { name: "Quickselect", slug: "quickselect" }
video: "YPTqKIgVk-k"
issue: "https://github.com/prdlk/leetcode/issues/31"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 72.9 }
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "Google", period: "Thirty Days", frequency: 42.3 }
- { name: "Bloomberg", period: "Three Months", frequency: 54 }
- { name: "Snap", period: "Six Months", frequency: 100 }
- { name: "SoFi", period: "Six Months", frequency: 100 }
- { name: "PayPal", period: "Six Months", frequency: 88.4 }
- { name: "Walmart Labs", period: "Six Months", frequency: 81.4 }
- { name: "Nvidia", period: "Six Months", frequency: 78.7 }
- { name: "eBay", period: "Six Months", frequency: 75.2 }
- { name: "Visa", period: "Six Months", frequency: 69.7 }
- { name: "Snowflake", period: "Six Months", frequency: 58.7 }
- { name: "TikTok", period: "Six Months", frequency: 55.2 }
- { name: "Meta", period: "Six Months", frequency: 50.6 }
- { name: "Apple", period: "Six Months", frequency: 48.1 }
- { name: "tcs", period: "Six Months", frequency: 43.5 }
sidebar:
label: 'Top K Frequent Elements'
badge: 'Medium'
@@ -8,6 +8,16 @@ topics:
- { name: "Queue", slug: "queue" }
- { name: "Counting", slug: "counting" }
video: "rBENYgWy3xU"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 48.9 }
- { name: "Yandex", period: "Three Months", frequency: 76.1 }
- { name: "tcs", period: "Three Months", frequency: 73.7 }
- { name: "Bloomberg", period: "Three Months", frequency: 46.8 }
- { name: "Meta", period: "Three Months", frequency: 43.3 }
- { name: "Google", period: "Three Months", frequency: 23.9 }
- { name: "EPAM Systems", period: "Six Months", frequency: 76.2 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 59 }
- { name: "Microsoft", period: "Six Months", frequency: 32 }
sidebar:
label: 'First Unique Character in a String'
badge: 'Easy'
@@ -10,6 +10,12 @@ topics:
- { name: "Bucket Sort", slug: "bucket-sort" }
- { name: "Counting", slug: "counting" }
video: "OXdXc9HTrIg"
companies:
- { name: "Meta", period: "Three Months", frequency: 49.8 }
- { name: "Microsoft", period: "Three Months", frequency: 48.3 }
- { name: "Bloomberg", period: "Three Months", frequency: 46.8 }
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "Amazon", period: "Six Months", frequency: 20.9 }
sidebar:
label: 'Sort Characters By Frequency'
badge: 'Medium'
@@ -9,6 +9,31 @@ topics:
- { name: "Sorting", slug: "sorting" }
video: "vzdNOK2oB2E"
issue: "https://github.com/prdlk/leetcode/issues/30"
companies:
- { name: "Apple", period: "Thirty Days", frequency: 100 }
- { name: "Amazon", period: "Thirty Days", frequency: 78.4 }
- { name: "Ola Cabs", period: "Thirty Days", frequency: 76.7 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "tcs", period: "Thirty Days", frequency: 58.2 }
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Google", period: "Thirty Days", frequency: 50.2 }
- { name: "CrowdStrike", period: "Three Months", frequency: 100 }
- { name: "Deloitte", period: "Three Months", frequency: 100 }
- { name: "HashedIn", period: "Three Months", frequency: 100 }
- { name: "Smartsheet", period: "Three Months", frequency: 100 }
- { name: "Walmart Labs", period: "Three Months", frequency: 100 }
- { name: "Microsoft", period: "Three Months", frequency: 33.4 }
- { name: "Expedia", period: "Six Months", frequency: 100 }
- { name: "Wipro", period: "Six Months", frequency: 100 }
- { name: "Adobe", period: "Six Months", frequency: 93.7 }
- { name: "Zoho", period: "Six Months", frequency: 79.6 }
- { name: "Nvidia", period: "Six Months", frequency: 78.7 }
- { name: "Oracle", period: "Six Months", frequency: 78.7 }
- { name: "EPAM Systems", period: "Six Months", frequency: 76.2 }
- { name: "Palo Alto Networks", period: "Six Months", frequency: 75.9 }
- { name: "Visa", period: "Six Months", frequency: 69.7 }
- { name: "Infosys", period: "Six Months", frequency: 67.9 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 53.1 }
sidebar:
label: 'Group Anagrams'
badge: 'Medium'
@@ -13,7 +13,7 @@ Data is stored in a fix-sized array, where each data value has an unique index.
<Frame caption="Hash Table Operations">
![](/hash-table-operations.svg)
![](../../../public/hash-table-operations.svg)
</Frame>
@@ -39,10 +39,12 @@ def get_counter(arr: list[int]) -> dict[int, int]:
return counter
```
::: tip[Python provides a built-in `collections.Counter` class]
:::tip
Python provides a built-in [`collections.Counter`](https://docs.python.org/3/library/collections.html#collections.Counter) class that does this for us.
:::
```python
from collections import Counter
fq = Counter(arr)
```
:::
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Hash Table", order: 1, icon: "hash", display: "group" });
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Linear Structures", order: 1 });
@@ -0,0 +1,66 @@
---
title: '496. Next Greater Element I'
description: The next greater element of some element x in an array is the first greater element that is to the right of x in the same array
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Hash Table", slug: "hash-table" }
- { name: "Stack", slug: "stack" }
- { name: "Monotonic Stack", slug: "monotonic-stack" }
issue: "https://github.com/prdlk/leetcode/issues/74"
companies:
- { name: "Google", period: "Thirty Days", frequency: 46.6 }
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Bloomberg", period: "Three Months", frequency: 59.7 }
- { name: "Meta", period: "Three Months", frequency: 43.3 }
- { name: "Microsoft", period: "Three Months", frequency: 33.4 }
- { name: "tcs", period: "Six Months", frequency: 43.5 }
sidebar:
label: 'Next Greater Element I'
badge: 'Easy'
---
::::warning
Could you find an O(nums1.length + nums2.length) solution?
::::
### Example 1:
- Input: `nums1 = [4,1,2], nums2 = [1,3,4,2]`
- Output: `[-1,3,-1]`
- Explanation:
- The next greater element for each value of `nums1` is as follows:
- `4` is underlined in `nums2 = [1,3,4,2]`. There is no next greater element, so the answer is `-1`.
- `1` is underlined in `nums2 = [1,3,4,2]`. The next greater element is `3`.
- `2` is underlined in `nums2 = [1,3,4,2]`. There is no next greater element, so the answer is `-1`.
### Example 2:
- Input: `nums1 = [2,4], nums2 = [1,2,3,4]`
- Output: `[3,-1]`
- Explanation:
- The next greater element for each value of `nums1` is as follows:
- `2` is underlined in `nums2 = [1,2,3,4]`. The next greater element is `3`.
- `4` is underlined in `nums2 = [1,2,3,4]`. There is no next greater element, so the answer is `-1`.
### Constraints:
- `1 <= nums1.length <= nums2.length <= 1000`
- `0 <= nums1[i], nums2[i] <= 10^4`
- All integers in `nums1` and `nums2` are unique.
- All the integers of `nums1` also appear in nums2.
## Solution
```py
class Solution:
def nextGreaterElement(self, nums1: list[int], nums2: list[int]) -> list[int]:
mono_stack = []
next_greater = {}
for cur in nums2:
while mono_stack and cur > mono_stack[-1]:
val = mono_stack.pop()
next_greater[val] = cur
mono_stack.append(cur)
return [next_greater.get(x, -1) for x in nums1]
```
@@ -8,6 +8,16 @@ topics:
- { name: "Monotonic Stack", slug: "monotonic-stack" }
video: "cTBiBSnjO3c"
issue: "https://github.com/prdlk/leetcode/issues/72"
companies:
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "Chime", period: "Three Months", frequency: 100 }
- { name: "Bloomberg", period: "Three Months", frequency: 59.7 }
- { name: "Amazon", period: "Three Months", frequency: 42.8 }
- { name: "Meta", period: "Three Months", frequency: 34.5 }
- { name: "Anduril", period: "Six Months", frequency: 92.8 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 44.8 }
- { name: "tcs", period: "Six Months", frequency: 43.5 }
- { name: "Microsoft", period: "Six Months", frequency: 42.5 }
sidebar:
label: 'Daily Temperatures'
badge: 'Medium'
@@ -0,0 +1,59 @@
---
title: '84. Largest Rectangle in Histogram'
description: Given an array of integers heights representing the histogram's bar height where the width of each bar is 1, return the area of the largest rectangle in the histogram
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Stack", slug: "stack" }
- { name: "Monotonic Stack", slug: "monotonic-stack" }
- { name: "Range Minimum/Maximum Query", slug: "range-minimum-maximum-query" }
issue: "https://github.com/prdlk/leetcode/issues/76"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "Accenture", period: "Three Months", frequency: 88.6 }
- { name: "tcs", period: "Three Months", frequency: 73.7 }
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Bloomberg", period: "Three Months", frequency: 36.8 }
- { name: "Meta", period: "Three Months", frequency: 34.5 }
sidebar:
label: 'Largest Rectangle in Histogram'
badge: 'Hard'
---
### Example 1:
- Input: `heights = [2,1,5,6,2,3]`
- Output: `10`
- Explanation: The above is a histogram where width of each bar is `1`. The largest rectangle is shown in the red area, which has an area = `10` units.
### Example 2:
- Input: `heights = [2,4]`
- Output: `4`
### Constraints:
- `1 <= heights.length <= 10^5`
- `0 <= heights[i] <= 10^4`
## Solution
```py
class Solution:
def largestRectangleArea(self, heights: list[int]) -> int:
maxArea = 0
stack = []
for i, h in enumerate(heights):
start = i
while stack and stack[-1][1] > h:
index, height = stack.pop()
width = i - index
maxArea = max(maxArea, height * width)
start = index
stack.append((start, h))
for i, h in stack:
maxArea = max(maxArea, h * (len(heights) - i))
return maxArea
```
@@ -9,6 +9,9 @@ topics:
- { name: "Monotonic Stack", slug: "monotonic-stack" }
video: "Pr6T-3yB9RM"
issue: "https://github.com/prdlk/leetcode/issues/73"
companies:
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "Amazon", period: "Six Months", frequency: 26 }
sidebar:
label: 'Car Fleet'
badge: 'Medium'
@@ -0,0 +1,50 @@
---
title: '901. Online Stock Span'
description: Design an algorithm that collects daily price quotes for some stock and returns the span of that stock's price for the current day
icon: dot
topics:
- { name: "Stack", slug: "stack" }
- { name: "Design", slug: "design" }
- { name: "Monotonic Stack", slug: "monotonic-stack" }
- { name: "Data Stream", slug: "data-stream" }
issue: "https://github.com/prdlk/leetcode/issues/75"
companies:
- { name: "Meta", period: "Three Months", frequency: 34.5 }
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "Amazon", period: "Six Months", frequency: 26 }
- { name: "Bloomberg", period: "Six Months", frequency: 25.5 }
sidebar:
label: 'Online Stock Span'
badge: 'Medium'
---
### Example 1:
- Input: ``
- Output: ``
- Explanation: Input ["StockSpanner", "next", "next", "next", "next", "next", "next", "next"] `[[]`, `[100]`, `[80]`, `[60]`, `[70]`, `[60]`, `[75]`, [85]] Output [null, `1`, `1`, `1`, `2`, `1`, `4`, 6] Explanation StockSpanner stockSpanner = new StockSpanner(); stockSpanner.next(100); // return `1` stockSpanner.next(80); // return `1` stockSpanner.next(60); // return `1` stockSpanner.next(70); // return `2` stockSpanner.next(60); // return `1` stockSpanner.next(75); // return `4`, because the last `4` prices (including today's `price` of `75`) were less than or equal to today's `price`. stockSpanner.next(85); // return `6`
### Constraints:
- `1 <= price <= 10^5`
- At most 10^4 calls will be made to next.
## Solution
```py
class StockSpanner:
def __init__(self):
self.stack = []
def next(self, price: int) -> int:
ans = 1
while self.stack and self.stack[-1][0] <= price:
ans += self.stack.pop()[1]
self.stack.append([price, ans])
return ans
# Your StockSpanner object will be instantiated and called as such:
# obj = StockSpanner()
# param_1 = obj.next(price)
```
@@ -0,0 +1,36 @@
---
title: Monotonic Stack
description: 'A stack keeps its items in sorted order, which finds the next larger or smaller item in one pass.'
hidden: true
---
<YouTube id="Dq_ObZwTY_Q" title="Monotonic Stack" />
A monotonic stack keeps its values in sorted order as you push elements. The order can be increasing or decreasing, depending on the problem.
A decreasing monotonic stack is the most common version. Before pushing a new value `x`, we pop from the top while the top is smaller than or equal to `x`. After those pops, pushing `x` keeps the stack decreasing from bottom to top.
This push rule is the whole idea. A monotonic deque uses the same rule, but a deque also lets you remove old elements from the front, which is useful for sliding window problems.
## Example Implementation
This implementation maintains a decreasing monotonic stack.
```python
def mono_stack(insert_entries):
stack = []
for entry in insert_entries:
# Pop smaller (or equal) values so stack stays decreasing.
while stack and stack[-1] <= entry:
stack.pop()
# Do something with the popped item here
stack.append(entry)
```
:::tip
In interview problems, the stack usually stores indices instead of values. You can read the value from the original array when needed, and the index gives you distance information such as **"how many steps away is the next greater element."**
:::
For an array `nums`, if index `j` pops when you process index `i`, then `i` is the first index to the right that satisfies the stack condition for `j`. In a next-greater-element problem, that means `nums[i]` is the next greater value for `nums[j]`.
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Monotonic Stack", order: 7, icon: "trending-up", display: "group" });
@@ -7,6 +7,14 @@ topics:
- { name: "Prefix Sum", slug: "prefix-sum" }
video: "bNvIQI2wAjk"
issue: "https://github.com/prdlk/leetcode/issues/37"
companies:
- { name: "Meta", period: "Thirty Days", frequency: 70.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 61.3 }
- { name: "Microsoft", period: "Thirty Days", frequency: 57.2 }
- { name: "Google", period: "Thirty Days", frequency: 42.3 }
- { name: "Bloomberg", period: "Three Months", frequency: 59.7 }
- { name: "Asana", period: "Six Months", frequency: 100 }
- { name: "tcs", period: "Six Months", frequency: 43.5 }
sidebar:
label: 'Product of Array Except Self'
badge: 'Medium'
@@ -8,6 +8,11 @@ topics:
- { name: "Prefix Sum", slug: "prefix-sum" }
video: "2pndAmo_sMA"
issue: "https://github.com/prdlk/leetcode/issues/36"
companies:
- { name: "Google", period: "Thirty Days", frequency: 42.3 }
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Meta", period: "Six Months", frequency: 24.5 }
- { name: "Microsoft", period: "Six Months", frequency: 24.1 }
sidebar:
label: 'Range Sum Query - Immutable'
badge: 'Easy'
@@ -7,6 +7,14 @@ topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "Prefix Sum", slug: "prefix-sum" }
issue: "https://github.com/prdlk/leetcode/issues/39"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Cisco", period: "Three Months", frequency: 100 }
- { name: "Meta", period: "Three Months", frequency: 49.8 }
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "Akamai", period: "Six Months", frequency: 100 }
- { name: "Bloomberg", period: "Six Months", frequency: 40.6 }
- { name: "Microsoft", period: "Six Months", frequency: 32 }
sidebar:
label: 'Contiguous Array'
badge: 'Medium'
@@ -8,6 +8,21 @@ topics:
- { name: "Prefix Sum", slug: "prefix-sum" }
video: "fFVZt-6sgyo"
issue: "https://github.com/prdlk/leetcode/issues/38"
companies:
- { name: "Meta", period: "Thirty Days", frequency: 63.8 }
- { name: "Google", period: "Thirty Days", frequency: 50.2 }
- { name: "Visa", period: "Three Months", frequency: 80.9 }
- { name: "Bloomberg", period: "Three Months", frequency: 74.8 }
- { name: "Salesforce", period: "Three Months", frequency: 64.7 }
- { name: "Amazon", period: "Three Months", frequency: 57.1 }
- { name: "Microsoft", period: "Three Months", frequency: 48.3 }
- { name: "Accenture", period: "Six Months", frequency: 100 }
- { name: "IBM", period: "Six Months", frequency: 94.5 }
- { name: "Infosys", period: "Six Months", frequency: 76.3 }
- { name: "Apple", period: "Six Months", frequency: 74.7 }
- { name: "josh technology", period: "Six Months", frequency: 74 }
- { name: "Walmart Labs", period: "Six Months", frequency: 70.5 }
- { name: "Yandex", period: "Six Months", frequency: 69.2 }
sidebar:
label: 'Subarray Sum Equals K'
badge: 'Medium'
@@ -7,6 +7,12 @@ topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "Prefix Sum", slug: "prefix-sum" }
issue: "https://github.com/prdlk/leetcode/issues/40"
companies:
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "TikTok", period: "Three Months", frequency: 70.9 }
- { name: "Amazon", period: "Three Months", frequency: 21.2 }
- { name: "Microsoft", period: "Six Months", frequency: 24.1 }
sidebar:
label: 'Subarray Sums Divisible by K'
badge: 'Medium'
@@ -27,7 +27,7 @@ The leading `prefix[0] = 0` is the sum of zero elements. That extra slot is what
<Frame caption="Each bar is the running total up to that index. Every step keeps the previous total (blue) and adds one new element (amber)" hint="">
![](/prefix-sum_1.png)
![](../../../public/prefix-sum_1.png)
</Frame>
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Prefix Sum", order: 2, icon: "sigma", display: "group" });
@@ -8,6 +8,17 @@ topics:
- { name: "Sliding Window", slug: "sliding-window" }
- { name: "Prefix Sum", slug: "prefix-sum" }
issue: "https://github.com/prdlk/leetcode/issues/56"
companies:
- { name: "EPAM Systems", period: "Thirty Days", frequency: 89.8 }
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Tekion", period: "Three Months", frequency: 100 }
- { name: "DE Shaw", period: "Three Months", frequency: 80.2 }
- { name: "Google", period: "Three Months", frequency: 38.7 }
- { name: "Infosys", period: "Six Months", frequency: 67.9 }
- { name: "Yandex", period: "Six Months", frequency: 59.6 }
- { name: "LinkedIn", period: "Six Months", frequency: 54.1 }
- { name: "Microsoft", period: "Six Months", frequency: 42.5 }
sidebar:
label: 'Max Consecutive Ones III'
badge: 'Medium'
@@ -7,6 +7,26 @@ topics:
- { name: "Dynamic Programming", slug: "dynamic-programming" }
video: "1pkOgXD63yU"
issue: "https://github.com/prdlk/leetcode/issues/50"
companies:
- { name: "Bloomberg", period: "Thirty Days", frequency: 83.1 }
- { name: "Infosys", period: "Thirty Days", frequency: 80.2 }
- { name: "Amazon", period: "Thirty Days", frequency: 69.6 }
- { name: "Ola Cabs", period: "Thirty Days", frequency: 65.6 }
- { name: "Google", period: "Thirty Days", frequency: 65.3 }
- { name: "tcs", period: "Thirty Days", frequency: 58.2 }
- { name: "Atlassian", period: "Three Months", frequency: 100 }
- { name: "Media.net", period: "Three Months", frequency: 100 }
- { name: "Nvidia", period: "Three Months", frequency: 87.7 }
- { name: "Meta", period: "Three Months", frequency: 70.4 }
- { name: "Apple", period: "Three Months", frequency: 69 }
- { name: "Microsoft", period: "Three Months", frequency: 60.5 }
- { name: "Mastercard", period: "Six Months", frequency: 79 }
- { name: "Accenture", period: "Six Months", frequency: 75 }
- { name: "Adobe", period: "Six Months", frequency: 74.3 }
- { name: "Walmart Labs", period: "Six Months", frequency: 70.5 }
- { name: "IBM", period: "Six Months", frequency: 70.1 }
- { name: "TikTok", period: "Six Months", frequency: 55.2 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 53.1 }
sidebar:
label: 'Best Time to Buy and Sell Stock'
badge: 'Easy'
@@ -8,6 +8,12 @@ topics:
- { name: "Sliding Window", slug: "sliding-window" }
- { name: "Prefix Sum", slug: "prefix-sum" }
issue: "https://github.com/prdlk/leetcode/issues/54"
companies:
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Bloomberg", period: "Three Months", frequency: 46.8 }
- { name: "Google", period: "Three Months", frequency: 45 }
- { name: "Amazon", period: "Three Months", frequency: 28.9 }
sidebar:
label: 'Minimum Size Subarray Sum'
badge: 'Medium'
@@ -8,6 +8,35 @@ topics:
- { name: "Sliding Window", slug: "sliding-window" }
video: "wiGpQwVHdE0"
issue: "https://github.com/prdlk/leetcode/issues/51"
companies:
- { name: "Ola Cabs", period: "Thirty Days", frequency: 100 }
- { name: "Visa", period: "Thirty Days", frequency: 100 }
- { name: "Amazon", period: "Thirty Days", frequency: 84.9 }
- { name: "Microsoft", period: "Thirty Days", frequency: 75.6 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Meta", period: "Thirty Days", frequency: 63.8 }
- { name: "Google", period: "Thirty Days", frequency: 63.7 }
- { name: "tcs", period: "Thirty Days", frequency: 58.2 }
- { name: "Cognizant", period: "Three Months", frequency: 100 }
- { name: "Optum", period: "Three Months", frequency: 100 }
- { name: "Spotify", period: "Three Months", frequency: 100 }
- { name: "Deloitte", period: "Three Months", frequency: 89.1 }
- { name: "Infosys", period: "Three Months", frequency: 87.4 }
- { name: "Apple", period: "Three Months", frequency: 69 }
- { name: "Goldman Sachs", period: "Three Months", frequency: 66.3 }
- { name: "Media.net", period: "Six Months", frequency: 100 }
- { name: "Wipro", period: "Six Months", frequency: 100 }
- { name: "Wissen Technology", period: "Six Months", frequency: 100 }
- { name: "Zoho", period: "Six Months", frequency: 100 }
- { name: "Qualcomm", period: "Six Months", frequency: 92 }
- { name: "Turing", period: "Six Months", frequency: 89 }
- { name: "EPAM Systems", period: "Six Months", frequency: 86.7 }
- { name: "Netflix", period: "Six Months", frequency: 83.1 }
- { name: "Tesla", period: "Six Months", frequency: 80.9 }
- { name: "Yandex", period: "Six Months", frequency: 69.2 }
- { name: "Agoda", period: "Six Months", frequency: 65 }
- { name: "IBM", period: "Six Months", frequency: 60.1 }
- { name: "LinkedIn", period: "Six Months", frequency: 54.1 }
sidebar:
label: 'Longest Substring Without Repeating Characters'
badge: 'Medium'
@@ -8,6 +8,15 @@ topics:
- { name: "Sliding Window", slug: "sliding-window" }
video: "gqXU1UyA8pk"
issue: "https://github.com/prdlk/leetcode/issues/52"
companies:
- { name: "Zoho", period: "Thirty Days", frequency: 100 }
- { name: "Google", period: "Thirty Days", frequency: 46.6 }
- { name: "DE Shaw", period: "Three Months", frequency: 91.8 }
- { name: "Infosys", period: "Three Months", frequency: 66 }
- { name: "Bloomberg", period: "Three Months", frequency: 46.8 }
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Amazon", period: "Three Months", frequency: 34.6 }
- { name: "Meta", period: "Six Months", frequency: 32.7 }
sidebar:
label: 'Longest Repeating Character Replacement'
badge: 'Medium'
@@ -9,6 +9,14 @@ topics:
- { name: "Sliding Window", slug: "sliding-window" }
video: "UbyhOgBN834"
issue: "https://github.com/prdlk/leetcode/issues/53"
companies:
- { name: "Meta", period: "Thirty Days", frequency: 63.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 48.9 }
- { name: "tcs", period: "Three Months", frequency: 47.7 }
- { name: "Bloomberg", period: "Three Months", frequency: 46.8 }
- { name: "Microsoft", period: "Three Months", frequency: 33.4 }
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "Apple", period: "Six Months", frequency: 48.1 }
sidebar:
label: 'Permutation in String'
badge: 'Medium'
@@ -6,6 +6,13 @@ topics:
- { name: "Array", slug: "array" }
- { name: "Sliding Window", slug: "sliding-window" }
issue: "https://github.com/prdlk/leetcode/issues/55"
companies:
- { name: "Bloomberg", period: "Thirty Days", frequency: 83.1 }
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "Amazon", period: "Thirty Days", frequency: 55.9 }
- { name: "Google", period: "Thirty Days", frequency: 36.7 }
- { name: "tcs", period: "Three Months", frequency: 57.2 }
- { name: "Meta", period: "Six Months", frequency: 32.7 }
sidebar:
label: 'Maximum Average Subarray I'
badge: 'Easy'
@@ -0,0 +1,55 @@
---
title: '76. Minimum Window Substring'
description: Given two strings s and t of lengths m and n respectively, return the minimum window substring of s such that every character in t (including duplicates) is included in the window. If there is no such substring, return the empty string ""
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
- { name: "Sliding Window", slug: "sliding-window" }
issue: "https://github.com/prdlk/leetcode/issues/57"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 55.9 }
- { name: "Lyft", period: "Three Months", frequency: 93.3 }
- { name: "Microsoft", period: "Three Months", frequency: 48.3 }
- { name: "Meta", period: "Three Months", frequency: 34.5 }
- { name: "Google", period: "Three Months", frequency: 23.9 }
- { name: "Oracle", period: "Six Months", frequency: 78.7 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 44.8 }
- { name: "Bloomberg", period: "Six Months", frequency: 25.5 }
sidebar:
label: 'Minimum Window Substring'
badge: 'Hard'
---
::::warning
Could you find an algorithm that runs in O(m + n) time?
::::
### Example 1:
- Input: `s = "ADOBECODEBANC", t = "ABC"`
- Output: `"BANC"`
- Explanation: The minimum window substring "BANC" includes 'A', 'B', and 'C' from string `t`.
### Example 2:
- Input: `s = "a", t = "a"`
- Output: `"a"`
- Explanation: The entire string `s` is the minimum window.
### Example 3:
- Input: `s = "a", t = "aa"`
- Output: `""`
- Explanation: Both 'a's from `t` must be included in the window. Since the largest window of `s` only has one 'a', return empty string.
### Constraints:
- `m == s.length`
- `n == t.length`
- `1 <= m, n <= 10^5`
- `s` and `t` consist of uppercase and lowercase English letters.
## Solution
```py
class Solution:
def minWindow(self, s: str, t: str) -> str:
```
@@ -42,7 +42,7 @@ The window size never changes. The main task is updating the window state effici
<Frame caption="Sliding Window Fixed" hint="">
![](/sliding-window-fixed-size.svg)
![](../../../public/sliding-window-fixed-size.svg)
</Frame>
@@ -79,7 +79,7 @@ Here the window expands until invalid, then shrinks until valid again.
<Frame caption="Sliding Window Flexible" hint="">
![](/sliding-window-flexible.svg)
![](../../../public/sliding-window-flexible.svg)
</Frame>
@@ -143,7 +143,7 @@ The difficulty comes from keeping the bookkeeping correct as characters enter an
<Frame caption="Frequency-tracking windows" hint="">
![](/sliding-window-frequency.svg)
![](../../../public/sliding-window-frequency.svg)
</Frame>
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Sliding Window", order: 4, icon: "app-window", display: "group" });
@@ -8,6 +8,12 @@ topics:
- { name: "Stack", slug: "stack" }
video: "iu0082c4HDE"
issue: "https://github.com/prdlk/leetcode/issues/69"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Google", period: "Three Months", frequency: 17.3 }
- { name: "Yandex", period: "Six Months", frequency: 59.6 }
- { name: "Meta", period: "Six Months", frequency: 38.7 }
- { name: "Bloomberg", period: "Six Months", frequency: 25.5 }
sidebar:
label: 'Evaluate Reverse Polish Notation'
badge: 'Medium'
@@ -7,6 +7,15 @@ topics:
- { name: "Design", slug: "design" }
video: "qkLl7nAwDPo"
issue: "https://github.com/prdlk/leetcode/issues/68"
companies:
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Meta", period: "Three Months", frequency: 34.5 }
- { name: "Amazon", period: "Three Months", frequency: 28.9 }
- { name: "Google", period: "Three Months", frequency: 28.8 }
- { name: "Odoo", period: "Six Months", frequency: 100 }
- { name: "UiPath", period: "Six Months", frequency: 100 }
- { name: "Infosys", period: "Six Months", frequency: 67.9 }
- { name: "Bloomberg", period: "Six Months", frequency: 45.6 }
sidebar:
label: 'Min Stack'
badge: 'Medium'
@@ -8,6 +8,24 @@ topics:
- { name: "Bracket Sequences", slug: "bracket-sequences" }
video: "WTzjTskDFMg"
issue: "https://github.com/prdlk/leetcode/issues/67"
companies:
- { name: "BlackRock", period: "Thirty Days", frequency: 100 }
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "Amazon", period: "Thirty Days", frequency: 61.3 }
- { name: "Google", period: "Thirty Days", frequency: 42.3 }
- { name: "Accolite", period: "Three Months", frequency: 100 }
- { name: "Intuit", period: "Three Months", frequency: 73.2 }
- { name: "Bloomberg", period: "Three Months", frequency: 68.3 }
- { name: "Meta", period: "Three Months", frequency: 65.3 }
- { name: "tcs", period: "Three Months", frequency: 57.2 }
- { name: "Autodesk", period: "Six Months", frequency: 100 }
- { name: "Criteo", period: "Six Months", frequency: 100 }
- { name: "Odoo", period: "Six Months", frequency: 100 }
- { name: "Tripadvisor", period: "Six Months", frequency: 100 }
- { name: "Virtu Financial", period: "Six Months", frequency: 100 }
- { name: "IBM", period: "Six Months", frequency: 91.1 }
- { name: "Epic Systems", period: "Six Months", frequency: 89.6 }
- { name: "LinkedIn", period: "Six Months", frequency: 85.6 }
sidebar:
label: 'Valid Parentheses'
badge: 'Easy'
@@ -0,0 +1,49 @@
---
title: '224. Basic Calculator'
description: Given a string s representing a valid expression, implement a basic calculator to evaluate it, and return the result of the evaluation
icon: dot
topics:
- { name: "Math", slug: "math" }
- { name: "String", slug: "string" }
- { name: "Stack", slug: "stack" }
- { name: "Recursion", slug: "recursion" }
issue: "https://github.com/prdlk/leetcode/issues/71"
companies:
- { name: "Bloomberg", period: "Three Months", frequency: 54 }
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Amazon", period: "Three Months", frequency: 28.9 }
- { name: "Google", period: "Three Months", frequency: 23.9 }
- { name: "Meta", period: "Six Months", frequency: 24.5 }
sidebar:
label: 'Basic Calculator'
badge: 'Hard'
---
### Example 1:
- Input: `s = "1 + 1"`
- Output: `2`
### Example 2:
- Input: `s = " 2-1 + 2 "`
- Output: `3`
### Example 3:
- Input: `s = "(1+(4+5+2)-3)+(6+8)"`
- Output: `23`
### Constraints:
- `1 <= s.length <= 3 * 10^5`
- `s` consists of digits, '+', '-', '(', ')', and ' '.
- `s` represents a valid expression.
- '+' is not used as a unary operation (i.e., "+1" and "+(2 + 3)" is invalid).
- '-' could be used as a unary operation (i.e., "-1" and "-(2 + 3)" is valid).
- There will be no two consecutive operators in the input.
- Every number and running calculation will fit in a signed 32-bit integer.
## Solution
```py
class Solution:
def calculate(self, s: str) -> int:
```
@@ -6,6 +6,13 @@ topics:
- { name: "String", slug: "string" }
- { name: "Stack", slug: "stack" }
issue: "https://github.com/prdlk/leetcode/issues/70"
companies:
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Google", period: "Three Months", frequency: 28.8 }
- { name: "Amazon", period: "Three Months", frequency: 21.2 }
- { name: "Snowflake", period: "Six Months", frequency: 58.7 }
- { name: "Meta", period: "Six Months", frequency: 50.6 }
- { name: "Microsoft", period: "Six Months", frequency: 24.1 }
sidebar:
label: 'Simplify Path'
badge: 'Medium'
@@ -0,0 +1,62 @@
---
title: Stack
description: 'Items are added and removed from one end, so the last item added is the first item removed.'
hidden: true
---
A stack works like a stack of plates in a cafeteria. You add new plates on top. When you need a plate, you take from the top. The last plate you added is the first one you remove.
This is **Last-In-First-Out (LIFO)**. The most recent item goes out first.
![](../../../public/stack-plates-metaphor.svg)
## Core Stack Operations
A stack has two fundamental operations:
- **Push**: add an item to the top of the stack
- **Pop**: remove the item at the top of the stack
Some stacks have additional operations:
- **Peek**: get the item at the top of the stack without removing it
- **IsEmpty**: check if the stack is empty
![](../../../public/stack-operations-sequence.svg)
```python
# Using Python list as a stack
stack = []
# Push elements
stack.append(10)
stack.append(20)
stack.append(30)
# Stack: [10, 20, 30] (30 is on top)
# Pop element
top = stack.pop() # Returns 30
# Stack: [10, 20]
# Peek at top
if stack:
top_element = stack[-1] # Returns 20 without removing
```
Python lists provide `append()` for pushing and `pop()` for popping. The `peek()` method is not built-in, but you can implement it yourself.
## Last in, First out
The defining characteristic of a stack is LIFO order. Whatever you put in last comes out first. This is different from a queue, which is First-In-First-Out (FIFO).
Add items: A, B, C
- Stack: [A, B, C] (C on top)
Remove items:
- First pop: C
- Second pop: B
- Third pop: A
The order reverses. Items come out in the opposite order they went in.
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Stack", order: 6, icon: "layers", display: "group" });
@@ -8,6 +8,18 @@ topics:
- { name: "Greedy", slug: "greedy" }
video: "UuiTKBwPgAo"
issue: "https://github.com/prdlk/leetcode/issues/44"
companies:
- { name: "Bloomberg", period: "Thirty Days", frequency: 83.1 }
- { name: "Amazon", period: "Thirty Days", frequency: 75.8 }
- { name: "Infosys", period: "Thirty Days", frequency: 68.6 }
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "tcs", period: "Thirty Days", frequency: 67.6 }
- { name: "Ola Cabs", period: "Thirty Days", frequency: 65.6 }
- { name: "Google", period: "Thirty Days", frequency: 62 }
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Goldman Sachs", period: "Three Months", frequency: 57 }
- { name: "Walmart Labs", period: "Six Months", frequency: 70.5 }
- { name: "IBM", period: "Six Months", frequency: 60.1 }
sidebar:
label: 'Container With Most Water'
badge: 'Medium'
@@ -7,6 +7,18 @@ topics:
- { name: "String", slug: "string" }
video: "jJXJ16kPFWg"
issue: "https://github.com/prdlk/leetcode/issues/41"
companies:
- { name: "tcs", period: "Thirty Days", frequency: 58.2 }
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "Bloomberg", period: "Three Months", frequency: 79.9 }
- { name: "Infosys", period: "Three Months", frequency: 66 }
- { name: "Microsoft", period: "Three Months", frequency: 42.1 }
- { name: "Deloitte", period: "Six Months", frequency: 81 }
- { name: "Cognizant", period: "Six Months", frequency: 76.3 }
- { name: "Axon", period: "Six Months", frequency: 75.4 }
- { name: "Apple", period: "Six Months", frequency: 64.8 }
sidebar:
label: 'Valid Palindrome'
badge: 'Easy'
@@ -8,6 +8,22 @@ topics:
- { name: "Sorting", slug: "sorting" }
video: "jzZsG8n2R9A"
issue: "https://github.com/prdlk/leetcode/issues/43"
companies:
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 69.6 }
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "Google", period: "Thirty Days", frequency: 66.8 }
- { name: "Ola Cabs", period: "Thirty Days", frequency: 65.6 }
- { name: "Meta", period: "Thirty Days", frequency: 63.8 }
- { name: "Apple", period: "Three Months", frequency: 80.4 }
- { name: "Infosys", period: "Three Months", frequency: 66 }
- { name: "tcs", period: "Three Months", frequency: 57.2 }
- { name: "Cisco", period: "Six Months", frequency: 100 }
- { name: "Wissen Technology", period: "Six Months", frequency: 100 }
- { name: "eBay", period: "Six Months", frequency: 75.2 }
- { name: "Accenture", period: "Six Months", frequency: 75 }
- { name: "Visa", period: "Six Months", frequency: 69.7 }
- { name: "Goldman Sachs", period: "Six Months", frequency: 53.1 }
sidebar:
label: '3Sum'
badge: 'Medium'
@@ -8,6 +8,14 @@ topics:
- { name: "Binary Search", slug: "binary-search" }
video: "cQ1Oz4ckceM"
issue: "https://github.com/prdlk/leetcode/issues/42"
companies:
- { name: "Amazon", period: "Thirty Days", frequency: 65.8 }
- { name: "Google", period: "Thirty Days", frequency: 46.6 }
- { name: "tcs", period: "Three Months", frequency: 57.2 }
- { name: "Meta", period: "Three Months", frequency: 54.8 }
- { name: "Microsoft", period: "Three Months", frequency: 33.4 }
- { name: "Oracle", period: "Six Months", frequency: 78.7 }
- { name: "Bloomberg", period: "Six Months", frequency: 40.6 }
sidebar:
label: 'Two Sum II - Input Array Is Sorted'
badge: 'Medium'
@@ -7,6 +7,17 @@ topics:
- { name: "Math", slug: "math" }
- { name: "Two Pointers", slug: "two-pointers" }
issue: "https://github.com/prdlk/leetcode/issues/47"
companies:
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "Amazon", period: "Thirty Days", frequency: 48.9 }
- { name: "Google", period: "Thirty Days", frequency: 46.6 }
- { name: "Infosys", period: "Three Months", frequency: 66 }
- { name: "tcs", period: "Three Months", frequency: 57.2 }
- { name: "Meta", period: "Three Months", frequency: 54.8 }
- { name: "Box", period: "Six Months", frequency: 100 }
- { name: "TikTok", period: "Six Months", frequency: 55.2 }
- { name: "Apple", period: "Six Months", frequency: 48.1 }
sidebar:
label: 'Rotate Array'
badge: 'Medium'
@@ -6,6 +6,14 @@ topics:
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "String", slug: "string" }
video: "_d0T_2Lk2qA"
companies:
- { name: "tcs", period: "Thirty Days", frequency: 58.2 }
- { name: "Google", period: "Thirty Days", frequency: 50.2 }
- { name: "Amazon", period: "Thirty Days", frequency: 39.1 }
- { name: "Microsoft", period: "Three Months", frequency: 48.3 }
- { name: "Bloomberg", period: "Three Months", frequency: 46.8 }
- { name: "Meta", period: "Three Months", frequency: 34.5 }
- { name: "Qualcomm", period: "Six Months", frequency: 80.7 }
sidebar:
label: 'Reverse String'
badge: 'Easy'
@@ -8,6 +8,12 @@ topics:
- { name: "Dynamic Programming", slug: "dynamic-programming" }
video: "99RVfqklbCE"
issue: "https://github.com/prdlk/leetcode/issues/45"
companies:
- { name: "Amazon", period: "Three Months", frequency: 34.6 }
- { name: "Bloomberg", period: "Six Months", frequency: 25.5 }
- { name: "Google", period: "Six Months", frequency: 25.4 }
- { name: "Meta", period: "Six Months", frequency: 24.5 }
- { name: "Microsoft", period: "Six Months", frequency: 24.1 }
sidebar:
label: 'Is Subsequence'
badge: 'Easy'
@@ -9,6 +9,20 @@ topics:
- { name: "Stack", slug: "stack" }
- { name: "Monotonic Stack", slug: "monotonic-stack" }
issue: "https://github.com/prdlk/leetcode/issues/211"
companies:
- { name: "Ola Cabs", period: "Thirty Days", frequency: 90.7 }
- { name: "Amazon", period: "Thirty Days", frequency: 80.7 }
- { name: "Goldman Sachs", period: "Thirty Days", frequency: 71.7 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Infosys", period: "Thirty Days", frequency: 68.6 }
- { name: "Google", period: "Thirty Days", frequency: 63.7 }
- { name: "Snowflake", period: "Three Months", frequency: 90.3 }
- { name: "Meta", period: "Three Months", frequency: 58.9 }
- { name: "Microsoft", period: "Three Months", frequency: 57.2 }
- { name: "tcs", period: "Three Months", frequency: 47.7 }
- { name: "Docusign", period: "Six Months", frequency: 100 }
- { name: "Qualcomm", period: "Six Months", frequency: 80.7 }
- { name: "Nvidia", period: "Six Months", frequency: 78.7 }
sidebar:
label: 'Trapping Rain Water'
badge: 'Hard'
@@ -0,0 +1,45 @@
---
title: '80. Remove Duplicates from Sorted Array II'
description: Given an integer array nums sorted in non-decreasing order, remove some duplicates in-place such that each unique element appears at most twice. The relative order of the elements should be kept the same
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Two Pointers", slug: "two-pointers" }
issue: "https://github.com/prdlk/leetcode/issues/48"
companies:
- { name: "Google", period: "Thirty Days", frequency: 29 }
- { name: "Amazon", period: "Three Months", frequency: 34.6 }
- { name: "Meta", period: "Six Months", frequency: 32.7 }
sidebar:
label: 'Remove Duplicates from Sorted Array II'
badge: 'Medium'
---
### Example 1:
- Input: `nums = [1,1,1,2,2,3]`
- Output: `5, nums = [1,1,2,2,3,_]`
- Explanation: Your function should return `k = 5`, with the first five elements of `nums` being `1`, `1`, `2`, `2` and `3` respectively. It does not matter what you leave beyond the returned `k` (hence they are underscores).
### Example 2:
- Input: `nums = [0,0,1,1,1,1,2,3,3]`
- Output: `7, nums = [0,0,1,1,2,3,3,_,_]`
- Explanation: Your function should return `k = 7`, with the first seven elements of `nums` being `0`, `0`, `1`, `1`, `2`, `3` and `3` respectively. It does not matter what you leave beyond the returned `k` (hence they are underscores).
### Constraints:
- `1 <= nums.length <= 3 * 10^4`
- `-10^4 <= nums[i] <= 10^4`
- `nums` is sorted in non-decreasing order.
## Solution
```py
class Solution:
def removeDuplicates(self, nums: list[int]) -> int:
slow = 0
for fast in range(len(nums)):
if slow < 2 or nums[fast] != nums[slow - 2]:
nums[slow] = nums[fast]
slow += 1
return slow
```
@@ -8,6 +8,15 @@ topics:
- { name: "Sorting", slug: "sorting" }
video: "FPCZsG_AkUg"
issue: "https://github.com/prdlk/leetcode/issues/46"
companies:
- { name: "Google", period: "Thirty Days", frequency: 42.3 }
- { name: "Instacart", period: "Three Months", frequency: 100 }
- { name: "Uber", period: "Three Months", frequency: 80 }
- { name: "Bloomberg", period: "Three Months", frequency: 54 }
- { name: "Meta", period: "Three Months", frequency: 49.8 }
- { name: "Amazon", period: "Three Months", frequency: 46 }
- { name: "tcs", period: "Six Months", frequency: 60 }
- { name: "Yandex", period: "Six Months", frequency: 59.6 }
sidebar:
label: 'Squares of a Sorted Array'
badge: 'Easy'
@@ -0,0 +1,162 @@
---
title: Two Pointers
description: 'Two indices move through the data from different positions and meet at the answer.'
hidden: true
---
Most interview problems in this section fall into one of four families:
- **Same direction pointers**: one pointer reads, one pointer writes, or one pointer stays behind another while scanning left to right.
- **Opposite direction pointers**: one pointer starts on the left, one on the right, and each move eliminates impossible answers.
- **Sliding window**: the two pointers define a contiguous interval, and you maintain some property of that interval as the window expands and shrinks.
- **Fast and slow pointers**: both pointers move through a linked structure, but at different speeds or with a fixed gap.
![](../../../public/two-pointers-decision-tree.svg)
## Same Direction
Same-direction pointer problems usually scan from left to right, but the two pointers do different jobs.
Usually:
- one pointer reads the next value from the original input
- one pointer marks where the next kept value should go
Sometimes both pointers move forward together, but one pointer stays behind the other to preserve a gap or a relationship.
### When this family applies
Reach for same-direction pointers when the problem asks you to:
- rewrite an array in-place
- remove or compress elements without extra memory
- keep only the values that satisfy some condition
- maintain a fixed gap between two forward-moving pointers
### Core Invariant
The invariant in this family is usually about the prefix you have already processed.
Typical invariant:
> "Everything before the write pointer is already in the correct final form."
That is what makes these problems feel different from opposite-direction scans. You are not searching from both sides. You are building the answer progressively as you sweep through the input.
### Subtypes in this section
#### 1. Compaction / Overwrite
The slow pointer marks the next output position. The fast pointer scans the input. When the current value should be kept, copy it forward and advance the slow pointer.
![](../../../public/same-direction-compaction.svg)
#### 2. Relative Speed
Both pointers move forward, but one moves twice as fast. The invariant is about their distance and how much of the structure each pointer has consumed.
![](../../../public/same-direction-relative-speed.svg)
#### 3. Fixed Gap
Both pointers move forward while preserving a gap. The invariant is not about a rewritten prefix. It is about the distance between the pointers.
![](../../../public/same-direction-fixed-gap.svg)
### What changes across problems
The family stays the same, but the invariant changes slightly:
- in array compaction, the invariant is about the prefix being correct
- in fast/slow midpoint problems, the invariant is about relative speed
- in fixed-gap problems, the invariant is about distance
So the unifying idea is not "both pointers move right." The unifying idea is:
**"the structure to the left of the pointers already has a known meaning"**
### What to watch out for
Common mistakes:
- Updating the write pointer before copying the current value.
- Forgetting exactly what region is already valid.
- Losing track of whether the invariant is about a prefix, a gap, or relative speed.
As you go through the following problems, pay attention to one question:
**What does the region before the slow pointer mean right now?**
## Opposite Direction
Opposite-direction pointer problems place one pointer on the left and one on the right, then move inward.
The hard part is never the loop itself. **The hard part is justifying which pointer is safe to move.**
### When this family applies
Reach for opposite-direction pointers when the problem asks you to:
- the input is sorted, or can be made monotonic by sorting
- you are comparing a left choice and a right choice
- the problem has symmetry
- moving one side lets you eliminate a set of impossible answers
### Core Invariant
The invariant in this family is usually:
> "All candidate answers outside the current `[left, right]` range have already been ruled out."
That means each move must come with a proof. If you move a pointer without knowing why the discarded cases cannot contain the optimum, the algorithm is just guessing.
### Subtypes in this section
#### 1. Monotonic sum elimination
Because the array is sorted:
- if the sum is too small, moving the left pointer right is the only useful move
- if the sum is too large, moving the right pointer left is the only useful move
Sorted order gives you a monotonic rule.
![](../../../public/opposite-direction-monotonic-sum.svg)
#### 2. Symmetry checking
Here the pointers compare mirrored positions. The movement rule is mostly "move both," except when you must skip punctuation or spaces.
![](../../../public/opposite-direction-symmetry.svg)
#### 3. Bottleneck elimination
This one is important because it looks different from the other two. The key idea is that the shorter wall limits the area. If you keep that shorter wall and decrease the width, you cannot improve the answer. So it is safe to discard that wall.
![](../../../public/opposite-direction-bottleneck.svg)
### Why this is not a sliding window
Opposite-direction scans do not maintain an interval property like "this window is valid."
Instead, each step does one of these:
- rules out sums that are too small or too large
- checks mirrored characters
- discards a bottleneck boundary
The interval itself is just the remaining search space.
### What to watch out for
Common mistakes:
- Using two pointers on an unsorted pair problem without first creating monotonicity.
- Moving the taller wall in Container With Most Water without an elimination proof.
- Calling the method sliding window just because two boundaries exist.
As you read the problems in this family, keep asking:
**What exact set of candidate answers did this pointer move eliminate?**
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Two Pointers", order: 3, icon: "arrow-left-right", display: "group" });
@@ -9,6 +9,35 @@ topics:
- { name: "Doubly-Linked List", slug: "doubly-linked-list" }
video: "7ABFKPK2hD4"
issue: "https://github.com/prdlk/leetcode/issues/87"
companies:
- { name: "NetApp", period: "Thirty Days", frequency: 100 }
- { name: "Adobe", period: "Thirty Days", frequency: 83.5 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 69.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 69.6 }
- { name: "Meta", period: "Thirty Days", frequency: 53.8 }
- { name: "Google", period: "Thirty Days", frequency: 36.7 }
- { name: "TikTok", period: "Three Months", frequency: 100 }
- { name: "Palo Alto Networks", period: "Three Months", frequency: 92.8 }
- { name: "Apple", period: "Three Months", frequency: 88.5 }
- { name: "Goldman Sachs", period: "Three Months", frequency: 66.3 }
- { name: "Microsoft", period: "Three Months", frequency: 53.2 }
- { name: "Airwallex", period: "Six Months", frequency: 100 }
- { name: "Docusign", period: "Six Months", frequency: 100 }
- { name: "Moloco", period: "Six Months", frequency: 100 }
- { name: "Nutanix", period: "Six Months", frequency: 100 }
- { name: "Oracle", period: "Six Months", frequency: 100 }
- { name: "Twitch", period: "Six Months", frequency: 100 }
- { name: "Snap", period: "Six Months", frequency: 92.4 }
- { name: "Qualcomm", period: "Six Months", frequency: 92 }
- { name: "Nvidia", period: "Six Months", frequency: 91.1 }
- { name: "ByteDance", period: "Six Months", frequency: 88.6 }
- { name: "MongoDB", period: "Six Months", frequency: 87.7 }
- { name: "Expedia", period: "Six Months", frequency: 87.5 }
- { name: "eBay", period: "Six Months", frequency: 86.1 }
- { name: "LinkedIn", period: "Six Months", frequency: 74.9 }
- { name: "Visa", period: "Six Months", frequency: 69.7 }
- { name: "Uber", period: "Six Months", frequency: 61.9 }
- { name: "Salesforce", period: "Six Months", frequency: 50.2 }
sidebar:
label: 'LRU Cache'
badge: 'Medium'
@@ -8,6 +8,17 @@ topics:
- { name: "Recursion", slug: "recursion" }
video: "wgFPrzTjm7s"
issue: "https://github.com/prdlk/leetcode/issues/88"
companies:
- { name: "Ola Cabs", period: "Thirty Days", frequency: 95.8 }
- { name: "Bloomberg", period: "Thirty Days", frequency: 83.1 }
- { name: "Google", period: "Thirty Days", frequency: 72.8 }
- { name: "Microsoft", period: "Thirty Days", frequency: 67.9 }
- { name: "Meta", period: "Thirty Days", frequency: 63.8 }
- { name: "Amazon", period: "Thirty Days", frequency: 61.3 }
- { name: "EarnIn", period: "Six Months", frequency: 100 }
- { name: "Jane Street", period: "Six Months", frequency: 100 }
- { name: "Palo Alto Networks", period: "Six Months", frequency: 75.9 }
- { name: "tcs", period: "Six Months", frequency: 43.5 }
sidebar:
label: 'Add Two Numbers'
badge: 'Medium'
Loaded 100 of 296 files, more files were not shown because too many files have changed in this diff. Show more