From b01c3cc5837c8cb60f8a9e4545f741299f8f36f3 Mon Sep 17 00:00:00 2001 From: Bmowville Date: Sat, 6 Jun 2026 23:53:47 -0400 Subject: [PATCH] Add static challenge viewer --- README.md | 1 + docs/challenge-finder.js | 20 ++- docs/challenge-viewer.js | 254 +++++++++++++++++++++++++++++++++++++++ docs/challenge.html | 95 +++++++++++++++ docs/index.html | 3 +- docs/site.css | 180 +++++++++++++++++++++++++++ 6 files changed, 547 insertions(+), 6 deletions(-) create mode 100644 docs/challenge-viewer.js create mode 100644 docs/challenge.html diff --git a/README.md b/README.md index 635f395..0e0bbca 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Short SQL case studies (SQLite/Postgres style): cleaning, aggregation, window fu | --- | --- | | Shareable project overview | [Project site](https://bmowville.github.io/sql-mini-challenges/) | | Find a challenge by topic | [Challenge finder](https://bmowville.github.io/sql-mini-challenges/#challenge-finder) | +| Read a challenge in the browser | [Challenge viewer](https://bmowville.github.io/sql-mini-challenges/challenge.html?id=038_inventory_stockout_risk) | | Try one challenge quickly | [001 Passenger survival](challenges/001_passenger_survival) | | Follow a structured path | [Learning paths](docs/learning-paths.md) | | Suggest a new challenge | [Challenge roadmap](docs/challenge-roadmap.md) | diff --git a/docs/challenge-finder.js b/docs/challenge-finder.js index ca160a7..7e91879 100644 --- a/docs/challenge-finder.js +++ b/docs/challenge-finder.js @@ -1,4 +1,4 @@ -const CHALLENGES = [ +window.SQL_CHALLENGES = [ { id: "001", title: "Passenger survival by class", folder: "001_passenger_survival", skill: "foundations", difficulty: "starter", focus: "Grouping, rates, and segmented aggregation." }, { id: "002", title: "Top customers by total spend", folder: "002_top_customers", skill: "foundations", difficulty: "starter", focus: "Ranking customer totals with window functions." }, { id: "003", title: "Customer retention", folder: "003_customer_retention", skill: "retention", difficulty: "starter", focus: "Repeat customers by month." }, @@ -41,7 +41,9 @@ const CHALLENGES = [ { id: "040", title: "Revenue leakage audit", folder: "040_revenue_leakage_audit", skill: "engineering", difficulty: "advanced", focus: "Invoice, payment, refund, and exception reconciliation." }, ]; -const SKILL_LABELS = { +const CHALLENGES = window.SQL_CHALLENGES; + +window.SQL_SKILL_LABELS = { all: "Any skill area", foundations: "Analytics foundations", retention: "Retention and cohorts", @@ -50,17 +52,25 @@ const SKILL_LABELS = { engineering: "Data engineering SQL", }; -const DIFFICULTY_LABELS = { +const SKILL_LABELS = window.SQL_SKILL_LABELS; + +window.SQL_DIFFICULTY_LABELS = { all: "Any difficulty", starter: "Starter", intermediate: "Intermediate", advanced: "Advanced", }; -function getChallengeUrl(challenge) { +const DIFFICULTY_LABELS = window.SQL_DIFFICULTY_LABELS; + +function getChallengeGitHubUrl(challenge) { return `https://github.com/Bmowville/sql-mini-challenges/tree/main/challenges/${challenge.folder}`; } +function getChallengeViewerUrl(challenge) { + return `challenge.html?id=${encodeURIComponent(challenge.folder)}`; +} + function getMatches(skill, difficulty) { return CHALLENGES.filter((challenge) => { const skillMatches = skill === "all" || challenge.skill === skill; @@ -82,7 +92,7 @@ function renderResults(results, summary, container) {

${challenge.focus}

` diff --git a/docs/challenge-viewer.js b/docs/challenge-viewer.js new file mode 100644 index 0000000..4d14e50 --- /dev/null +++ b/docs/challenge-viewer.js @@ -0,0 +1,254 @@ +const FILES = [ + { key: "readme", label: "README.md", file: "README.md", format: "markdown" }, + { key: "schema", label: "schema.sql", file: "schema.sql", format: "code" }, + { key: "solution", label: "solution.sql", file: "solution.sql", format: "code" }, + { key: "expected", label: "expected.json", file: "expected.json", format: "code" }, +]; + +const RAW_BASE = "https://raw.githubusercontent.com/Bmowville/sql-mini-challenges/main/challenges"; +const GITHUB_BASE = "https://github.com/Bmowville/sql-mini-challenges/tree/main/challenges"; + +function escapeHtml(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function renderInlineMarkdown(value) { + return escapeHtml(value) + .replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1') + .replace(/`([^`]+)`/g, "$1") + .replace(/\*\*([^*]+)\*\*/g, "$1"); +} + +function closeList(html, listType) { + if (listType) { + html.push(``); + } + return null; +} + +function renderMarkdown(markdown) { + const html = []; + const codeLines = []; + let inCode = false; + let listType = null; + + for (const line of markdown.split("\n")) { + if (line.startsWith("```")) { + if (inCode) { + html.push(`
${escapeHtml(codeLines.join("\n"))}
`); + codeLines.length = 0; + inCode = false; + } else { + listType = closeList(html, listType); + inCode = true; + } + continue; + } + + if (inCode) { + codeLines.push(line); + continue; + } + + if (line.trim() === "") { + listType = closeList(html, listType); + continue; + } + + if (line.startsWith("# ")) { + listType = closeList(html, listType); + html.push(`

${renderInlineMarkdown(line.slice(2))}

`); + continue; + } + + if (line.startsWith("## ")) { + listType = closeList(html, listType); + html.push(`

${renderInlineMarkdown(line.slice(3))}

`); + continue; + } + + if (line.startsWith("- ")) { + if (listType !== "ul") { + listType = closeList(html, listType); + html.push("