${challenge.title}
+${challenge.focus}
+ +diff --git a/README.md b/README.md
index 413ddd2..0f06a65 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ Short SQL case studies (SQLite/Postgres style): cleaning, aggregation, window fu
| Goal | Where to go |
| --- | --- |
| 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) |
| 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
new file mode 100644
index 0000000..c1b3a48
--- /dev/null
+++ b/docs/challenge-finder.js
@@ -0,0 +1,138 @@
+const 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." },
+ { id: "004", title: "Cohort retention", folder: "004_cohort_retention", skill: "retention", difficulty: "intermediate", focus: "Cohort offsets and activity rates." },
+ { id: "005", title: "Daily revenue", folder: "005_daily_revenue", skill: "revenue", difficulty: "starter", focus: "Time-based revenue aggregation." },
+ { id: "006", title: "Funnel conversion rates", folder: "006_funnel_conversion", skill: "behavior", difficulty: "starter", focus: "Event funnel counts and conversion rates." },
+ { id: "007", title: "Category revenue share", folder: "007_category_revenue_share", skill: "revenue", difficulty: "starter", focus: "Percent-of-total analysis by category." },
+ { id: "008", title: "Daily revenue with missing dates", folder: "008_daily_revenue_date_spine", skill: "revenue", difficulty: "intermediate", focus: "Date spines and rolling windows." },
+ { id: "009", title: "Top products per category", folder: "009_top_products_per_category", skill: "foundations", difficulty: "intermediate", focus: "Top-N ranking within groups." },
+ { id: "010", title: "Customer LTV", folder: "010_customer_ltv", skill: "foundations", difficulty: "starter", focus: "Customer-level spend, order count, and rank." },
+ { id: "011", title: "Pareto customers", folder: "011_pareto_customers", skill: "revenue", difficulty: "intermediate", focus: "Revenue concentration and cumulative share." },
+ { id: "012", title: "Product return rate", folder: "012_product_return_rate", skill: "behavior", difficulty: "starter", focus: "Product quality metrics from orders and returns." },
+ { id: "013", title: "Monthly revenue growth", folder: "013_monthly_revenue_growth", skill: "revenue", difficulty: "intermediate", focus: "Month-over-month revenue change." },
+ { id: "014", title: "Category monthly growth", folder: "014_category_monthly_growth", skill: "revenue", difficulty: "intermediate", focus: "Month-over-month growth by category." },
+ { id: "015", title: "Sessionization", folder: "015_sessionization", skill: "behavior", difficulty: "advanced", focus: "Session boundaries from event timestamps." },
+ { id: "016", title: "Monthly median order value", folder: "016_monthly_median_order_value", skill: "revenue", difficulty: "advanced", focus: "Median calculations with SQLite windows." },
+ { id: "017", title: "Longest purchase streak", folder: "017_longest_purchase_streak", skill: "behavior", difficulty: "advanced", focus: "Gap-and-islands logic for consecutive months." },
+ { id: "018", title: "Time to repeat purchase", folder: "018_time_to_repeat", skill: "behavior", difficulty: "intermediate", focus: "First and second order timing." },
+ { id: "019", title: "Repeat purchase within 30 days", folder: "019_repeat_within_30d_cohort", skill: "retention", difficulty: "intermediate", focus: "Cohort repeat rates with a time window." },
+ { id: "020", title: "Retention matrix", folder: "020_retention_matrix", skill: "retention", difficulty: "advanced", focus: "Cohort month by age matrix output." },
+ { id: "021", title: "Churned customers", folder: "021_churned_customers", skill: "behavior", difficulty: "starter", focus: "Inactive customers based on recent orders." },
+ { id: "022", title: "Customer reactivation", folder: "022_customer_reactivation", skill: "behavior", difficulty: "intermediate", focus: "Customers returning after inactivity." },
+ { id: "023", title: "Weekly active users", folder: "023_weekly_active_users", skill: "behavior", difficulty: "starter", focus: "Weekly activity counts." },
+ { id: "024", title: "RFM segmentation", folder: "024_rfm_segmentation", skill: "behavior", difficulty: "advanced", focus: "Recency, frequency, and monetary quartiles." },
+ { id: "025", title: "Signup to first purchase", folder: "025_signup_to_first_purchase", skill: "behavior", difficulty: "intermediate", focus: "Lag from signup to first order." },
+ { id: "026", title: "Repeat purchase within 30 days", folder: "026_repeat_purchase_30d", skill: "retention", difficulty: "intermediate", focus: "Repeat purchase rates by cohort." },
+ { id: "027", title: "Weekly retention", folder: "027_weekly_retention", skill: "retention", difficulty: "intermediate", focus: "Week-over-week active customer retention." },
+ { id: "028", title: "Rolling 7-day active users", folder: "028_rolling_7d_active_users", skill: "retention", difficulty: "advanced", focus: "Date spines, regions, and rolling activity." },
+ { id: "029", title: "Cohort weekly retention", folder: "029_cohort_weekly_retention_rolling", skill: "retention", difficulty: "advanced", focus: "Rolling retention averages by cohort." },
+ { id: "030", title: "Top customers per month", folder: "030_top_customers_monthly_ties", skill: "revenue", difficulty: "advanced", focus: "Monthly ties, rank changes, and share of revenue." },
+ { id: "031", title: "Longest consecutive activity streak", folder: "031_longest_activity_streak", skill: "behavior", difficulty: "advanced", focus: "Gap-and-islands streak detection." },
+ { id: "032", title: "Sessionization with inactivity gap", folder: "032_sessionization_30min", skill: "behavior", difficulty: "advanced", focus: "Thirty-minute inactivity session logic." },
+ { id: "033", title: "Session funnel conversion", folder: "033_session_funnel_conversion", skill: "behavior", difficulty: "advanced", focus: "Session-level view, cart, and purchase funnels." },
+ { id: "034", title: "Reactivation cohorts", folder: "034_reactivation_cohorts", skill: "retention", difficulty: "advanced", focus: "Reactivation after gaps and next-month retention." },
+ { id: "035", title: "Subscription renewals and winback", folder: "035_subscription_renewal_winback", skill: "engineering", difficulty: "advanced", focus: "Renewal states, missed renewals, and winbacks." },
+ { id: "036", title: "SCD Type 2 customer dimension", folder: "036_scd2_customer_dimension", skill: "engineering", difficulty: "advanced", focus: "History table design and effective dating." },
+ { id: "037", title: "Incremental fact upsert", folder: "037_incremental_fact_upsert", skill: "engineering", difficulty: "advanced", focus: "Staging-to-fact insert and update patterns." },
+];
+
+const SKILL_LABELS = {
+ all: "Any skill area",
+ foundations: "Analytics foundations",
+ retention: "Retention and cohorts",
+ revenue: "Revenue and growth",
+ behavior: "Product and customer behavior",
+ engineering: "Data engineering SQL",
+};
+
+const DIFFICULTY_LABELS = {
+ all: "Any difficulty",
+ starter: "Starter",
+ intermediate: "Intermediate",
+ advanced: "Advanced",
+};
+
+function getChallengeUrl(challenge) {
+ return `https://github.com/Bmowville/sql-mini-challenges/tree/main/challenges/${challenge.folder}`;
+}
+
+function getMatches(skill, difficulty) {
+ return CHALLENGES.filter((challenge) => {
+ const skillMatches = skill === "all" || challenge.skill === skill;
+ const difficultyMatches = difficulty === "all" || challenge.difficulty === difficulty;
+ return skillMatches && difficultyMatches;
+ });
+}
+
+function renderResults(results, summary, container) {
+ container.innerHTML = results
+ .map(
+ (challenge) => `
+ ${challenge.focus} The roadmap is open for new analytics patterns. Suggest a challenge with the tables, question, and expected output you want to practice.${challenge.title}
+ Request a related challenge
+
Challenge finder
++ Filter the challenge set by topic and difficulty, then open one of the suggested folders with its schema, solution, and expected output. +
+Study paths
diff --git a/docs/site.css b/docs/site.css index ec4866f..e373ab3 100644 --- a/docs/site.css +++ b/docs/site.css @@ -238,6 +238,109 @@ code { background: var(--surface); } +.finder-section { + scroll-margin-top: 24px; +} + +.section-copy { + max-width: 720px; + color: var(--muted); +} + +.finder-panel { + padding: 24px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface); +} + +.finder-controls { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; + gap: 16px; + align-items: end; +} + +.finder-controls label { + display: grid; + gap: 8px; + color: var(--muted); + font-size: 0.9rem; + font-weight: 650; +} + +.finder-controls select { + width: 100%; + min-height: 44px; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--bg); + color: var(--text); + font: inherit; +} + +.finder-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.finder-actions .button { + cursor: pointer; +} + +.finder-summary { + margin: 22px 0 14px; + color: var(--muted); + font-size: 0.95rem; +} + +.result-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.result-card { + display: grid; + gap: 12px; + min-height: 220px; + padding: 18px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--bg); +} + +.result-meta, +.result-footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 10px; + color: var(--muted); + font-size: 0.84rem; +} + +.result-footer { + align-self: end; +} + +.result-footer a { + color: var(--accent); + font-weight: 650; + text-decoration: none; +} + +.result-footer a:hover { + color: var(--accent-strong); +} + +.empty-result { + grid-column: 1 / -1; + min-height: auto; +} + .split { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); @@ -296,9 +399,16 @@ code { } .metrics, - .path-grid { + .path-grid, + .result-grid, + .finder-controls { grid-template-columns: 1fr; } + + .finder-actions .button { + width: 100%; + justify-content: center; + } } @media (prefers-color-scheme: dark) {