diff --git a/README.md b/README.md index 6db31fb..9e6ba16 100644 --- a/README.md +++ b/README.md @@ -1 +1,158 @@ -https://sql-playground.codeadventure.net/ +# SQL Adventure (sql-playground) + +**Live site:** [https://sql-playground.codeadventure.net/](https://sql-playground.codeadventure.net/) + +Interactive SQL curriculum framed as a storyline: you are a junior data analyst at *Stellar Sound Records*, using SQL to explore chart data, label metadata, and reporting-style questions. Everything runs **in the browser**—there is no server-side SQL execution. + +--- + +## What’s in this repo + +| Area | Purpose | +|------|---------| +| **Next.js 14 (App Router)** | UI, routing, static prerendering | +| **`sql.js`** | SQLite compiled to WebAssembly; your queries run locally | +| **`public/curriculum/`** | Lesson definitions (JSON) and database setup (SQL) | +| **`src/config/moduleConfig.js`** | Module list, titles, level counts, storyline blurbs, images | +| **CodeMirror** | SQL editor with syntax highlighting | + +The production build is a **fully static** site (`output: 'export'` in `next.config.mjs`). It can be hosted on any static file host (S3/CloudFront, Netlify, GitHub Pages, nginx, etc.). + +--- + +## How it works (for developers) + +### Routes + +- **`/`** — Landing page and module grid (`src/app/page.js`). +- **`/module/{moduleId}/`** — Module intro / storyline, then continue to level 1 (`src/app/module/[moduleId]/ModuleClient.jsx`). +- **`/module/{moduleId}/{levelId}/`** — Main SQL playground (`src/app/module/[moduleId]/[levelId]/LevelClient.jsx` + `SQLEditorContainer.jsx`). +- **`/module/{moduleId}/complete/`** — End-of-module screen (`src/app/module/[moduleId]/complete/page.js`). + +`trailingSlash: true` is enabled, so URLs are generated with a trailing `/` (e.g. `/module/1/1/`). + +### Curriculum loading + +1. Level content is fetched from **`/curriculum/modules/{moduleId}.json`** (see `src/lib/curriculum/fetchLevel.js`). +2. Schema SQL is loaded from one or more files under **`/curriculum/schemas/`**, depending on module id (`src/lib/curriculum/paths.js` → `curriculumSchemaUrls`). +3. Those files are plain static assets under **`public/curriculum/`**, so they ship with the build and work offline after load. + +### Checking answers + +`src/lib/curriculum/executeUserSql.js` creates two in-memory databases, applies the same schema to both, runs the learner’s query and the **reference solution** from the level JSON, and compares the **final statement’s result rows** (as JSON). Semicolons split statements; only the **last** statement is treated as the graded `SELECT` (earlier statements can set up temp data, etc., mirroring the original “lambda” style semantics noted in that file). + +The **solution query is not removed from the bundle** for the level page (it is kept in a ref for grading); treat curriculum JSON as visible to anyone who inspects the app. This is appropriate for a learning tool, not for hiding answers. + +### SQL WebAssembly + +`package.json` **`postinstall`** copies `sql-wasm.wasm` from `sql.js` into **`public/sql-wasm.wasm`**. The app loads it via `src/lib/curriculum/paths.js` (`sqlWasmUrl()`). After `npm install`, you should have that file under `public/` (it may be gitignored or tracked depending on your workflow—if missing, re-run `npm install`). + +--- + +## Local development + +**Requirements:** Node.js 18+ (LTS recommended) and npm. + +```bash +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). Edit React/curriculum files; the dev server hot-reloads. + +**Lint:** + +```bash +npm run lint +``` + +--- + +## Production build and preview + +```bash +npm run build +``` + +This runs `next build` and emits a static site to the **`out/`** directory (gitignored). With `output: 'export'`, **`npm run start` (`next start`) is not used** for this project—there is no Node server bundle for the exported app. + +Preview the static output locally, for example: + +```bash +npx --yes serve out +``` + +Then open the URL the CLI prints (defaults to port **3000**, so stop `npm run dev` first or pass `-l 4173` to listen on another port). + +--- + +## Deployment + +1. Run **`npm run build`** in CI or on your machine. +2. Upload the contents of **`out/`** to your static host’s root (or to a subpath—see below). +3. Ensure the host serves **`index.html`** for directory paths and that **`_next/`**, **`curriculum/`**, **`sql-wasm.wasm`**, and **`images/`** are deployed with correct MIME types (WASM must be `application/wasm`). + +**Subpath / GitHub Pages:** If the site is not at the domain root, set `basePath` (and usually `assetPrefix`) in `next.config.mjs` to match your path, and set **`NEXT_PUBLIC_BASE_PATH`** to the same value before building so client-side `fetch()` URLs resolve correctly (`src/lib/curriculum/paths.js` documents this). Rebuild after any change. + +The project already uses **`images: { unoptimized: true }`** so static export works without the Next image optimization server. + +--- + +## Adding or editing content + +### New levels in an existing module + +Edit **`public/curriculum/modules/{moduleId}.json`**. Each level object must include (validated in `fetchLevel.js`): + +- `id`, `title`, `task`, `initialCode`, `solution`, `hintMessage`, `successMessage`, `table` + +`table` is either a single table name string or an array of names; the first is used for the initial “preview” `SELECT *`. + +If you change the number of levels, update **`levels`** for that module in **`src/config/moduleConfig.js`** and ensure **`generateStaticParams`** in `src/app/module/[moduleId]/[levelId]/page.jsx` still matches (it derives from `moduleConfig`). + +### New module + +1. Add an entry to **`src/config/moduleConfig.js`** (title, `levels`, storyline, image path). +2. Add **`public/curriculum/modules/{id}.json`** with a `levels` array. +3. If the module needs new tables or different schema composition, extend **`curriculumSchemaUrls`** in **`src/lib/curriculum/paths.js`** and add SQL under **`public/curriculum/schemas/`**. + +### Images + +Storyline art lives under **`public/images/storyline/`**. Paths in `moduleConfig` are case-sensitive on Linux CI—keep filenames and references consistent. + +--- + +## Tech stack (quick reference) + +- **Framework:** Next.js 14.2, React 18 +- **Styling:** Tailwind CSS 3, `tailwindcss-animate` +- **Editor:** `@uiw/react-codemirror`, `@codemirror/lang-sql` +- **Database in browser:** `sql.js` +- **Icons / motion:** `lucide-react`, `framer-motion` +- **UI primitives:** Radix (`@radix-ui/react-scroll-area`, `@radix-ui/react-slot`), local `src/components/ui/*` + +--- + +## Repository layout (high level) + +``` +src/app/ # App Router pages (home, module, level, complete) +src/components/ # Layout, header, storyline, sql-editor UI +src/config/moduleConfig.js +src/lib/curriculum/ # fetch level, schema cache, sql.js runner, URL helpers +public/curriculum/ # modules/*.json, schemas/*.sql (shipped as static files) +public/images/ # logos, storyline artwork +public/sql-wasm.wasm # produced by postinstall from sql.js +``` + +--- + +## Troubleshooting + +| Issue | What to try | +|-------|----------------| +| WASM / SQL fails to load | Confirm `public/sql-wasm.wasm` exists; run `npm install` again | +| 404 on `/curriculum/...` | Ensure `public/curriculum` is present and deployed; check `NEXT_PUBLIC_BASE_PATH` if using a subpath | +| Module or level not found | Align `moduleConfig` keys and JSON `levels[].id` with routes under `public/curriculum/modules/` | + +If something still fails after a clean install, delete `node_modules` and `package-lock.json` only as a last resort, then `npm install` again. diff --git a/next.config.mjs b/next.config.mjs index 97658ff..8488052 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,6 +8,17 @@ const nextConfig = { trailingSlash: true, assetPrefix: '/', basePath: '', + webpack: (config, { isServer }) => { + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + path: false, + crypto: false, + }; + } + return config; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index a3b62b5..58a8a26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,18 +7,13 @@ "": { "name": "sql-playground", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@codemirror/lang-sql": "^6.8.0", - "@radix-ui/react-dialog": "^1.1.2", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.0", "@uiw/codemirror-theme-vscode": "^4.23.5", "@uiw/react-codemirror": "^4.23.5", - "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.4.1", @@ -26,12 +21,11 @@ "next": "14.2.15", "react": "^18", "react-dom": "^18", - "shadcn-ui": "^0.9.4", + "sql.js": "^1.11.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@shadcn/ui": "^0.0.4", "@types/node": "22.7.7", "@types/react": "18.3.11", "eslint": "^8", @@ -52,384 +46,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@antfu/ni": { - "version": "0.21.12", - "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-0.21.12.tgz", - "integrity": "sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==", - "license": "MIT", - "bin": { - "na": "bin/na.mjs", - "nci": "bin/nci.mjs", - "ni": "bin/ni.mjs", - "nlx": "bin/nlx.mjs", - "nr": "bin/nr.mjs", - "nu": "bin/nu.mjs", - "nun": "bin/nun.mjs" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz", - "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.7", - "@babel/parser": "^7.26.8", - "@babel/template": "^7.26.8", - "@babel/traverse": "^7.26.8", - "@babel/types": "^7.26.8", - "@types/gensync": "^1.0.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz", - "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.8", - "@babel/types": "^7.26.8", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", - "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.8.tgz", - "integrity": "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/runtime": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", @@ -442,60 +58,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", - "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.8", - "@babel/types": "^7.26.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz", - "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.8", - "@babel/parser": "^7.26.8", - "@babel/template": "^7.26.8", - "@babel/types": "^7.26.8", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", - "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@codemirror/autocomplete": { "version": "6.18.1", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz", @@ -665,44 +227,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1078,654 +602,40 @@ "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", "license": "MIT" }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", - "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.1", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.2", - "@radix-ui/react-presence": "1.1.1", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.6.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", - "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", - "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-menu": "2.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", - "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", - "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", - "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", - "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", - "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", - "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-icons": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", - "integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x" - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", - "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", - "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.0" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", - "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "node_modules/@radix-ui/react-direction": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1854,85 +764,6 @@ } } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", - "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -1966,42 +797,6 @@ } } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", @@ -2017,48 +812,6 @@ } } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", - "license": "MIT" - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2073,49 +826,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@shadcn/ui": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@shadcn/ui/-/ui-0.0.4.tgz", - "integrity": "sha512-0dtu/5ApsOZ24qgaZwtif8jVwqol7a4m1x5AxPuM1k5wxhqU7t/qEfBGtaSki1R8VlbTQfCj5PAlO45NKCa7Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "5.2.0", - "commander": "^10.0.0", - "execa": "^7.0.0", - "fs-extra": "^11.1.0", - "node-fetch": "^3.3.0", - "ora": "^6.1.2", - "prompts": "^2.4.2", - "zod": "^3.20.2" - }, - "bin": { - "ui": "dist/index.js" - } - }, - "node_modules/@shadcn/ui/node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@shadcn/ui/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2132,48 +842,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@ts-morph/common": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", - "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.12", - "minimatch": "^7.4.3", - "mkdirp": "^2.1.6", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", - "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@types/gensync": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz", - "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==", - "license": "MIT" - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2539,15 +1207,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2618,20 +1277,9 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -2800,18 +1448,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2851,34 +1487,14 @@ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/binary-extensions": { @@ -2893,17 +1509,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2927,62 +1532,6 @@ "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -3018,6 +1567,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3052,16 +1602,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas-confetti": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", - "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==", - "license": "ISC", - "funding": { - "type": "donate", - "url": "https://www.paypal.me/kirilvatev" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3127,48 +1667,12 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3178,12 +1682,6 @@ "node": ">=6" } }, - "node_modules/code-block-writer": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", - "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", - "license": "MIT" - }, "node_modules/codemirror": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", @@ -3233,38 +1731,6 @@ "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -3311,15 +1777,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3378,6 +1835,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3431,18 +1889,6 @@ "dev": true, "license": "MIT" }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3479,27 +1925,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -3525,12 +1956,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/electron-to-chromium": { - "version": "1.5.96", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.96.tgz", - "integrity": "sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==", - "license": "ISC" - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3551,15 +1976,6 @@ "node": ">=10.13.0" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -3747,15 +2163,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4173,19 +2580,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -4232,35 +2626,6 @@ "node": ">=0.10.0" } }, - "node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4319,29 +2684,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4432,18 +2774,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/framer-motion": { "version": "12.4.1", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.1.tgz", @@ -4471,20 +2801,6 @@ } } }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4544,15 +2860,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -4573,27 +2880,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -4829,48 +3115,6 @@ "node": ">= 0.4" } }, - "node_modules/https-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-6.2.1.tgz", - "integrity": "sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4885,6 +3129,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -4923,6 +3168,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -4974,12 +3220,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -5167,18 +3407,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -5277,25 +3505,13 @@ "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "license": "MIT", + "call-bind": "^1.0.7" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-string": { @@ -5346,18 +3562,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -5468,6 +3672,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5476,18 +3681,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5495,12 +3688,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5528,18 +3715,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -5566,15 +3741,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -5640,12 +3806,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5653,34 +3813,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", - "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", - "license": "MIT", - "dependencies": { - "chalk": "^5.0.0", - "is-unicode-supported": "^1.1.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5708,12 +3840,6 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5736,18 +3862,6 @@ "node": ">=8.6" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5765,6 +3879,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5779,21 +3894,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", - "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/motion-dom": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz", @@ -5813,6 +3913,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -5929,49 +4030,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5981,33 +4039,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6162,21 +4193,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6195,68 +4211,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz", - "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==", - "license": "MIT", - "dependencies": { - "chalk": "^5.0.0", - "cli-cursor": "^4.0.0", - "cli-spinners": "^2.6.1", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^1.1.0", - "log-symbols": "^5.1.0", - "stdin-discarder": "^0.1.0", - "strip-ansi": "^7.0.1", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6293,6 +4247,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -6301,30 +4256,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6376,15 +4307,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6596,19 +4518,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6648,110 +4557,41 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-remove-scroll": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", - "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.6", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + ], + "license": "MIT" }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" + "loose-envify": "^1.1.0" }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6761,20 +4601,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6787,22 +4613,6 @@ "node": ">=8.10.0" } }, - "node_modules/recast": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", - "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -6871,6 +4681,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6886,52 +4697,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7023,26 +4788,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -7117,84 +4862,6 @@ "node": ">= 0.4" } }, - "node_modules/shadcn-ui": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/shadcn-ui/-/shadcn-ui-0.9.4.tgz", - "integrity": "sha512-75nqu4+y4mlhNXGfHPoPd1r2fgqGQgSEPzPe8TV39WRinafKHuBX2xkgoQOwu+NhiRPKV5TrRSCZ5ytGrFU1oQ==", - "license": "MIT", - "dependencies": { - "@antfu/ni": "^0.21.4", - "@babel/core": "^7.22.1", - "@babel/parser": "^7.22.6", - "@babel/plugin-transform-typescript": "^7.22.5", - "chalk": "5.2.0", - "commander": "^10.0.0", - "cosmiconfig": "^8.1.3", - "diff": "^5.1.0", - "execa": "^7.0.0", - "fast-glob": "^3.3.2", - "fs-extra": "^11.1.0", - "https-proxy-agent": "^6.2.0", - "lodash": "^4.17.21", - "node-fetch": "^3.3.0", - "ora": "^6.1.2", - "prompts": "^2.4.2", - "recast": "^0.23.2", - "ts-morph": "^18.0.0", - "tsconfig-paths": "^4.2.0", - "zod": "^3.20.2" - }, - "bin": { - "shadcn-ui": "dist/index.js" - } - }, - "node_modules/shadcn-ui/node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/shadcn-ui/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/shadcn-ui/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/shadcn-ui/node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7247,21 +4914,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7271,20 +4923,11 @@ "node": ">=0.10.0" } }, - "node_modules/stdin-discarder": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", - "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", - "license": "MIT", - "dependencies": { - "bl": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/sql.js": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.11.0.tgz", + "integrity": "sha512-GsLUDU3vhOo14Pd5ME0y2te49JQyby6HuoCuadevEV+CGgTUjmYRrm7B7lhRyzOgrmcWmspUfyjNb6sOAEqdsA==", + "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", @@ -7307,15 +4950,6 @@ "node": ">=10.0.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -7515,23 +5149,12 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -7715,12 +5338,6 @@ "node": ">=0.8" } }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7752,16 +5369,6 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, - "node_modules/ts-morph": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", - "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", - "license": "MIT", - "dependencies": { - "@ts-morph/common": "~0.19.0", - "code-block-writer": "^12.0.0" - } - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -7888,7 +5495,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -7922,45 +5529,6 @@ "dev": true, "license": "MIT" }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7971,49 +5539,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8026,24 +5551,6 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8253,12 +5760,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, "node_modules/yaml": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", @@ -8283,15 +5784,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index ebce4f5..988db25 100644 --- a/package.json +++ b/package.json @@ -6,20 +6,15 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "postinstall": "node -e \"const fs=require('fs'),p=require('path');const a=p.join('node_modules','sql.js','dist','sql-wasm.wasm'),b=p.join('public','sql-wasm.wasm');if(fs.existsSync(a))fs.copyFileSync(a,b);\"" }, "dependencies": { "@codemirror/lang-sql": "^6.8.0", - "@radix-ui/react-dialog": "^1.1.2", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.0", "@uiw/codemirror-theme-vscode": "^4.23.5", "@uiw/react-codemirror": "^4.23.5", - "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.4.1", @@ -27,12 +22,11 @@ "next": "14.2.15", "react": "^18", "react-dom": "^18", - "shadcn-ui": "^0.9.4", + "sql.js": "^1.11.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@shadcn/ui": "^0.0.4", "@types/node": "22.7.7", "@types/react": "18.3.11", "eslint": "^8", diff --git a/public/curriculum/modules/1.json b/public/curriculum/modules/1.json new file mode 100644 index 0000000..2fac467 --- /dev/null +++ b/public/curriculum/modules/1.json @@ -0,0 +1,65 @@ +{ + "moduleId": 1, + "levels": [ + { + "id": 1, + "title": "First Day on the Job", + "task": "Your supervisor at Stellar Sound wants a quick look at the latest chart. She asks you to open the whole top_songs table. Show every column with SELECT * FROM top_songs.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT * FROM top_songs;", + "hintMessage": "Write only: SELECT * FROM top_songs.", + "successMessage": "Great job! You showed the full chart table.", + "table": [ + "top_songs" + ] + }, + { + "id": 2, + "title": "Select Specific Column", + "task": "You are asked to help the playlist team at Stellar Sound. They want all song titles in alphabetical order so the team can scan ideas fast. Return track_name in alphabetical order.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT track_name FROM top_songs ORDER BY track_name;", + "hintMessage": "Use SELECT track_name FROM top_songs ORDER BY track_name.", + "successMessage": "Great! The team can read the titles in alphabetical order.", + "table": [ + "top_songs" + ] + }, + { + "id": 3, + "title": "Select Distinct", + "task": "The A&R team at Stellar Sound finds new artists. They want a list of artists on the Top 50 chart with no repeats—each name once. Return column artist, sorted alphabetically by artist.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT DISTINCT artist FROM top_songs ORDER BY artist;", + "hintMessage": "Use DISTINCT artist and ORDER BY artist so the list order is the same every time.", + "successMessage": "Nice work! The A&R team has a clean artist list.", + "table": [ + "top_songs" + ] + }, + { + "id": 4, + "title": "Sorting with ORDER BY", + "task": "Your boss is curious about how release timing affects popularity. She asks you to organize the songs by release date so she can look for trends. Provide a list that shows the most recent release dates first.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT track_name, artist, release_date FROM top_songs ORDER BY release_date DESC;", + "hintMessage": "Use columns in this order: track_name, artist, release_date. Then ORDER BY release_date DESC.", + "successMessage": "Great work! Your boss can now see which songs are newest first.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "Select with ORDER BY", + "task": "Another intern at Stellar Sound is building an artist spotlight wall. She wants to see only the track_name and artist. Sort by artist and track_name.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT track_name, artist FROM top_songs ORDER BY artist, track_name;", + "hintMessage": "SELECT track_name, artist then ORDER BY artist, track_name so the order is the same every time.", + "successMessage": "Great! Your intern can build the wall with this sorted list.", + "table": [ + "top_songs" + ] + } + ] +} diff --git a/public/curriculum/modules/10.json b/public/curriculum/modules/10.json new file mode 100644 index 0000000..af24692 --- /dev/null +++ b/public/curriculum/modules/10.json @@ -0,0 +1,67 @@ +{ + "moduleId": 10, + "levels": [ + { + "id": 1, + "title": "Artists with more than one hit (HAVING)", + "task": "The CEO wants to know how many artists have 2 or more songs on the top 50 list. Show the artist and the count of songs (song_count). Use HAVING to filter the aggregate functions after the GROUP BY.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT artist, COUNT(*) AS song_count\nFROM top_songs\nGROUP BY artist\nHAVING COUNT(*) >= 2", + "hintMessage": "WHERE runs before GROUP BY. HAVING runs after the count—use it to drop small groups. Use HAVING COUNT(*) >= 2 at the end of your query.", + "successMessage": "Great! HAVING keeps only artists with enough songs on the chart.", + "table": [ + "top_songs" + ] + }, + { + "id": 2, + "title": "Very new acts (MIN + HAVING)", + "task": "A&R at Stellar Sound wants artists whose first release date is after 2024-03-01. Show artist and the earliest release date (earliest_release). Use HAVING on that MIN date.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT artist, MIN(release_date) AS earliest_release\nFROM top_songs\nGROUP BY artist\nHAVING MIN(release_date) > '2024-03-01'", + "hintMessage": " Use HAVING MIN(release_date) > '2024-03-01' at the end of your query.", + "successMessage": "Great! Every artist here is new on the chart after that date.", + "table": [ + "top_songs" + ] + }, + { + "id": 3, + "title": "CASE: viral or rising", + "task": "The social media team wants simple categories for songs released by Summit Records. Join top_songs to album_info on album_name. Show track_name, spotify_streams, and a new column called stream_status. Use CASE: if spotify_streams is 2000000000 or more, stream_status should be 'Viral'. Otherwise, stream_status should be 'Rising'. Only include songs where the label is 'Summit Records'.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT t.track_name,\n t.spotify_streams,\n CASE\n WHEN t.spotify_streams >= 2000000000 THEN 'Viral'\n ELSE 'Rising'\n END AS stream_status\nFROM top_songs t\nINNER JOIN album_info a ON t.album_name = a.album_name\nWHERE a.label = 'Summit Records';", + "hintMessage": "CASE checks each row one at a time. Use WHEN spotify_streams >= 2000000000 THEN 'Viral'. Use ELSE 'Rising' for everything else.", + "successMessage": "Great work! You used a JOIN to filter by label and CASE to create readable stream categories.", + "table": [ + "top_songs", + "album_info" + ] + }, + { + "id": 4, + "title": "CASE: release quarter", + "task": "The analytics team wants to group 2024 songs by release quarter. Show track_name, release_date, and a new column called release_quarter. Use CASE to create these labels: dates from January through March = 'Quarter 1', April through June = 'Quarter 2', July through September = 'Quarter 3', and October through December = 'Quarter 4'. Only include songs released in 2024. ORDER BY release_date.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT track_name,\n release_date,\n CASE\n WHEN release_date BETWEEN '2024-01-01' AND '2024-03-31' THEN 'Quarter 1'\n WHEN release_date BETWEEN '2024-04-01' AND '2024-06-30' THEN 'Quarter 2'\n WHEN release_date BETWEEN '2024-07-01' AND '2024-09-30' THEN 'Quarter 3'\n ELSE 'Quarter 4'\n END AS release_quarter\nFROM top_songs\nWHERE release_date LIKE '2024-%'\nORDER BY release_date;", + "hintMessage": "Start your CASE with: WHEN release_date BETWEEN '2024-01-01' AND '2024-03-31' THEN 'Quarter 1'", + "successMessage": "Great work! CASE can turn raw dates into simple business categories like quarters.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "CASE and REPLACE with labels", + "task": "The playlist team is reviewing 2024 songs from Summit Records. Join top_songs to album_info on album_name. Show track_name, a cleaned-up artist name called artist_clean, and a new column called song_length. Use REPLACE to shorten every 'Kendrick Lamar' to 'K. Lamar'. Use CASE: if duration_ms is 200000 or more, song_length should be 'Long'. Otherwise, song_length should be 'Short'. Only include songs where the label is 'Summit Records'.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT t.track_name,\n REPLACE(t.artist, 'Kendrick Lamar', 'K. Lamar') AS artist_clean,\n CASE\n WHEN t.duration_ms >= 200000 THEN 'Long'\n ELSE 'Short'\n END AS song_length\nFROM top_songs t\nINNER JOIN album_info a ON t.album_name = a.album_name\nWHERE a.label = 'Summit Records'\n AND t.release_date LIKE '2024-%';", + "hintMessage": "REPLACE changes the artist name text. CASE checks whether duration_ms is 200000 or more.", + "successMessage": "Awesome work! You combined a JOIN, REPLACE, and CASE in one query.", + "table": [ + "top_songs", + "album_info" + ] + } + ] +} diff --git a/public/curriculum/modules/2.json b/public/curriculum/modules/2.json new file mode 100644 index 0000000..ce6d26d --- /dev/null +++ b/public/curriculum/modules/2.json @@ -0,0 +1,65 @@ +{ + "moduleId": 2, + "levels": [ + { + "id": 1, + "title": "Select with WHERE", + "task": "The promotions team at Stellar Sound is making a highlight video for Sabrina Carpenter. List every column for all her songs on the chart so they can prepare the clips.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT * FROM top_songs WHERE artist = 'Sabrina Carpenter';", + "hintMessage": "Use WHERE artist = 'Sabrina Carpenter'.", + "successMessage": "Great! You listed all of her songs.", + "table": [ + "top_songs" + ] + }, + { + "id": 2, + "title": "Select with WHERE Greater Than", + "task": "Your manager at Stellar Sound wants the biggest streaming hits: show all columns for all songs with more than 2 billion Spotify streams.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT * FROM top_songs WHERE spotify_streams > 2000000000;", + "hintMessage": "Use WHERE spotify_streams > 2000000000 (9 zeroes).", + "successMessage": "Well done! These are the strongest numbers on the chart.", + "table": [ + "top_songs" + ] + }, + { + "id": 3, + "title": "Select with WHERE Dates", + "task": "A famous blogger is writing a blog post about music before covid and how covid shifted the music industry. Can you find all songs released before January 1st, 2020? Show the track_name, artist, and release_date.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT track_name, artist, release_date FROM top_songs WHERE release_date < '2020-01-01';", + "hintMessage": "Compare dates with < '2020-01-01'.", + "successMessage": "Nice work! Now they’ve got all the data they need.", + "table": [ + "top_songs" + ] + }, + { + "id": 4, + "title": "Select with WHERE for Two Elements", + "task": "Your team is putting together a report of recommended artists for an Outdoor Music Festival, but they want to leave out any artists in the Country or Folk genres. Return only the unique artist names whose genres are not Country or Folk.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT DISTINCT artist FROM top_songs WHERE genre != 'Country' AND genre != 'Folk';", + "hintMessage": "Use DISTINCT artist, filter with genre != 'Country' AND genre != 'Folk'.", + "successMessage": "Great! The festival list has no Country or Folk artists.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "Select with WHERE for 2 Different Elements", + "task": "The magazine team is writing about Morgan Wallen’s biggest album. Show the track_name, artist, and album_name for songs where the artist is Morgan Wallen and the album is \"One Thing At A Time\".", + "initialCode": "-- Write your query here\n", + "solution": "SELECT track_name, artist, album_name FROM top_songs WHERE artist = 'Morgan Wallen' AND album_name = 'One Thing At A Time';", + "hintMessage": "Use AND for artist and album_name.", + "successMessage": "Perfect! Now the feature can highlight just the right songs.", + "table": [ + "top_songs" + ] + } + ] +} diff --git a/public/curriculum/modules/3.json b/public/curriculum/modules/3.json new file mode 100644 index 0000000..17259f3 --- /dev/null +++ b/public/curriculum/modules/3.json @@ -0,0 +1,65 @@ +{ + "moduleId": 3, + "levels": [ + { + "id": 1, + "title": "Filter with BETWEEN for Dates", + "task": "The planning team wants to study music trends in 2023. Show all columns for every song released between January 1, 2023 and December 31, 2023.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT * FROM top_songs WHERE release_date BETWEEN '2023-01-01' AND '2023-12-31';", + "hintMessage": "Use BETWEEN '2023-01-01' AND '2023-12-31' on release_date.", + "successMessage": "Good work! You listed only the 2023 songs.", + "table": [ + "top_songs" + ] + }, + { + "id": 2, + "title": "Using LIKE with Wildcards", + "task": "The marketing team is making a pink-themed playlist poster. Find song titles that contain the word 'pink'.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT track_name FROM top_songs WHERE track_name LIKE '%pink%';", + "hintMessage": "Try WHERE track_name LIKE '%pink%'.", + "successMessage": "Nice! The pink playlist titles are ready.", + "table": [ + "top_songs" + ] + }, + { + "id": 3, + "title": "Albums Starting with f", + "task": "For a February magazine spread, the design team needs every song where the album name starts with the letter F, show all columns for each song.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT * FROM top_songs WHERE album_name LIKE 'F%';", + "hintMessage": "Use LIKE 'F%'.", + "successMessage": "Done! You’ve filtered the songs for the design feature.", + "table": [ + "top_songs" + ] + }, + { + "id": 4, + "title": "Songs Released in May 2024", + "task": "To celebrate the start of spring, the marketing team needs every song released in May 2024. Show all columns for each song released in May 2024.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT * FROM top_songs WHERE release_date LIKE '2024-05-%';", + "hintMessage": "Use LIKE with a date pattern like '2024-05-%'.", + "successMessage": "Perfect! The May playlist is ready to go.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "Select with IN", + "task": "You’re building a comparison between Taylor Swift, Kendrick Lamar, and Morgan Wallen. Show all columns for all songs by these artists.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT * FROM top_songs WHERE artist IN ('Taylor Swift', 'Kendrick Lamar', 'Morgan Wallen');", + "hintMessage": "Use artist IN ('Taylor Swift', 'Kendrick Lamar', 'Morgan Wallen').", + "successMessage": "Great! All three artists are in one result table.", + "table": [ + "top_songs" + ] + } + ] +} diff --git a/public/curriculum/modules/4.json b/public/curriculum/modules/4.json new file mode 100644 index 0000000..879aab1 --- /dev/null +++ b/public/curriculum/modules/4.json @@ -0,0 +1,65 @@ +{ + "moduleId": 4, + "levels": [ + { + "id": 1, + "title": "Total Spotify Streams", + "task": "Your boss wants you to calculate one number: the sum of all Spotify streams on the Top 50 chart. Return one column named total_spotify_streams (use SUM).", + "initialCode": "-- Write your query here\n", + "solution": "SELECT SUM(spotify_streams) AS total_spotify_streams FROM top_songs;", + "hintMessage": "Use SUM() to calculate the total and rename the column to total_spotify_streams.", + "successMessage": "Nice! Now your boss has the total numbers.", + "table": [ + "top_songs" + ] + }, + { + "id": 2, + "title": "Shortest Song by Artist", + "task": "The social team is making a TikTok about Benson Boone. Show his shortest song length by using MIN() and naming the column shortest_song.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT MIN(duration_ms) AS shortest_song FROM top_songs WHERE artist = 'Benson Boone';", + "hintMessage": "Use MIN() to calculate the shortest song length and name the column shortest_song. Filter with WHERE artist = 'Benson Boone'.", + "successMessage": "Perfect, now the team can make the TikTok.", + "table": [ + "top_songs" + ] + }, + { + "id": 3, + "title": "Song Count by Genre", + "task": "A genre specialist wants to know how many songs are in each genre. Show each genre and the count of songs using COUNT(*). Rename the column song_count.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT genre, COUNT(*) AS song_count FROM top_songs GROUP BY genre;", + "hintMessage": "Use COUNT(*) AS song_count, group the results by genre, and name the aggregated column song_count.", + "successMessage": "Perfect! The label can see how many songs sit in each genre.", + "table": [ + "top_songs" + ] + }, + { + "id": 4, + "title": "Average Song Duration by Album", + "task": "Audio Engineers want to know the average song length per album. Show album_name and the average song length using AVG().", + "initialCode": "-- Write your query here\n", + "solution": "SELECT album_name, AVG(duration_ms) FROM top_songs GROUP BY album_name;", + "hintMessage": "Show the album_name and the average song length using AVG(duration_ms). Group the results by album_name.", + "successMessage": "Great! The audio engineers can plan timing with these averages.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "Most Recent Release by Artist", + "task": "Leadership wants a list of the most recent releases by each artist. Show the artist name and their most recent release date, sorted with the newest release date at the top.", + "initialCode": "-- Write your query here\n", + "solution": "SELECT artist, MAX(release_date) AS max_release_date FROM top_songs GROUP BY artist ORDER BY max_release_date DESC;", + "hintMessage": "Use MAX(release_date) to find the most recent release date for each artist. Group the results by artist and sort by the release date in descending order.", + "successMessage": "Great! This list helps the team spot who released music most recently.", + "table": [ + "top_songs" + ] + } + ] +} diff --git a/public/curriculum/modules/5.json b/public/curriculum/modules/5.json new file mode 100644 index 0000000..9dc03ed --- /dev/null +++ b/public/curriculum/modules/5.json @@ -0,0 +1,67 @@ +{ + "moduleId": 5, + "levels": [ + { + "id": 1, + "title": "Build the scouting roster (CREATE TABLE)", + "task": "Stellar Sound is launching a new-artist scouting program and the database is empty. Create the new_artists table with the following columns: id, name, genre, country, signed_date. Use your best judgement for the data types.", + "initialCode": "CREATE TABLE new_artists (...);\n\nSELECT * FROM new_artists;", + "solution": "CREATE TABLE new_artists (id INTEGER PRIMARY KEY, name TEXT, genre TEXT, country TEXT, signed_date DATE);", + "hintMessage": "Use CREATE TABLE name (col TYPE, col TYPE, ...);. Don't forget to add your primary key column.", + "successMessage": "Nice! new_artists is live.", + "schema": "module5_lesson1.sql" + }, + { + "id": 2, + "title": "Sign one new artist (INSERT one row)", + "task": "A&R just signed a new pop artist out of the United States. Add ONE row to new_artists with id 1, name 'Echo Park', genre 'Pop', country 'United States', signed_date '2026-06-01'.", + "initialCode": "INSERT INTO new_artists (...)\nVALUES (...);\n\nSELECT * FROM new_artists;", + "solution": "INSERT INTO new_artists (id, name, genre, country, signed_date) VALUES (1, 'Echo Park', 'Pop', 'United States', '2026-06-01');\nSELECT * FROM new_artists;", + "hintMessage": "Use quotes around text and date values. List the columns inside the INSERT in the same order as the VALUES.", + "successMessage": "Great! Echo Park is officially on the roster.", + "schema": "module5_lesson2.sql", + "table": [ + "new_artists" + ] + }, + { + "id": 3, + "title": "Showcase signing (INSERT multiple rows)", + "task": "After Friday's showcase, three artists signed within a week. Insert all three into new_artists in a SINGLE INSERT statement (one INSERT, multiple VALUES rows). \nArtist 2: River Lights, Indie, Cambodia, 2026-06-15\nArtist 3: Crown Heights, Hip-Hop, England, 2026-06-18\nArtist 4: Luna Ray, R&B, Australia, 2026-06-20", + "initialCode": "INSERT INTO new_artists (...)\nVALUES\n (...),\n (...),\n (...);\n\nSELECT * FROM new_artists;", + "solution": "INSERT INTO new_artists (id, name, genre, country, signed_date) VALUES\n (2, 'River Lights', 'Indie', 'Cambodia', '2026-06-15'),\n (3, 'Crown Heights', 'Hip-Hop', 'England', '2026-06-18'),\n (4, 'Luna Ray', 'R&B', 'Australia', '2026-06-20');\nSELECT * FROM new_artists;", + "hintMessage": "One INSERT can add many rows at once, separated by commas: INSERT INTO t (cols) VALUES (...), (...), (...);. Don't forget single quotes around text and dates.", + "successMessage": "Perfect! All three showcase signings are in the system.", + "schema": "module5_lesson3.sql", + "table": [ + "new_artists" + ] + }, + { + "id": 4, + "title": "Move an artist's home base (UPDATE)", + "task": "Echo Park (id 1) just moved from the United States to Mexico. Change her country to 'Mexico'. End with: SELECT id, name, country FROM new_artists WHERE id = 1;", + "initialCode": "UPDATE new_artists SET ... WHERE ...;\n\nSELECT * FROM new_artists \nWHERE ...;", + "solution": "UPDATE new_artists SET country = 'Mexico' WHERE id = 1;\n\nSELECT * FROM new_artists \nWHERE id = 1;", + "hintMessage": "ALWAYS include a WHERE with an UPDATE statement. Without it, every row in the table changes, so filter on id = 1.", + "successMessage": "Got it — Echo Park's profile now shows Mexico.", + "schema": "module5_lesson4.sql", + "table": [ + "new_artists" + ] + }, + { + "id": 5, + "title": "Close the Australian branch (DELETE)", + "task": "Stellar Sound is closing its Australian office and dropping every artist signed there. DELETE all rows from new_artists who live in Australia.", + "initialCode": "DELETE FROM new_artists WHERE ...;\n\nSELECT * FROM new_artists;", + "solution": "DELETE FROM new_artists WHERE country = 'Australia';\n\nSELECT * FROM new_artists;", + "hintMessage": "Same warning as UPDATE: a DELETE with no WHERE empties the whole table. Filter on country = 'Australia'.", + "successMessage": "Done — Australian roster cleared.", + "schema": "module5_lesson5.sql", + "table": [ + "new_artists" + ] + } + ] +} diff --git a/public/curriculum/modules/6.json b/public/curriculum/modules/6.json new file mode 100644 index 0000000..b8d4946 --- /dev/null +++ b/public/curriculum/modules/6.json @@ -0,0 +1,65 @@ +{ + "moduleId": 6, + "levels": [ + { + "id": 1, + "title": "2024 songs per genre", + "task": "Stellar Sound wants a quick snapshot of 2024: how many chart songs landed in each genre. Sort so genres with the most songs appear first; if two genres tie, list them A–Z by genre name.", + "initialCode": "-- SELECT genre, COUNT(*) AS song_count FROM top_songs WHERE release_date LIKE '2024-%' GROUP BY genre ORDER BY ...\n", + "solution": "SELECT genre, COUNT(*) AS song_count\nFROM top_songs\nWHERE release_date LIKE '2024-%'\nGROUP BY genre\nORDER BY song_count DESC, genre;", + "hintMessage": "Try WHERE release_date LIKE '2024-%' to lock the year, then GROUP BY genre so each count is per genre.", + "successMessage": "Nice! WHERE picks the rows first, then GROUP BY organizes them by genre.", + "table": [ + "top_songs" + ] + }, + { + "id": 2, + "title": "Pop hits over a billion", + "task": "Marketing is making a poster of pop songs with over 1 billion streams. Return each song’s name, artist, and stream count, with the biggest hits at the top.", + "initialCode": "-- WHERE genre = 'Pop' AND spotify_streams >= 1000000000\n", + "solution": "SELECT track_name, artist, spotify_streams\nFROM top_songs\nWHERE genre = 'Pop' AND spotify_streams >= 1000000000\nORDER BY spotify_streams DESC;", + "hintMessage": "Put both tests in one WHERE … AND …. For “at least” a billion, spotify_streams >= 1000000000 does the job.", + "successMessage": "Great! AND keeps only rows that pass BOTH tests.", + "table": [ + "top_songs" + ] + }, + { + "id": 3, + "title": "Chart-wide stats in one row", + "task": "Your boss wants a single summary row for the whole chart: total number of songs, sum of all streams, average streams per song, and the longest track by duration (in milliseconds). No filtering—just one row of four numbers.", + "initialCode": "-- SELECT COUNT(*) AS song_count, SUM(...) AS total_streams, AVG(...) AS avg_streams, MAX(...) AS longest_song FROM top_songs;\n", + "solution": "SELECT\n COUNT(*) AS song_count,\n SUM(spotify_streams) AS total_streams,\n AVG(spotify_streams) AS avg_streams,\n MAX(duration_ms) AS longest_song\nFROM top_songs;", + "hintMessage": "COUNT(*) and SUM(spotify_streams) are two of the calculations. Skip GROUP BY for this one since we are looking at the whole chart.", + "successMessage": "Perfect! One row, four useful numbers—great for a status email.", + "table": [ + "top_songs" + ] + }, + { + "id": 4, + "title": "Pop artists: songs and total streams", + "task": "A&R wants a Pop-only leaderboard: one row per artist showing how many songs they have on the chart and their combined streams. Order by combined streams highest first then by artist name alphabetically.", + "initialCode": "-- SELECT artist, COUNT(*) AS song_count, SUM(spotify_streams) AS total_streams FROM top_songs WHERE genre = 'Pop' GROUP BY artist ORDER BY ...\n", + "solution": "SELECT artist, COUNT(*) AS song_count, SUM(spotify_streams) AS total_streams\nFROM top_songs\nWHERE genre = 'Pop'\nGROUP BY artist\nORDER BY total_streams DESC, artist;", + "hintMessage": "WHERE genre = 'Pop' first, then GROUP BY artist. COUNT(*) and SUM(spotify_streams) give songs vs. combined streams per artist.", + "successMessage": "Strong! You filtered, grouped, and stacked two aggregates in one query.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "Artists on the 2023–2024 chart", + "task": "Stellar Sound is prepping a two-year recap (releases from the start of 2023 through the end of 2024). For each artist, show how many qualifying songs they have and their average streams on those songs. Sort it by artists with the most songs first, then alphabetically by artist name.", + "initialCode": "-- WHERE release_date BETWEEN '2023-01-01' AND '2024-12-31' GROUP BY artist\n", + "solution": "SELECT artist, COUNT(*) AS song_count, AVG(spotify_streams) AS avg_streams\nFROM top_songs\nWHERE release_date BETWEEN '2023-01-01' AND '2024-12-31'\nGROUP BY artist\nORDER BY song_count DESC, artist;", + "hintMessage": "WHERE release_date BETWEEN '2023-01-01' AND '2024-12-31' picks the window (inclusive). You will use COUNT() and AVG() for this query.", + "successMessage": "Great work! You combined a date range filter with two aggregates and a group—that's real reporting SQL.", + "table": [ + "top_songs" + ] + } + ] +} diff --git a/public/curriculum/modules/7.json b/public/curriculum/modules/7.json new file mode 100644 index 0000000..18d34c8 --- /dev/null +++ b/public/curriculum/modules/7.json @@ -0,0 +1,70 @@ +{ + "moduleId": 7, + "levels": [ + { + "id": 1, + "title": "Licensing wants albums and labels information", + "task": "Ops just added a second table called album_info. Each row contains an album name and label. Join the top_songs table to album_info using the album_name column. Show all columns from both tables. Keep every song from top_songs, even if there is no matching album in album_info.", + "initialCode": "SELECT *\nFROM ...\nLEFT JOIN ... ON ...;", + "solution": "SELECT * FROM top_songs LEFT JOIN album_info ON top_songs.album_name = album_info.album_name;", + "hintMessage": "LEFT JOIN the top_songs and album_info tables on the album_name column.", + "successMessage": "Great job! You joined the two tables on the album_name column and selected all the columns from both tables.", + "table": [ + "album_info", + "top_songs" + ] + }, + { + "id": 2, + "title": "Only songs with label rows", + "task": "Someone on the festival marketing squad wants only the songs that have a matching album in album_info. Show all rows where top_songs has a matching album in album_info.", + "initialCode": "SELECT *\nFROM ...\nINNER JOIN ... ON ...;", + "solution": "SELECT * FROM top_songs INNER JOIN album_info ON top_songs.album_name = album_info.album_name;", + "hintMessage": "INNER JOIN the top_songs and album_info tables on the album_name column.", + "successMessage": "Great job! You joined the two tables for albums that have a label.", + "table": [ + "top_songs", + "album_info" + ] + }, + { + "id": 3, + "title": "Top five: track and label", + "task": "Tour ops is printing wristbands for the top 5 songs. For each of those rows, show track_name and label. Keep tracks even when the directory has no row for that album.", + "initialCode": "SELECT ... \nFROM top_songs t\n", + "solution": "SELECT t.track_name, a.label AS label\nFROM top_songs t\nLEFT JOIN album_info a ON t.album_name = a.album_name\nWHERE t.id BETWEEN 1 AND 5;", + "hintMessage": "LEFT JOIN album_info on the album_name column. Check where the id is between 1 and 5.", + "successMessage": "Great job! You joined the two tables for the top 5 songs and selected the track_name and label columns.", + "table": [ + "top_songs", + "album_info" + ] + }, + { + "id": 4, + "title": "Top five with a label match", + "task": "We only have wristbands for the top five chart ranks that already have a label on file. Return every column from top_songs for those ranks (1–5) whose album appears in album_info.", + "initialCode": "--Write your solution here--", + "solution": "SELECT *\nFROM top_songs t\nINNER JOIN album_info a ON t.album_name = a.album_name\nWHERE t.id BETWEEN 1 AND 5;", + "hintMessage": "INNER JOIN album_info on album_name. WHERE t.id BETWEEN 1 AND 5.", + "successMessage": "We only have wristbands for the top five songs that have a matching album in album_info.", + "table": [ + "top_songs", + "album_info" + ] + }, + { + "id": 5, + "title": "Long chart run plus label", + "task": "Radio partnerships wants a full song row for every track that has been on the chart at least 25 weeks and has a matching album in the label directory.", + "initialCode": "--Write your solution here--", + "solution": "SELECT *\nFROM top_songs t\nINNER JOIN album_info a ON t.album_name = a.album_name\nWHERE t.weeks_on_chart >= 25;", + "hintMessage": "INNER JOIN album_info on album_name. WHERE weeks_on_chart >= 25.", + "successMessage": "Radio has full rows for every long chart run that already has label info—ready to pitch without chasing paperwork first.", + "table": [ + "top_songs", + "album_info" + ] + } + ] +} diff --git a/public/curriculum/modules/8.json b/public/curriculum/modules/8.json new file mode 100644 index 0000000..7406269 --- /dev/null +++ b/public/curriculum/modules/8.json @@ -0,0 +1,66 @@ +{ + "moduleId": 8, + "levels": [ + { + "id": 1, + "title": "Fix a typo with REPLACE", + "task": "Kendrick Lamar has rebranded to K. Lamar, so we need to fix the artist column. Replace 'Kendrick Lamar' with 'K. Lamar'. Show track_name and the new column called artist_clean.", + "initialCode": "-- REPLACE(artist, 'Kendrick Lamar', 'K. Lamar') AS artist_clean\n", + "solution": "SELECT track_name,\n REPLACE(artist, 'Kendrick Lamar', 'K. Lamar') AS artist_clean\nFROM top_songs;", + "hintMessage": "REPLACE(artist, 'Kendrick Lamar', 'K. Lamar') AS artist_clean.", + "successMessage": "Great job! You used REPLACE to fix the artist column.", + "table": [ + "top_songs" + ] + }, + { + "id": 2, + "title": "Normalize a label with REPLACE", + "task": "Marketing wants the & in R&B switched to 'and'. From top_songs, return track_name and the new column called genre_display.", + "initialCode": "-- WHERE genre = 'R&B' ... REPLACE(genre, 'R&B', 'R and B') AS genre_display\n", + "solution": "SELECT track_name,\n REPLACE(genre, 'R&B', 'R and B') AS genre_display\nFROM top_songs;", + "hintMessage": "use REPLACE(genre, 'R&B', 'R and B') AS genre_display.", + "successMessage": "Great job! You used REPLACE to fix the genre column.", + "table": [ + "top_songs" + ] + }, + { + "id": 3, + "title": "Friendly labels with CASE", + "task": "The VP of sales wants a clean report of the songs names and their labels. Use a LEFT JOIN to connect top_songs to album_info on album_name. If the label is NULL (missing), show the text 'No label info'. Otherwise, show the real label. Use CASE ... END AS label.", + "initialCode": "SELECT track_name, CASE WHEN ... END AS label ...\n", + "solution": "SELECT t.track_name,\n CASE WHEN a.label IS NULL THEN 'No label info' ELSE a.label END AS label\nFROM top_songs t\nLEFT JOIN album_info a ON t.album_name = a.album_name;", + "hintMessage": "CASE WHEN a.label IS NULL THEN 'No label info' ELSE a.label END AS label.", + "successMessage": "Perfect! CASE turns NULLs into human-readable text for the same report.", + "table": [ + "top_songs", + "album_info" + ] + }, + { + "id": 4, + "title": "Bucket rows with CASE", + "task": "Marketing wants to know how songs are performing based on how long they have been on the chart. Create a new column called chart_segment using CASE: if weeks_on_chart is 40 or more, use 'Long run'; if 20 or more (but under 40), use 'Solid weeks'; otherwise use 'Fresh entry'.", + "initialCode": "-- CASE WHEN weeks_on_chart >= 40 THEN 'Long run' WHEN ... END AS chart_segment\n", + "solution": "SELECT id,\n track_name,\n CASE\n WHEN weeks_on_chart >= 40 THEN 'Long run'\n WHEN weeks_on_chart >= 20 THEN 'Solid weeks'\n ELSE 'Fresh entry'\n END AS chart_segment\nFROM top_songs;", + "hintMessage": "Three branches: >=40, then >=20, then ELSE.", + "successMessage": "Great! CASE is ideal for turning one numeric column into simple story buckets.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "REPLACE and CASE together", + "task": "The playlist team is preparing a report for the CEO. They want artist names to look cleaner on the page, so shorten every 'Kendrick Lamar' to 'K. Lamar' as artist_fixed. They also want an easy way to describe how big each song is based on spotify_streams: songs with 2 billion or more streams should say 'Mega hit', songs with at least 1 billion streams should say 'Big streams', and everything else should say 'Growing'. Show the song name, the cleaned-up artist name, and the stream_category.", + "initialCode": "-- REPLACE(...) AS artist_fixed, CASE WHEN ... END AS stream_story\n", + "solution": "SELECT track_name,\n REPLACE(artist, 'Kendrick Lamar', 'K. Lamar') AS artist_fixed,\n CASE\n WHEN spotify_streams >= 2000000000 THEN 'Mega hit'\n WHEN spotify_streams >= 1000000000 THEN 'Big streams'\n ELSE 'Growing'\n END AS stream_category\nFROM top_songs;", + "hintMessage": "REPLACE shortens the one matching billing name; CASE uses spotify_streams thresholds 2000000000 and 1000000000. Labels: 'Mega hit', 'Big streams', 'Growing'.", + "successMessage": "That’s the full picture—clean text and readable categories in one SELECT.", + "table": [ + "top_songs" + ] + } + ] +} diff --git a/public/curriculum/modules/9.json b/public/curriculum/modules/9.json new file mode 100644 index 0000000..e052456 --- /dev/null +++ b/public/curriculum/modules/9.json @@ -0,0 +1,67 @@ +{ + "moduleId": 9, + "levels": [ + { + "id": 1, + "title": "Harbor Light in the 2024 window", + "task": "Royalties at Stellar Sound are looking at songs released in 2024, but only for albums with the Harbor Light Music label. Return every column from top_songs for rows where label is Harbor Light Music and released in 2024 (from '2024-01-01' through '2024-12-31'). Sort by release_date, then id.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT t.*\nFROM top_songs t\nINNER JOIN album_info a ON t.album_name = a.album_name\nWHERE a.label = 'Harbor Light Music'\n AND t.release_date >= '2024-01-01'\n AND t.release_date <= '2024-12-31'\nORDER BY t.release_date, t.id;", + "hintMessage": "INNER JOIN album_info a ON t.album_name = a.album_name. WHERE a.label = 'Harbor Light Music' AND t.release_date from '2024-01-01' through '2024-12-31'. SELECT t.*. ORDER BY t.release_date, t.id.", + "successMessage": "Good—label metadata plus a year window is a common royalties cut. Here the slice is empty for this sample data.", + "table": [ + "top_songs", + "album_info" + ] + }, + { + "id": 2, + "title": "How many albums per label", + "task": "Label relations want to know how many albums each label has. Show each label and the number of albums that label lists (album_count). Group by label and sort with the largest count first.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT label, COUNT(*) AS album_count\nFROM album_info\nGROUP BY label\nORDER BY album_count DESC;", + "hintMessage": "You only need use the album_info table and don't forget to GROUP BY label.", + "successMessage": "Great—that GROUP BY snapshot is how teams compare partner depth.", + "table": [ + "album_info" + ] + }, + { + "id": 3, + "title": "Riverside songs on the chart", + "task": "Partnerships only wants songs that are signed with a label. Show the count of songs (song_count) signed with Riverside Audio. ", + "initialCode": "-- Write your solution here --", + "solution": "SELECT COUNT(*) AS song_count\nFROM top_songs t\nINNER JOIN album_info a ON t.album_name = a.album_name\nWHERE a.label = 'Riverside Audio';", + "hintMessage": "Only keep rows where label = 'Riverside Audio'", + "successMessage": "Great job! You provided the count of songs signed with Riverside Audio.", + "table": [ + "top_songs", + "album_info" + ] + }, + { + "id": 4, + "title": "Genre change", + "task": "The genre 'Alternative' has changed its name to 'Indie', so we need to use REPLACE to update the genre column. Return the track_name and the new genre column called genre_display for rows where genre is 'Alternative'.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT track_name, REPLACE(genre, 'Alternative', 'Indie') AS genre_display\nFROM top_songs\nWHERE genre = 'Alternative';", + "hintMessage": "REPLACE(genre, 'Alternative', 'Indie') AS genre_display.", + "successMessage": "Great job! You changed the genre from Alternative to Indie.", + "table": [ + "top_songs" + ] + }, + { + "id": 5, + "title": "Song count per artist", + "task": "The CEO wants to know how many songs each artist has. Show the count of songs (song_count) per artist. Group by artist and sort with the largest count first.", + "initialCode": "-- Write your solution here --", + "solution": "SELECT artist, COUNT(*) AS song_count\nFROM top_songs\nGROUP BY artist\nORDER BY song_count DESC;", + "hintMessage": "You only need use the top_songs table and don't forget to GROUP BY artist.", + "successMessage": "Great job! You provided the count of songs per artist.", + "table": [ + "top_songs" + ] + } + ] +} diff --git a/public/curriculum/schemas/album_info.sql b/public/curriculum/schemas/album_info.sql new file mode 100644 index 0000000..3468a96 --- /dev/null +++ b/public/curriculum/schemas/album_info.sql @@ -0,0 +1,41 @@ +-- Label directory for Stellar Sound. Eight labels only; album_name matches top_songs +-- exactly. Ten chart albums are omitted (no row here) so JOIN lessons show gaps—including +-- ranks 3–4 on the top five, which still have no label directory match. +CREATE TABLE album_info ( + album_name TEXT PRIMARY KEY, + label TEXT NOT NULL +); + +INSERT INTO album_info (album_name, label) VALUES + ('american dream', 'Prairie Post Sound'), + ('Blonde', 'Bay Street Entertainment'), + ('DECIDE', 'Neon Tape LLC'), + ('Die With A Smile', 'Cedar Lane Label'), + ('emails i can''t send fwd:', 'Riverside Audio'), + ('eternal sunshine', 'Midnight Circuit'), + ('Fireworks & Rollerblades', 'Riverside Audio'), + ('Flower Boy', 'Bay Street Entertainment'), + ('Good Luck, Babe!', 'Riverside Audio'), + ('greedy', 'Midnight Circuit'), + ('Heading South', 'Summit Records'), + ('HIT ME HARD AND SOFT', 'Riverside Audio'), + ('I Love You.', 'Bay Street Entertainment'), + ('I''ve Tried Everything But Therapy (Part 1)', 'Neon Tape LLC'), + ('Lover', 'Riverside Audio'), + ('Lovin On Me', 'Midnight Circuit'), + ('MILLION DOLLAR BABY', 'Neon Tape LLC'), + ('Not Like Us', 'Harbor Light Music'), + ('One Thing At A Time', 'Summit Records'), + ('Pink Skies', 'Summit Records'), + ('Scared To Start', 'Neon Tape LLC'), + ('Short n'' Sweet', 'Riverside Audio'), + ('Something in the Orange', 'Summit Records'), + ('SOS', 'Bay Street Entertainment'), + ('Stick Season', 'Summit Records'), + ('The Land Is Inhospitable and So Are We', 'Cedar Lane Label'), + ('The Rise and Fall of a Midwest Princess', 'Riverside Audio'), + ('THE TORTURED POETS DEPARTMENT', 'Riverside Audio'), + ('UTOPIA', 'Prairie Post Sound'), + ('VULTURES 1', 'Prairie Post Sound'), + ('WE DON''T TRUST YOU', 'Harbor Light Music'), + ('Zach Bryan', 'Summit Records'); diff --git a/public/curriculum/schemas/module5_lesson1.sql b/public/curriculum/schemas/module5_lesson1.sql new file mode 100644 index 0000000..516052d --- /dev/null +++ b/public/curriculum/schemas/module5_lesson1.sql @@ -0,0 +1,4 @@ +-- Module 5, lesson 1 starts from a truly empty database. The learner +-- runs CREATE TABLE new_artists (...); themselves. Lessons 2-5 each +-- ship their own starting-state schema that picks up where the previous +-- lesson would have left off. diff --git a/public/curriculum/schemas/module5_lesson2.sql b/public/curriculum/schemas/module5_lesson2.sql new file mode 100644 index 0000000..148a763 --- /dev/null +++ b/public/curriculum/schemas/module5_lesson2.sql @@ -0,0 +1,9 @@ +-- Module 5, lesson 2 starts in the state lesson 1 would leave behind: +-- the new_artists table exists but is empty. The learner inserts one row. +CREATE TABLE new_artists ( + id INTEGER PRIMARY KEY, + name TEXT, + genre TEXT, + country TEXT, + signed_date DATE +); diff --git a/public/curriculum/schemas/module5_lesson3.sql b/public/curriculum/schemas/module5_lesson3.sql new file mode 100644 index 0000000..642507d --- /dev/null +++ b/public/curriculum/schemas/module5_lesson3.sql @@ -0,0 +1,13 @@ +-- Module 5, lesson 3 starts in the state lesson 2 would leave behind: +-- new_artists has exactly one row (Echo Park, the artist signed in lesson 2). +-- The learner inserts three more rows in a single INSERT statement. +CREATE TABLE new_artists ( + id INTEGER PRIMARY KEY, + name TEXT, + genre TEXT, + country TEXT, + signed_date DATE +); + +INSERT INTO new_artists (id, name, genre, country, signed_date) VALUES + (1, 'Echo Park', 'Pop', 'United States', '2026-06-01'); diff --git a/public/curriculum/schemas/module5_lesson4.sql b/public/curriculum/schemas/module5_lesson4.sql new file mode 100644 index 0000000..72a3ade --- /dev/null +++ b/public/curriculum/schemas/module5_lesson4.sql @@ -0,0 +1,17 @@ +-- Module 5, lesson 4 starts in the state lesson 3 would leave behind: +-- new_artists holds the four rows built up by lessons 2 and 3 (one signing +-- followed by a three-row showcase signing). The learner runs UPDATE +-- against one of those rows. +CREATE TABLE new_artists ( + id INTEGER PRIMARY KEY, + name TEXT, + genre TEXT, + country TEXT, + signed_date DATE +); + +INSERT INTO new_artists (id, name, genre, country, signed_date) VALUES + (1, 'Echo Park', 'Pop', 'United States', '2026-06-01'), + (2, 'River Lights', 'Indie', 'Cambodia', '2026-06-15'), + (3, 'Crown Heights', 'Hip-Hop', 'England', '2026-06-18'), + (4, 'Luna Ray', 'R&B', 'Australia', '2026-06-20'); diff --git a/public/curriculum/schemas/module5_lesson5.sql b/public/curriculum/schemas/module5_lesson5.sql new file mode 100644 index 0000000..7b60184 --- /dev/null +++ b/public/curriculum/schemas/module5_lesson5.sql @@ -0,0 +1,17 @@ +-- Module 5, lesson 5 starts in the state lesson 4 would leave behind: +-- the same four rows as lesson 4's schema, except Echo Park (id 1) has +-- already been moved from United States to Mexico by the lesson-4 UPDATE. +-- The learner runs DELETE to drop the Australia artist. +CREATE TABLE new_artists ( + id INTEGER PRIMARY KEY, + name TEXT, + genre TEXT, + country TEXT, + signed_date DATE +); + +INSERT INTO new_artists (id, name, genre, country, signed_date) VALUES + (1, 'Echo Park', 'Pop', 'Mexico', '2026-06-01'), + (2, 'River Lights', 'Indie', 'Cambodia', '2026-06-15'), + (3, 'Crown Heights', 'Hip-Hop', 'England', '2026-06-18'), + (4, 'Luna Ray', 'R&B', 'Australia', '2026-06-20'); diff --git a/public/curriculum/schemas/top_songs.sql b/public/curriculum/schemas/top_songs.sql new file mode 100644 index 0000000..c990110 --- /dev/null +++ b/public/curriculum/schemas/top_songs.sql @@ -0,0 +1,65 @@ +-- Top 50 chart slice for Stellar Sound curriculum (stream counts from supplied 2023/2024 chart data). +-- id matches chart rank. weeks_on_chart is synthetic for lesson filters (Heading South streams corrected from typo). +CREATE TABLE top_songs ( + id INTEGER PRIMARY KEY, + track_name TEXT NOT NULL, + artist TEXT NOT NULL, + genre TEXT NOT NULL, + release_date TEXT NOT NULL, + spotify_streams INTEGER NOT NULL, + album_name TEXT NOT NULL, + duration_ms INTEGER NOT NULL, + weeks_on_chart INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO top_songs (id, track_name, artist, genre, release_date, spotify_streams, album_name, duration_ms, weeks_on_chart) VALUES + (1, 'Espresso', 'Sabrina Carpenter', 'Pop', '2024-08-23', 2068273167, 'Short n'' Sweet', 175459, 18), + (2, 'Not Like Us', 'Kendrick Lamar', 'Rap', '2024-05-04', 1332107662, 'Not Like Us', 274192, 22), + (3, 'A Bar Song (Tipsy)', 'Shaboozey', 'Country', '2024-05-31', 1172548056, 'Where I''ve Been, Isn''t Where I''m Going', 171291, 16), + (4, 'I Had Some Help (Feat. Morgan Wallen)', 'Post Malone', 'Country', '2024-08-15', 998827848, 'F-1 Trillion', 178205, 45), + (5, 'MILLION DOLLAR BABY', 'Tommy Richman', 'R&B', '2024-04-26', 1144105483, 'MILLION DOLLAR BABY', 155151, 28), + (6, 'Good Luck, Babe!', 'Chappell Roan', 'Pop', '2024-04-05', 1388956735, 'Good Luck, Babe!', 218423, 21), + (7, 'Beautiful Things', 'Benson Boone', 'Pop', '2024-04-05', 1956419360, 'Fireworks & Rollerblades', 180304, 22), + (8, 'BIRDS OF A FEATHER', 'Billie Eilish', 'Alternative', '2024-05-17', 2279701156, 'HIT ME HARD AND SOFT', 210373, 23), + (9, 'I Remember Everything (feat. Kacey Musgraves)', 'Zach Bryan', 'Country', '2023-08-25', 1021962100, 'Zach Bryan', 227195, 24), + (10, 'Stick Season', 'Noah Kahan', 'Folk', '2022-10-14', 1446397146, 'Stick Season', 182346, 25), + (11, 'Too Sweet', 'Hozier', 'Pop', '2024-08-19', 1381277863, 'Unreal Unearth: Unaired', 251424, 15), + (12, 'Please Please Please', 'Sabrina Carpenter', 'Pop', '2024-08-23', 1328351605, 'Short n'' Sweet', 186365, 16), + (13, 'Lose Control', 'Teddy Swims', 'Latin', '2023-09-15', 1636146020, 'I''ve Tried Everything But Therapy (Part 1)', 210688, 17), + (14, 'Like That', 'Future', 'Electronic', '2024-03-22', 727795685, 'WE DON''T TRUST YOU', 267706, 18), + (15, 'Something in the Orange', 'Zach Bryan', 'Rap', '2022-04-22', 1219335031, 'Something in the Orange', 228013, 19), + (16, 'CARNIVAL', '¥$', 'Indie', '2024-02-09', 719102331, 'VULTURES 1', 264334, 20), + (17, 'Last Night', 'Morgan Wallen', 'Pop', '2023-03-03', 1207385497, 'One Thing At A Time', 163854, 21), + (18, 'End of Beginning', 'Djo', 'R&B', '2022-09-16', 1403768518, 'DECIDE', 159245, 22), + (19, 'See You Again (feat. Kali Uchis)', 'Tyler, The Creator', 'Alternative', '2017-07-21', 2258335081, 'Flower Boy', 180386, 23), + (20, 'Cruel Summer', 'Taylor Swift', 'Pop', '2019-08-23', 2818127493, 'Lover', 178426, 24), + (21, 'My Love Mine All Mine', 'Mitski', 'Rap', '2023-09-15', 1519064478, 'The Land Is Inhospitable and So Are We', 137773, 25), + (22, 'HOT TO GO!', 'Chappell Roan', 'R&B', '2023-09-22', 628667095, 'The Rise and Fall of a Midwest Princess', 184841, 15), + (23, 'Fortnight (feat. Post Malone)', 'Taylor Swift', 'Pop', '2024-04-18', 876252028, 'THE TORTURED POETS DEPARTMENT', 228965, 16), + (24, 'redrum', '21 Savage', 'Country', '2024-01-12', 669638735, 'american dream', 270697, 17), + (25, 'FE!N (feat. Playboi Carti)', 'Travis Scott', 'Pop', '2023-07-28', 1233142673, 'UTOPIA', 191700, 18), + (26, 'we can''t be friends (wait for your love)', 'Ariana Grande', 'Rap', '2024-03-08', 1304163624, 'eternal sunshine', 228639, 19), + (27, 'Cowgirls (feat. ERNEST)', 'Morgan Wallen', 'Country', '2023-03-03', 536825057, 'One Thing At A Time', 181621, 20), + (28, 'Lovin On Me', 'Jack Harlow', 'Pop', '2023-11-10', 891280051, 'Lovin On Me', 138411, 21), + (29, 'Saturn', 'SZA', 'Rap', '2024-02-22', 780936471, 'Saturn', 186191, 22), + (30, 'Heading South', 'Zach Bryan', 'Indie', '2019-09-30', 767085781, 'Heading South', 171692, 23), + (31, 'Thinkin'' Bout Me', 'Morgan Wallen', 'Pop', '2023-03-03', 615876381, 'One Thing At A Time', 177387, 24), + (32, 'Austin (Boots Stop Workin'')', 'Dasha', 'Country', '2024-02-16', 791882761, 'What Happens Now?', 171782, 25), + (33, 'i like the way you kiss me', 'Artemas', 'Pop', '2024-07-11', 1232765244, 'yustyna', 142514, 15), + (34, 'greedy', 'Tate McRae', 'Rap', '2023-09-15', 1699732481, 'greedy', 131872, 16), + (35, 'Sweater Weather', 'The Neighbourhood', 'Pop', '2013-04-22', 3657049019, 'I Love You.', 240400, 17), + (36, 'Snooze', 'SZA', 'Pop', '2022-12-09', 1467941801, 'SOS', 201800, 18), + (37, 'Scared To Start', 'Michael Marcagi', 'R&B', '2024-01-12', 614871463, 'Scared To Start', 159636, 19), + (38, 'You Proof', 'Morgan Wallen', 'Country', '2023-03-03', 831784164, 'One Thing At A Time', 157477, 20), + (39, 'Die With A Smile', 'Lady Gaga', 'Electronic', '2024-08-16', 2152766195, 'Die With A Smile', 251667, 21), + (40, 'Pink + White', 'Frank Ocean', 'Folk', '2016-08-20', 1736505015, 'Blonde', 184516, 22), + (41, 'Pink Skies', 'Zach Bryan', 'Rap', '2024-05-24', 440275160, 'Pink Skies', 194920, 23), + (42, 'Feather', 'Sabrina Carpenter', 'Pop', '2023-03-17', 853866104, 'emails i can''t send fwd:', 185552, 24), + (43, 'I Can Do It With a Broken Heart', 'Taylor Swift', 'Rap', '2024-04-18', 620838342, 'THE TORTURED POETS DEPARTMENT', 218004, 25), + (44, 'Red Wine Supernova', 'Chappell Roan', 'Pop', '2023-09-22', 443128606, 'The Rise and Fall of a Midwest Princess', 192720, 15), + (45, 'Mmhmm', 'BigXthaPlug', 'Pop', '2023-12-01', 353749490, 'THE BIGGEST', 119015, 16), + (46, 'Who', 'Jimin', 'Pop', '2024-07-19', 1566820397, 'MUSE', 170887, 17), + (47, 'Never Lose Me', 'Flo Milli', 'Latin', '2024-03-15', 456154778, 'Fine Ho, Stay', 125901, 18), + (48, 'Slow It Down', 'Benson Boone', 'Rap', '2024-04-05', 695440907, 'Fireworks & Rollerblades', 161831, 19), + (49, 'Taste', 'Sabrina Carpenter', 'Pop', '2024-08-23', 913031818, 'Short n'' Sweet', 157279, 20), + (50, 'Evergreen', 'Richy Mitch & The Coal Miners', 'R&B', '2017-05-17', 923981659, 'RMCM', 87000, 21); diff --git a/public/data/tiktok_music_trends_2026_synthetic.csv b/public/data/tiktok_music_trends_2026_synthetic.csv new file mode 100644 index 0000000..eaebd62 --- /dev/null +++ b/public/data/tiktok_music_trends_2026_synthetic.csv @@ -0,0 +1,31 @@ +sound_id,track_title,artist,genre,mood_tag,bpm,energy_score,first_spike_week,peak_views_millions,avg_watch_pct,hashtag_count,creator_reuse_count,duet_stitch_rate,region_top,rights_risk,brand_safety_score,notes +SND001,Midnight Echo,Luna Vale,synth-pop,hypnotic,118,7.2,2026-W02,12.4,68,18400,920,0.14,NA,low,9,Favored by dance creators; clean stems. +SND002,Neon Static,Kairo + Miko,electronic,chaotic,140,9.1,2026-W05,28.1,52,42000,2100,0.22,EU,medium,7,Spike tied to fitness trend; verify clearance on sample loop. +SND003,Papercut Heart,Nia Rowan,indie,bittersweet,96,4.5,2026-W01,6.8,74,6200,310,0.31,NA,low,10,Strong completion; slower burn than viral peak suggests. +SND004,Clockwork Kiss,The Marbles,pop-punk,rebellious,152,8.4,2026-W03,15.9,49,22100,1800,0.18,APAC,low,8, +SND005,Velvet Algorithm,Studio 47,house,playful,124,6.9,2026-W04,9.2,61,9800,540,0.11,NA,low,9, +SND006,Saltwater Sky,Olive + Tide,folk-electronic,cinematic,88,5.1,2026-W02,4.1,81,3100,120,0.08,EU,low,10,Small volume but elite watch time; good for mood channel. +SND007,404 Feelings,byteboy,hyperpop,ironic,160,8.9,2026-W06,33.5,44,51000,3400,0.26,NA,high,6,Rights flagged in notes from distributor; double-check before promo. +SND008,Golden Hour Riot,Sable Crown,alt-R&B,confident,102,6.3,2026-W03,11.0,66,13300,670,0.15,LATAM,low,9, +SND009,Loop Me In,TikTok Symphony,orchestral-meme,humorous,72,3.2,2026-W05,19.7,58,38000,900,0.41,NA,medium,8,High duet/stitch; meme format may age quickly. +SND010,Run It Back,Jules Park,K-pop influenced,energetic,128,8.0,2026-W01,22.4,55,29000,1600,0.19,APAC,low,8, +SND011,Static Bloom,River North,ambient-pop,calm,92,3.8,2026-W04,3.6,77,2100,95,0.05,NA,low,10,Niche; great for wind-down content blocks. +SND012,Sidechain Serenade,DJ Parcel,UK garage,flirty,132,7.5,2026-W02,8.8,59,11200,880,0.12,EU,low,9, +SND013,Champagne Problems 3000,May Wright,pop,sassy,110,6.0,2026-W06,14.2,63,17500,720,0.17,NA,low,9, +SND014,Glass Cannon,REMNANT,trap-metal,intense,148,9.4,2026-W05,7.9,41,8600,410,0.09,NA,medium,6,Loud mix; brand-sensitive placements only. +SND015,Slow Zoom,Sofia Reyes (fictional),latin-pop,romantic,104,5.6,2026-W03,18.6,70,24000,1300,0.21,LATAM,low,9,Strong hashtag velocity pre-peak. +SND016,Copy/Paste My Heart,CTRL+C,indie-pop,quirky,118,5.9,2026-W04,5.5,69,5400,260,0.24,NA,low,9, +SND017,Blueprint Dreams,Twin Cities AI Choir,experimental,hopeful,100,4.9,2026-W01,2.9,73,1800,80,0.06,NA,low,10,Includes AI-generated vocal credit line; disclose in captions. +SND018,Heatmap Honey,VCR Stars,disco-funk,groovy,116,7.1,2026-W06,10.4,62,15100,950,0.16,NA,low,8, +SND019,No Signal,Operator Unknown,industrial,anxious,138,8.2,2026-W02,6.2,46,4700,220,0.07,EU,medium,7, +SND020,Soft Launch,Sleeve Notes,bedroom pop,wistful,94,4.2,2026-W05,9.1,76,8900,400,0.13,NA,low,10, +SND021,Go Viral Responsibly,Professor Meme,comedy-rap,satirical,112,6.4,2026-W06,25.0,51,45000,2200,0.33,NA,low,8,Satire; avoid if channel avoids politics-adjacent humor. +SND022,Pixel Heart,ARCADE LOVE,chiptune-nostalgia,joyful,140,7.8,2026-W03,13.7,57,19800,1400,0.20,APAC,low,9, +SND023,Afterparty Astronomy,Zed Orion,deep house,mysterious,122,6.6,2026-W04,7.4,60,7700,500,0.10,EU,low,9, +SND024,Replay Tax,The Loops,nu-disco,fun,124,7.0,2026-W01,16.3,54,20500,1100,0.14,NA,low,8, +SND025,Trend Decay,TikTok Symphony,orchestral-meme,humorous,74,3.5,2026-W06,12.8,48,36000,700,0.38,NA,medium,7,Sequel sound to SND009; audience overlap. +SND026,Offline Oasis,Yuki M.,lo-fi,cozy,82,2.9,2026-W02,5.1,84,4200,210,0.04,APAC,low,10, +SND027,Mirrorball Panic,Strobe Society,synthwave,dramatic,128,8.1,2026-W05,20.4,53,26800,1900,0.17,NA,low,8, +SND028,Swipe Left Symphony,Metro Minimalists,classical-remix,elegant,96,4.6,2026-W03,4.4,72,3300,150,0.09,EU,low,10, +SND029,Hashtag Healing,Dr. Groove,afrobeats,optimistic,108,7.3,2026-W04,17.9,67,21700,1500,0.23,AFR,low,9, +SND030,Bassline Bureaucracy,Red Tape DJs,funk,bureaucratic-silly,112,6.1,2026-W06,8.0,61,10100,600,0.20,NA,low,9,Novelty hook; monitor for fatigue after week 2. diff --git a/public/images/storyline/Congratulations.png b/public/images/storyline/Congratulations.png new file mode 100644 index 0000000..375c684 Binary files /dev/null and b/public/images/storyline/Congratulations.png differ diff --git a/public/images/storyline/Module1.png b/public/images/storyline/Module1.png index b89348b..99340fa 100644 Binary files a/public/images/storyline/Module1.png and b/public/images/storyline/Module1.png differ diff --git a/public/images/storyline/Module10.png b/public/images/storyline/Module10.png new file mode 100644 index 0000000..b9ed1b4 Binary files /dev/null and b/public/images/storyline/Module10.png differ diff --git a/public/images/storyline/Module7.png b/public/images/storyline/Module7.png new file mode 100644 index 0000000..e46fc1e Binary files /dev/null and b/public/images/storyline/Module7.png differ diff --git a/public/images/storyline/Module8.png b/public/images/storyline/Module8.png new file mode 100644 index 0000000..d367b5c Binary files /dev/null and b/public/images/storyline/Module8.png differ diff --git a/public/images/storyline/Module9.png b/public/images/storyline/Module9.png new file mode 100644 index 0000000..7c87f96 Binary files /dev/null and b/public/images/storyline/Module9.png differ diff --git a/public/images/storyline/module2.png b/public/images/storyline/module2.png index 10e328d..b4778b7 100644 Binary files a/public/images/storyline/module2.png and b/public/images/storyline/module2.png differ diff --git a/public/images/storyline/module3.png b/public/images/storyline/module3.png index c1756c9..176f83c 100644 Binary files a/public/images/storyline/module3.png and b/public/images/storyline/module3.png differ diff --git a/public/images/storyline/module4.png b/public/images/storyline/module4.png index 81e6e18..5ece8c2 100644 Binary files a/public/images/storyline/module4.png and b/public/images/storyline/module4.png differ diff --git a/public/images/storyline/module5.png b/public/images/storyline/module5.png index 08205c8..07856c8 100644 Binary files a/public/images/storyline/module5.png and b/public/images/storyline/module5.png differ diff --git a/public/images/storyline/module6.png b/public/images/storyline/module6.png index 11e1cc9..b28c4ac 100644 Binary files a/public/images/storyline/module6.png and b/public/images/storyline/module6.png differ diff --git a/public/sql-wasm.wasm b/public/sql-wasm.wasm new file mode 100755 index 0000000..66d4a5d Binary files /dev/null and b/public/sql-wasm.wasm differ diff --git a/src/app/ai-music-trend-challenge/layout.js b/src/app/ai-music-trend-challenge/layout.js new file mode 100644 index 0000000..e503489 --- /dev/null +++ b/src/app/ai-music-trend-challenge/layout.js @@ -0,0 +1,9 @@ +export const metadata = { + title: "AI Music Trend Challenge | SQL Adventure", + description: + "Stellar Sound TikTok channel challenge: download a music trends CSV, use AI with clear prompts, four steps, then a launch graphic.", +}; + +export default function AiMusicTrendChallengeLayout({ children }) { + return children; +} diff --git a/src/app/ai-music-trend-challenge/page.js b/src/app/ai-music-trend-challenge/page.js new file mode 100644 index 0000000..e315cc4 --- /dev/null +++ b/src/app/ai-music-trend-challenge/page.js @@ -0,0 +1,370 @@ +'use client' + +import Link from 'next/link' +import { motion } from 'framer-motion' +import { + ArrowLeft, + ArrowRight, + BarChart3, + Download, + MessageCircle, + Palette, + Sparkles, + Telescope, +} from 'lucide-react' +import { AppLayout } from '../../components/AppLayout' +import { Button } from '../../components/ui/button' +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '../../components/ui/card' + +const CSV_PATH = '/data/tiktok_music_trends_2026_synthetic.csv' + +const promptTips = [ + 'Give context — explain where the data came from and what you are trying to figure out', + 'Be specific — mention exact column names, rows, dates, or categories you want analyzed', + 'Explain how you want the answer shown — bullets, charts, summaries, tables, or step-by-step explanations', + 'Add details — tell AI exactly which columns, comparisons, filters, or trends to analyze', + 'Ask follow up questions — ask AI to explain the results in more detail or find specific examples', +] + +const promptExamples = [ + { + topic: 'Cooking', + fuzzy: 'Make a recipe.', + clearer: + 'Make a 30-minute dinner recipe for 2 people using the chicken, rice, and broccoli I have in my fridge. Keep it kid-friendly and list ingredients with measurements.', + }, + { + topic: 'Travel', + fuzzy: 'Plan a trip.', + clearer: + 'Plan a 3-day weekend trip to Chicago in October for two adults who love food and museums. Budget is $600 total, and we want to walk or take transit.', + }, + { + topic: 'Writing', + fuzzy: 'Write an email.', + clearer: + 'Write a short, polite email to my landlord asking when the broken dishwasher will be fixed. Keep it under 5 sentences and ask for a specific date.', + }, +] + +const missions = [ + { + n: 1, + title: 'Get to know the data', + body: 'Open the CSV and ask AI to explain what is inside. Some of the column names may look confusing at first, and that is okay. AI can describe each one in simple words so you know what you are looking at.', + icon: Telescope, + }, + { + n: 2, + title: 'Look for patterns', + body: 'Now that you know what each column means, ask AI to find patterns across the file — for example: Which genres get the most views? Which moods appear most often? Which regions perform best? Ask for small subsets of data or a few examples so the results are easy to read.', + icon: Sparkles, + }, + { + n: 3, + title: 'Explore two patterns', + body: 'From the patterns you just found, pick the two that interest you the most. Ask AI a few follow-up questions about each one — for example: How strong is this pattern? Are there songs that do not fit? What might explain it? Keep asking until you can describe each pattern in your own words.', + icon: MessageCircle, + }, + { + n: 4, + title: 'Build one chart', + body: 'From the two patterns you just explored, pick the one you find most interesting. Ask AI to create a chart or data visualization that shows the pattern. Have it include titles, labels, and a short caption that explains the chart in one sentence.', + icon: BarChart3, + }, +] + +function StepBadge({ children }) { + return ( + + {children} + + ) +} + +export default function AiMusicTrendChallengePage() { + return ( + +
+ {/* Hero — Stellar Sound storyline */} +
+
+
+ + + Back to home + + +
+ + + Stellar Sound Records + +
+

+ AI Music Trend Challenge +

+

+ Stellar Sound Records is launching a brand-new TikTok music channel. Your job is to act like a music data analyst and figure out which songs should be featured in the first 3 viral videos. +

+ You found a CSV file filled with music trend data. Use AI to analyze the data, discover patterns, and help the team pick the best tracks. +

+ +
+
+
+
+ + {/* Step 1 — Download (same idea as before, simpler copy) */} +
+
+
+ Step 1 +

+ Download the data +

+

+ Download the CSV and upload it to an AI tool like ChatGPT, Claude, or Gemini. +

+ +
+
+
+ + {/* Step 2 — How to write good prompts */} +
+
+
+ Step 2 +

+ Good prompts +

+

+ Aim for AI to do about 90% of the work. Small, clear prompts work better than one + huge vague ask. Use the checklist below to write good prompts. +

+ + + Checklist + + +
    + {promptTips.map((tip) => ( +
  • + + {tip} +
  • + ))} +
+
+
+ +
+

+ Examples +

+

+ Compare a vague prompt to a clearer one. The clearer version gives AI context, + details, and a format to follow. +

+
+ {promptExamples.map((ex) => ( +
+

+ {ex.topic} +

+
+
+

+ Avoid (too vague) +

+

{ex.fuzzy}

+
+
+

+ Try (clearer) +

+

{ex.clearer}

+
+
+
+ ))} +
+
+
+
+
+ + {/* Step 3 — Four missions */} +
+
+
+ Step 3 +

+ Find and explore patterns +

+

+ Now it is time to use what you learned about good prompts on real Stellar Sound + data. Work through these four steps in order. AI will do most of the analysis, and + you guide it with clear questions. By the end, you will have explored the file, + found patterns that stand out, and built one chart to share with the team. +

+
+ {missions.map((m, idx) => { + const Icon = m.icon + return ( + + +
+
+ {m.n} +
+
+
+ +

+ {m.title} +

+
+

{m.body}

+
+
+
+
+ ) + })} +
+
+
+
+ + {/* Step 4 — Final deliverable */} +
+
+
+ + Step 4 + +
+
+ +
+
+

+ Build the launch poster +

+

+ Create the final campaign pitch the Stellar Sound executives will review before launch. You will do this with two prompts: first + ask AI to recommend the songs, then ask AI to design the poster. +

+
+
+
+
+

+ Prompt 1 — Recommend the songs +

+

+ Ask AI to look back at the patterns and trends from your earlier conversations + and recommend 3 songs for the first week of videos. Have it explain in one + short sentence why each song fits the patterns you found. +

+
+
+

+ Prompt 2 — Design the poster +

+

+ Ask AI to create a poster that features those 3 songs. Be clear about both + what it should include and how it should look. +

+
    +
  • + Include: a clear title, the + channel name, and a short line of context for each song +
  • +
  • + Design: tell AI the mood, + color palette, fonts, and layout style you want +
  • +
+
+
+

+ Your final output: one finished + launch poster you can share with the Stellar Sound team. +

+
+ + +
+
+
+
+
+
+ ) +} diff --git a/src/app/congratulations/page.js b/src/app/congratulations/page.js new file mode 100644 index 0000000..7649d44 --- /dev/null +++ b/src/app/congratulations/page.js @@ -0,0 +1,75 @@ +import Image from 'next/image' +import Link from 'next/link' +import { ArrowRight, Sparkles } from 'lucide-react' +import { AppLayout } from '../../components/AppLayout' +import { Button } from '../../components/ui/button' + +export default function CongratulationsPage() { + return ( + +
+
+
+ Celebration illustration for completing the SQL curriculum +
+ +
+

+ Stellar Sound Records +

+

+ Congratulations — you are a data superstar +

+

+ You did it — you’ve officially become a Stellar Sound Data Superstar! Along the way, + you learned how to explore data, filter songs, sort results, group information, connect + tables with JOINs, and build smart categories with CASE statements. Those are real SQL + skills used by analysts, engineers, and data teams every day to answer questions and make + decisions. +

+

+ Over these modules, you worked with music charts, artist reports, playlists, labels, and + streaming data just like a real data analyst would. What started with simple SELECT statements + turned into building full reports and solving multi-step problems. Every query you wrote helped + strengthen the same problem-solving skills professionals use with real company data. +

+ +
+
+ +

+ Introducing the AI Music Trend Challenge +

+
+

+ Now it’s time for the AI Challenge! In this next activity, you’ll use AI as your teammate + to explore the data, ask your own questions, and build custom SQL queries. There won’t always + be one “correct” answer — this is your chance to experiment, get creative, and think like a + real analyst. +

+
+ +
+ + +
+
+
+
+
+ ) +} diff --git a/src/app/module/[moduleId]/[lessonId]/LessonClient.jsx b/src/app/module/[moduleId]/[lessonId]/LessonClient.jsx deleted file mode 100644 index efbf3c2..0000000 --- a/src/app/module/[moduleId]/[lessonId]/LessonClient.jsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client' - -import { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { moduleConfig } from '../../../../config/moduleConfig' -import { SQLEditor } from '../../../../components/sql-editor' - -export default function LessonClient({ params }) { - const router = useRouter() - const { moduleId, lessonId } = params - const [levelData, setLevelData] = useState(null) - - const numericLessonId = parseInt(lessonId) - const moduleData = moduleConfig[moduleId] - - // Fetch level data for the title - useEffect(() => { - const fetchLevelData = async () => { - const moduleLevelID = `${moduleId}${numericLessonId}` - try { - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/leveldata`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ moduleLevelID }), - }) - - if (response.ok) { - const data = await response.json() - setLevelData(data) - } - } catch (error) { - console.error('Error fetching level data:', error) - } - } - - fetchLevelData() - }, [moduleId, numericLessonId]) - - if (!moduleData) { - router.push('/') - return null - } - - const lesson = moduleData.lessons?.find(l => l.id === numericLessonId) - - if (!lesson) { - router.push(`/module/${moduleId}`) - return null - } - - const nextLessonId = numericLessonId + 1 - const hasNextLesson = moduleData.lessons?.some(l => l.id === nextLessonId) - - const handleComplete = () => { - if (hasNextLesson) { - router.push(`/module/${moduleId}/${nextLessonId}`) - } else { - router.push(`/module/${moduleId}/complete`) - } - } - - return ( -
- -
- ) -} \ No newline at end of file diff --git a/src/app/module/[moduleId]/[levelId]/LevelClient.jsx b/src/app/module/[moduleId]/[levelId]/LevelClient.jsx index f895982..02e7c50 100644 --- a/src/app/module/[moduleId]/[levelId]/LevelClient.jsx +++ b/src/app/module/[moduleId]/[levelId]/LevelClient.jsx @@ -1,6 +1,5 @@ 'use client' -import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { moduleConfig } from '../../../../config/moduleConfig' import { SQLEditor } from '../../../../components/sql-editor' @@ -8,61 +7,21 @@ import { SQLEditor } from '../../../../components/sql-editor' export default function LevelClient({ params }) { const router = useRouter() const { moduleId, levelId } = params - const [levelData, setLevelData] = useState(null) - const numericLevelId = parseInt(levelId) + const numericLevelId = parseInt(levelId, 10) const moduleData = moduleConfig[moduleId] - - // Fetch level data for the title - useEffect(() => { - const fetchLevelData = async () => { - const moduleLevelID = `${moduleId}${numericLevelId}` - try { - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/leveldata`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ moduleLevelID }), - }) - - if (response.ok) { - const data = await response.json() - setLevelData(data) - } - } catch (error) { - console.error('Error fetching level data:', error) - } - } - - fetchLevelData() - }, [moduleId, numericLevelId]) if (!moduleData) { router.push('/') return null } - const nextLevelId = numericLevelId + 1 - const hasNextLevel = nextLevelId <= moduleData.levels - - const handleComplete = () => { - if (hasNextLevel) { - router.push(`/module/${moduleId}/${nextLevelId}`) - } else { - router.push(`/module/${moduleId}/complete`) - } - } - return (
) diff --git a/src/app/module/[moduleId]/complete/page.js b/src/app/module/[moduleId]/complete/page.js index 41db223..eebb75f 100644 --- a/src/app/module/[moduleId]/complete/page.js +++ b/src/app/module/[moduleId]/complete/page.js @@ -15,7 +15,6 @@ export default function ModuleComplete({ params }) { const { moduleId } = params const moduleData = moduleConfig[moduleId] || { title: 'Unknown Module', - description: 'This module could not be found.' } const nextModuleId = Object.keys(moduleConfig).find(id => diff --git a/src/app/page.js b/src/app/page.js index 385d941..1a0e43e 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,18 +1,14 @@ 'use client' import Link from 'next/link'; +import Image from 'next/image'; import { moduleConfig, curriculumStoryline } from '../config/moduleConfig'; import { ModuleStorylinePreview } from '../components/ModuleStoryline'; import { AppLayout } from '../components/AppLayout'; import { Button } from '../components/ui/button'; -import { ArrowRight, BookOpen } from 'lucide-react'; +import { ArrowRight, Sparkles } from 'lucide-react'; export default function Home() { - // Function to handle smooth scrolling to modules section - const scrollToModules = () => { - document.getElementById('modules-section').scrollIntoView({ behavior: 'smooth' }); - }; - return ( {/* Hero Section with improved colors */} @@ -24,19 +20,31 @@ export default function Home() { {/* Storyline Container */} -
-
-

Your Story Begins

-
- -
-

- You dream of becoming a famous singer, and to get your foot in the door, you've landed an internship at one of the world's top music labels: Stellar Sound Records. -

- -

- As a junior data analyst, you'll use SQL to explore hit songs, uncover trends, and help the label make decisions—all while secretly hoping your name ends up on this list one day. -

+
+
+
+

+ Your Story Begins +

+
+

+ You dream of becoming a famous singer, and to get your foot in the door, you've landed an internship at one of the world's top music labels: Stellar Sound Records. +

+

+ As a junior data analyst, you'll use SQL to explore hit songs, uncover trends, and help the label make decisions—all while secretly hoping your name ends up on this list one day. +

+
+
+
+ Illustration for your journey at Stellar Sound Records +
@@ -69,15 +77,49 @@ export default function Home() {
{Object.entries(moduleConfig).map(([id, module]) => (
- +
))}
- -
-
- {Object.values(moduleConfig).reduce((acc, module) => acc + module.levels, 0)} total lessons - + +
+
+
+ + + +
+

+ Bonus · AI + data +

+

AI Music Trend Challenge

+
+
+ + Launch a Stellar Sound TikTok channel—prompts, four missions, then a launch graphic. + +
+
+

+ Download trend data for Stellar Sound's new TikTok channel, work through + simple prompt tips and four missions, then design a graphic with your top three + picks. +

+
diff --git a/src/components/IntegratedHeader.jsx b/src/components/IntegratedHeader.jsx index 772c105..fe2b81a 100644 --- a/src/components/IntegratedHeader.jsx +++ b/src/components/IntegratedHeader.jsx @@ -1,88 +1,39 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import Link from 'next/link' import Image from 'next/image' -import { Button } from './ui/button' -import { usePathname, useRouter } from 'next/navigation' -import { Menu, X } from 'lucide-react' +import { BookOpen, Sparkles } from 'lucide-react' import { cn } from '../lib/utils' export function IntegratedHeader() { - const pathname = usePathname() - const router = useRouter() - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) - const [prevScrollPos, setPrevScrollPos] = useState(0) const [visible, setVisible] = useState(true) + const prevScrollY = useRef(0) useEffect(() => { - // Set initial scroll position - setPrevScrollPos(window.scrollY) - - // Function to handle scroll events + prevScrollY.current = window.scrollY + const handleScroll = () => { - const currentScrollPos = window.scrollY - const scrollingUp = prevScrollPos > currentScrollPos - - // Make the header visible when scrolling up or at the top of the page - // or when the mobile menu is open - setVisible(scrollingUp || currentScrollPos < 10 || mobileMenuOpen) - - // Update previous scroll position only if we're not at the top - if (currentScrollPos > 0) { - setPrevScrollPos(currentScrollPos) - } + const y = window.scrollY + const scrollingUp = prevScrollY.current > y + setVisible(scrollingUp || y < 10) + if (y > 0) prevScrollY.current = y } - - // Add scroll event listener with passive option for better performance + window.addEventListener('scroll', handleScroll, { passive: true }) - - // Clean up return () => window.removeEventListener('scroll', handleScroll) - }, [prevScrollPos, mobileMenuOpen]) - - // Close mobile menu when escape key is pressed - useEffect(() => { - const handleEscKey = (event) => { - if (event.key === 'Escape' && mobileMenuOpen) { - setMobileMenuOpen(false) - } - } - - document.addEventListener('keydown', handleEscKey) - return () => document.removeEventListener('keydown', handleEscKey) - }, [mobileMenuOpen]) - - // Prevent scrolling when mobile menu is open - useEffect(() => { - if (mobileMenuOpen) { - document.body.style.overflow = 'hidden' - } else { - document.body.style.overflow = '' - } - - return () => { - document.body.style.overflow = '' - } - }, [mobileMenuOpen]) - - const toggleMobileMenu = () => { - setMobileMenuOpen(!mobileMenuOpen) - // Ensure header is visible when menu is opened - if (!mobileMenuOpen) setVisible(true) - } + }, []) return ( -
- {/* Logo */}
- - {/* Empty space instead of desktop navigation */} -
+
) -} \ No newline at end of file +} diff --git a/src/components/ModuleHomeButton.jsx b/src/components/ModuleHomeButton.jsx deleted file mode 100644 index d8e9bf4..0000000 --- a/src/components/ModuleHomeButton.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import Link from 'next/link' -import { Home } from 'lucide-react' - -export function ModuleHomeButton() { - return ( - - - Home - - ) -} \ No newline at end of file diff --git a/src/components/ModuleStoryline.jsx b/src/components/ModuleStoryline.jsx index a69d2d0..97caea6 100644 --- a/src/components/ModuleStoryline.jsx +++ b/src/components/ModuleStoryline.jsx @@ -3,7 +3,7 @@ import { useState } from 'react' import Image from 'next/image' import Link from 'next/link' -import { motion, AnimatePresence } from 'framer-motion' +import { motion } from 'framer-motion' import { Button } from "./ui/button" import { ArrowRight, ChevronRight, BookOpen } from 'lucide-react' import { moduleConfig, curriculumStoryline } from '../config/moduleConfig' @@ -107,7 +107,7 @@ export function ModuleStoryline({ moduleId, onContinue }) { ) } -export function ModuleStorylinePreview({ moduleId }) { +export function ModuleStorylinePreview({ moduleId, practiceTables }) { const moduleData = moduleConfig[moduleId] if (!moduleData || !moduleData.storyline) { @@ -134,7 +134,25 @@ export function ModuleStorylinePreview({ moduleId }) {

{moduleData.storyline.text}

- + + {Array.isArray(practiceTables) && practiceTables.length > 0 && ( +
+

+ Tables in lessons +

+
+ {practiceTables.map((name) => ( + + {name} + + ))} +
+
+ )} +
- - - -
-
-
- - {/* Mobile Menu Dropdown */} -
- -
- - ) -} \ No newline at end of file diff --git a/src/components/sql-editor.jsx b/src/components/sql-editor.jsx deleted file mode 100644 index 912f19b..0000000 --- a/src/components/sql-editor.jsx +++ /dev/null @@ -1,1070 +0,0 @@ -"use client" - -import { useState, useEffect, useRef } from "react" -import { Button } from "./ui/button" -import { Card, CardContent } from "./ui/card" -import { Progress } from "./ui/progress" -import CodeMirror from '@uiw/react-codemirror' -import { sql } from '@codemirror/lang-sql' -import { vscodeDark } from '@uiw/codemirror-theme-vscode' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "./ui/table" -import { ScrollArea, ScrollBar } from "./ui/scroll-area" -import { - MessageSquare, - ChevronDown, - ChevronUp, - Maximize2, - Minimize2, - Loader2, - ArrowLeft, - ArrowRight, - Menu, - BookOpen, - PlayCircle, - Monitor, - Database, - FileText, - CheckCircle2, - ChevronRight, - ArrowLeftToLine, - Home, - HelpCircle, - X, - Circle, - CircleDot, - PanelRight, - PanelLeft, - GripVertical, -} from 'lucide-react' -import Link from 'next/link' -import { getModuleLevels } from '../config/moduleConfig' -import { motion, AnimatePresence } from 'framer-motion' -import { moduleConfig } from '../config/moduleConfig' -import { cn } from '../lib/utils' -import Image from 'next/image' - -// Module navigation sidebar -const ModuleSidebar = ({ - isOpen, - onClose, - activeModuleId, - activeLevelId -}) => { - // Track which module is currently expanded (if any) - const [expandedModuleId, setExpandedModuleId] = useState(activeModuleId); - - // Function to toggle a module's expanded state - const toggleModule = (moduleId) => { - if (expandedModuleId === moduleId) { - // If clicking on already expanded module, collapse it - setExpandedModuleId(null); - } else { - // Otherwise, expand this module and collapse any other - setExpandedModuleId(moduleId); - } - }; - - if (!isOpen) return null; - - return ( - -
-

Modules

- -
- -
-
-

Select Module

-
- {Object.entries(moduleConfig).map(([id, module]) => ( - toggleModule(id)} - /> - ))} -
-
-
-
- ); -}; - -// Individual module navigation item with expandable levels -const ModuleNavigationItem = ({ - moduleId, - moduleData, - activeModuleId, - activeLevelId, - isExpanded, - onToggle -}) => { - const maxLevels = moduleData.levels || 0; - const isActive = moduleId === activeModuleId; - - return ( -
- - - {isExpanded && ( -
-
- {[...Array(maxLevels)].map((_, index) => { - const levelNumber = index + 1; - const isActiveLvl = isActive && activeLevelId === levelNumber; - - return ( - - {levelNumber} - - ); - })} -
- -
- - - Overview - - - {parseInt(moduleId) < Object.keys(moduleConfig).length && ( - - Next Module - - - )} -
-
- )} -
- ); -}; - -// Results table for SQL queries -const QueryResultsTable = ({ results, error }) => { - if (error) { - return ( -
-
-

SQL Error

-

{error}

-
-
- ) - } - - if (!results || results.length === 0) { - return ( -
- -

No Results Yet

-

Execute a query to see results here

-
- ) - } - - const columns = Object.keys(results[0]) - - return ( -
- -
- - - - {columns.map((column) => ( - - {column} - - ))} - - - - {results.map((row, rowIndex) => ( - - {columns.map((column) => ( - - {row[column]?.toString() ?? 'NULL'} - - ))} - - ))} - -
-
- -
-
- ) -} - -// Level progress indicator -const LevelProgressIndicator = ({ currentLevel, maxLevels, onLevelClick }) => { - return ( -
- {[...Array(maxLevels)].map((_, index) => { - const levelNumber = index + 1; - const isCurrentLevel = levelNumber === currentLevel; - const isPreviousLevel = levelNumber < currentLevel; - - return ( - - ); - })} -
- ); -}; - -// Success notification when query passes -const SuccessNotification = ({ isVisible }) => { - if (!isVisible) return null; - - return ( - - -
- -
-
-

Success!

-

Ready to proceed to next level

-
-
-
- ); -}; - -// Main SQL Editor component -export function SQLEditor({ moduleId, levelId, lesson, onComplete, hasNextLesson }) { - // Convert moduleId and levelId to numbers if they're strings - const moduleIdNum = typeof moduleId === 'string' ? parseInt(moduleId) : moduleId; - const levelIdNum = typeof levelId === 'string' ? parseInt(levelId) : levelId; - - // Get module data - const maxLevels = getModuleLevels(moduleIdNum.toString()); - const moduleData = moduleConfig[moduleIdNum.toString()]; - - // State variables - const [sqlCode, setSqlCode] = useState(''); - const [queryResults, setQueryResults] = useState([]); - const [sqlError, setSqlError] = useState(null); - const [taskMessage, setTaskMessage] = useState('Loading...'); - const [isMessageExpanded, setIsMessageExpanded] = useState(true); - const [showHint, setShowHint] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - const [isFullScreen, setIsFullScreen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isExecuting, setIsExecuting] = useState(false); - const [levelData, setLevelData] = useState(null); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - - // Fullscreen specific state variables - const [fsInstructionsVisible, setFsInstructionsVisible] = useState(false); - const [fsResultsVisible, setFsResultsVisible] = useState(false); - const [editorWidth, setEditorWidth] = useState('65%'); - const [resultsWidth, setResultsWidth] = useState('35%'); - const [isResizing, setIsResizing] = useState(false); - const [initialX, setInitialX] = useState(0); - - // Refs for editor and resizing - const editorRef = useRef(null); - const fullScreenContainerRef = useRef(null); - const editorContainerRef = useRef(null); - const resizableDividerRef = useRef(null); - - // API URLs - const sqlSpellApiUrl = `${process.env.NEXT_PUBLIC_API_URL}/sqlspell`; - const levelsApiUrl = `${process.env.NEXT_PUBLIC_API_URL}/leveldata`; - - // Toggle fullscreen - const toggleFullScreen = () => { - const isEntering = !isFullScreen; - setIsFullScreen(isEntering); - - // Reset panel states when entering fullscreen - if (isEntering) { - setFsInstructionsVisible(false); - setFsResultsVisible(false); - setEditorWidth('65%'); - setResultsWidth('35%'); - } - }; - - // Handle fullscreen effect - useEffect(() => { - const handleEscKey = (e) => { - if (e.key === "Escape" && isFullScreen) { - setIsFullScreen(false); - } - }; - - if (isFullScreen) { - document.addEventListener('keydown', handleEscKey); - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - } - - return () => { - document.removeEventListener('keydown', handleEscKey); - document.body.style.overflow = ''; - }; - }, [isFullScreen]); - - // Handle resizing in fullscreen mode - improved implementation - useEffect(() => { - if (!isFullScreen || !fsResultsVisible) return; - - const handleMouseMove = (e) => { - if (!isResizing) return; - - const containerWidth = fullScreenContainerRef.current?.clientWidth || 0; - const newX = e.clientX; - - // Calculate percentages based on container width - const editorWidthPercent = (newX / containerWidth) * 100; - const resultsWidthPercent = 100 - editorWidthPercent; - - // Enforce minimum widths (20% for each pane) - if (editorWidthPercent < 20 || resultsWidthPercent < 20) return; - - // Set the new widths - setEditorWidth(`${editorWidthPercent}%`); - setResultsWidth(`${resultsWidthPercent}%`); - }; - - const handleMouseUp = () => { - setIsResizing(false); - document.body.style.cursor = 'default'; - }; - - if (isResizing) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - } - - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - }, [isResizing, initialX, isFullScreen, fsResultsVisible]); - - // Handle resizer mousedown - simplified - const handleResizerMouseDown = (e) => { - if (!isFullScreen || !fsResultsVisible) return; - - setIsResizing(true); - setInitialX(e.clientX); - document.body.style.cursor = 'col-resize'; - e.preventDefault(); // Prevent text selection during resize - }; - - // Fetch level data - useEffect(() => { - const fetchLevelData = async () => { - const moduleLevelID = `${moduleIdNum}${levelIdNum}`; - - try { - const response = await fetch(levelsApiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ moduleLevelID }), - }); - - const data = await response.json(); - - if (response.ok) { - setLevelData(data); - setSqlCode(data.initialCode || ''); - setTaskMessage(data.task); - } else { - setTaskMessage(`Error: ${data.error || 'Failed to fetch level data.'}`); - } - } catch (error) { - console.error('Error fetching level data:', error); - setTaskMessage(`Error fetching level data: ${error.message}`); - } - }; - - fetchLevelData(); - }, [moduleIdNum, levelIdNum, levelsApiUrl]); - - // Execute SQL query - const handleExecute = async () => { - setIsExecuting(true); - setSqlError(null); - - try { - const payload = { - moduleId: moduleIdNum, - levelId: levelIdNum, - sqlCode, - }; - - const response = await fetch(sqlSpellApiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - const responseData = await response.json(); - const result = responseData.body ? JSON.parse(responseData.body) : responseData; - - // In fullscreen mode, always show results panel regardless of success or error - if (isFullScreen) { - setFsResultsVisible(true); - } - - if (result.error) { - setSqlError(result.error); - setQueryResults([]); - } else { - const { output, passed, message } = result; - setQueryResults(output); - - if (passed) { - setTaskMessage(message || 'You passed the level! 🎉'); - handleSuccess(); - } - } - } catch (error) { - console.error('Error executing query:', error); - setSqlError(`Error executing query: ${error.message}`); - setQueryResults([]); - - // Also show results panel for caught errors in fullscreen mode - if (isFullScreen) { - setFsResultsVisible(true); - } - } finally { - setIsExecuting(false); - } - }; - - // Toggle elements - const toggleMessageBox = () => setIsMessageExpanded(prev => !prev); - const toggleHint = () => setShowHint(prev => !prev); - const toggleSidebar = () => { - console.log("Toggling sidebar, current state:", !isSidebarOpen, "fullscreen:", isFullScreen); - setIsSidebarOpen(prev => !prev); - }; - - // Navigate between levels - const handleNavigation = (direction) => { - if (direction === 'back' && levelIdNum > 1) { - window.location.href = `/module/${moduleIdNum}/${levelIdNum - 1}/`; - } else if (direction === 'next' && levelIdNum < maxLevels) { - window.location.href = `/module/${moduleIdNum}/${levelIdNum + 1}/`; - } - }; - - // Navigate to specific level - const handleLevelClick = (level) => { - if (level !== levelIdNum) { - window.location.href = `/module/${moduleIdNum}/${level}/`; - } - }; - - // Success handling - const handleSuccess = () => { - setShowSuccess(true); - setTimeout(() => setShowSuccess(false), 3000); - }; - - return ( -
- {/* Module Navigation Sidebar */} - - {isSidebarOpen && ( - - )} - - - {/* Fullscreen Editor Container */} - {isFullScreen && ( -
- {/* Fullscreen Top Bar */} -
-
-
- -
- SQL Adventure Logo -
- -
- - Module {moduleIdNum} - - - - {levelData?.title || `Level ${levelIdNum}`} - -
-
- - -
-
- - {/* Main Content Area - Flexible Layout */} -
- {/* Instructions Panel - Cleaner Animation */} - {fsInstructionsVisible && ( -
-
-
- -

Instructions

-
- {levelData?.hintMessage && ( - - )} -
- -
-

{taskMessage}

- - {levelData?.hintMessage && showHint && ( -
-
- - Hint -
-

{levelData.hintMessage}

-
- )} -
-
- )} - - {/* SQL Editor Panel - Dark Theme */} -
- {/* Use a consistent dark background matching VSCode theme */} -
- setSqlCode(value)} - height="100%" - className="h-full" - foldGutter={false} - indentWithTab={false} - basicSetup={{ - lineNumbers: true, - highlightActiveLine: true, - highlightSelectionMatches: true, - closeBrackets: true, - autocompletion: true, - history: true, - highlightActiveLineGutter: true, - drawSelection: true, - indentOnInput: true, - bracketMatching: true, - syntaxHighlighting: true, - foldGutter: false, - foldGUI: false, - tabSize: 2, - placeholder: "Enter your SQL query here..." - }} - /> -
-
- - {/* Resizable divider */} - {fsResultsVisible && ( -
- -
- )} - - {/* Right Panel - Results - Cleaner Animation */} - {fsResultsVisible && ( -
-
-
- -

Query Results

-
- {queryResults?.length > 0 && ( - - {queryResults.length} {queryResults.length === 1 ? 'row' : 'rows'} - - )} -
- -
- -
-
- )} -
- - {/* Fullscreen Bottom Bar - combined execution controls and navigation */} -
-
- - - - - -
- -
- - -
- -
- - -
-
-
- )} - - {/* Regular Mode Layout */} - {!isFullScreen && ( -
- {/* Top Header Bar */} -
-
-
- -
- SQL Adventure Logo -
- -
- - Module {moduleIdNum} - - - - {levelData?.title || `Level ${levelIdNum}`} - -
-
- - -
-
- - {/* Main Editor Content */} -
- {/* Left Column - Instructions & Editor */} -
- {/* Task Instructions Card */} - - -
-
- -

Task

-
-
- {levelData?.hintMessage && ( - - )} - -
-
- - {isMessageExpanded && ( -
-

{taskMessage}

- - {levelData?.hintMessage && showHint && ( -
-
- - Hint -
-

{levelData.hintMessage}

-
- )} -
- )} -
-
- - {/* SQL Editor Card */} - - {/* Remove the top bar completely - no header for the editor */} - -
- setSqlCode(value)} - height="100%" - className="h-full" - foldGutter={false} - indentWithTab={false} - basicSetup={{ - lineNumbers: true, - highlightActiveLine: true, - highlightSelectionMatches: true, - closeBrackets: true, - autocompletion: true, - history: true, - highlightActiveLineGutter: true, - drawSelection: true, - indentOnInput: true, - bracketMatching: true, - syntaxHighlighting: true, - foldGutter: false, - foldGUI: false, - tabSize: 2, - placeholder: "Enter your SQL query here..." - }} - /> -
- -
- -
-
-
- - {/* Right Column - Results */} -
- - -
-

- - Query Results -

- {queryResults?.length > 0 && ( - - {queryResults.length} {queryResults.length === 1 ? 'row' : 'rows'} - - )} -
-
- -
-
-
-
-
- - {/* Bottom Navigation Bar */} -
- - - - - -
-
- )} - - {/* Success Notification */} - -
- ); -} \ No newline at end of file diff --git a/src/components/sql-editor/EditorHeader.jsx b/src/components/sql-editor/EditorHeader.jsx index 393b206..ac52d9d 100644 --- a/src/components/sql-editor/EditorHeader.jsx +++ b/src/components/sql-editor/EditorHeader.jsx @@ -1,15 +1,13 @@ import Link from 'next/link' import Image from 'next/image' -import { Button } from '../ui/button' -import { Maximize2, Minimize2, ChevronRight } from 'lucide-react' +import { ChevronRight } from 'lucide-react' export function EditorHeader({ isFullScreen, moduleId, levelId, levelData, - toggleSidebar, - toggleFullScreen + toggleSidebar }) { return (
- - ); diff --git a/src/components/sql-editor/FailureNotification.jsx b/src/components/sql-editor/FailureNotification.jsx new file mode 100644 index 0000000..34befc5 --- /dev/null +++ b/src/components/sql-editor/FailureNotification.jsx @@ -0,0 +1,25 @@ +import { motion, AnimatePresence } from 'framer-motion' +import { AlertCircle } from 'lucide-react' + +export function FailureNotification({ isVisible, message }) { + if (!isVisible) return null; + + return ( + + +
+ +
+
+

Not quite right

+

{message || 'Check out the hint or solution if needed.'}

+
+
+
+ ); +} diff --git a/src/components/sql-editor/FooterNavigation.jsx b/src/components/sql-editor/FooterNavigation.jsx index b9216aa..c22cc09 100644 --- a/src/components/sql-editor/FooterNavigation.jsx +++ b/src/components/sql-editor/FooterNavigation.jsx @@ -17,6 +17,7 @@ export function FooterNavigation({ moduleId, levelId, maxLevels, + canGoNext = true, isExecuting, handleExecute, handleNavigation, @@ -99,7 +100,7 @@ export function FooterNavigation({ variant="outline" size="sm" onClick={() => handleNavigation('next')} - disabled={levelId >= maxLevels} + disabled={!canGoNext} className="h-9 gap-1 text-slate-700 border-slate-300 hover:bg-slate-100" > Next @@ -134,7 +135,7 @@ export function FooterNavigation({ variant="outline" size="sm" onClick={() => handleNavigation('next')} - disabled={levelId >= maxLevels} + disabled={!canGoNext} className="h-9 gap-1 text-slate-700 border-slate-300 hover:bg-slate-100" > Next diff --git a/src/components/sql-editor/InstructionsPanel.jsx b/src/components/sql-editor/InstructionsPanel.jsx index bdf98ba..d37dcb2 100644 --- a/src/components/sql-editor/InstructionsPanel.jsx +++ b/src/components/sql-editor/InstructionsPanel.jsx @@ -1,11 +1,15 @@ import { Card, CardContent } from '../ui/card' -import { BookOpen, MessageSquare, ChevronUp, ChevronDown, HelpCircle } from 'lucide-react' +import { BookOpen, MessageSquare, ChevronUp, ChevronDown, HelpCircle, KeyRound } from 'lucide-react' export function InstructionsPanel({ taskMessage, levelData, showHint, toggleHint, + canShowSolution = false, + showSolution = false, + toggleSolution, + solutionText, isFullScreen = false, isMessageExpanded = true, toggleMessageBox, @@ -23,19 +27,30 @@ export function InstructionsPanel({

Instructions

- {levelData?.hintMessage && ( - - )} +
+ {levelData?.hintMessage && ( + + )} + {canShowSolution && ( + + )} +
-

{taskMessage}

+

{taskMessage}

{levelData?.hintMessage && showHint && (
@@ -43,7 +58,17 @@ export function InstructionsPanel({ Hint
-

{levelData.hintMessage}

+

{levelData.hintMessage}

+
+ )} + + {canShowSolution && showSolution && ( +
+
+ + Solution +
+
{solutionText}
)} @@ -60,14 +85,25 @@ export function InstructionsPanel({

Task

-
+
{levelData?.hintMessage && ( + )} + {canShowSolution && ( + )}
+ )} + + {canShowSolution && showSolution && ( +
+
+ + Solution +
+
{solutionText}
)}
diff --git a/src/components/sql-editor/SQLEditorContainer.jsx b/src/components/sql-editor/SQLEditorContainer.jsx index e680b23..d6c6b97 100644 --- a/src/components/sql-editor/SQLEditorContainer.jsx +++ b/src/components/sql-editor/SQLEditorContainer.jsx @@ -2,9 +2,17 @@ import { useState, useEffect, useRef } from "react" import { AnimatePresence } from 'framer-motion' -import { getModuleLevels } from '../../config/moduleConfig' +import { + CURRICULUM_COMPLETION_PATH, + getModuleLevels, + getNextModuleId, + isFinalCurriculumLevel, +} from '../../config/moduleConfig' import { useLocalStorage } from '../../hooks/useLocalStorage' import { GripVertical } from 'lucide-react' +import { fetchLevelDefinition, getPreviewTableName, stripLevelForClient } from '../../lib/curriculum/fetchLevel' +import { loadSchemaSql } from '../../lib/curriculum/schemaCache' +import { executeUserSql, executePreviewQuery } from '../../lib/curriculum/executeUserSql' // Import components import { Sidebar } from './Sidebar' @@ -14,14 +22,21 @@ import { SQLEditorPanel } from './SQLEditorPanel' import { ResultsPanel } from './ResultsPanel' import { FooterNavigation } from './FooterNavigation' import { SuccessNotification } from './SuccessNotification' +import { FailureNotification } from './FailureNotification' -export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasNextLesson }) { +export function SQLEditorContainer({ moduleId, levelId }) { // Convert moduleId and levelId to numbers if they're strings const moduleIdNum = typeof moduleId === 'string' ? parseInt(moduleId) : moduleId; const levelIdNum = typeof levelId === 'string' ? parseInt(levelId) : levelId; // Get module data const maxLevels = getModuleLevels(moduleIdNum.toString()); + const nextModuleId = getNextModuleId(moduleIdNum.toString()); + const onFinalSqlLevel = isFinalCurriculumLevel(moduleIdNum, levelIdNum, maxLevels); + const canGoNext = + levelIdNum < maxLevels || nextModuleId != null || onFinalSqlLevel; + + const levelDefinitionRef = useRef(null); // State variables const [sqlCode, setSqlCode] = useState(''); @@ -30,9 +45,13 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN const [taskMessage, setTaskMessage] = useState('Loading...'); const [isMessageExpanded, setIsMessageExpanded] = useState(true); const [showHint, setShowHint] = useState(false); + const [showSolution, setShowSolution] = useState(false); + const [canShowSolution, setCanShowSolution] = useState(false); const [showSuccess, setShowSuccess] = useState(false); + const [successMessage, setSuccessMessage] = useState(''); + const [showFailure, setShowFailure] = useState(false); + const [failureMessage, setFailureMessage] = useState(''); const [isFullScreen, setIsFullScreen] = useState(false); - const [isLoading, setIsLoading] = useState(false); const [isExecuting, setIsExecuting] = useState(false); const [levelData, setLevelData] = useState(null); const [isSidebarOpen, setIsSidebarOpen] = useState(false); @@ -53,10 +72,6 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN // State for managing fullscreen persistence const [fullscreenState, setFullscreenState] = useLocalStorage('sqlEditorFullscreen', null); - // API URLs - const sqlSpellApiUrl = `${process.env.NEXT_PUBLIC_API_URL}/sqlspell`; - const levelsApiUrl = `${process.env.NEXT_PUBLIC_API_URL}/leveldata`; - // Toggle fullscreen const toggleFullScreen = () => { const isEntering = !isFullScreen; @@ -140,37 +155,59 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN e.preventDefault(); // Prevent text selection during resize }; - // Fetch level data + // Load level content from public/curriculum/modules (see fetchLevel.js) useEffect(() => { - const fetchLevelData = async () => { - const moduleLevelID = `${moduleIdNum}${levelIdNum}`; - + let cancelled = false; + + const load = async () => { + levelDefinitionRef.current = null; + setTaskMessage('Loading...'); + setSqlError(null); + setQueryResults([]); + setSuccessMessage(''); + setShowFailure(false); + setFailureMessage(''); + setShowSolution(false); + setCanShowSolution(false); try { - const response = await fetch(levelsApiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ moduleLevelID }), + const full = await fetchLevelDefinition(moduleIdNum, levelIdNum); + if (cancelled) return; + levelDefinitionRef.current = full; + setLevelData(stripLevelForClient(full)); + setSqlCode(full.initialCode || ''); + setTaskMessage(full.task); + + const schemaSql = await loadSchemaSql(moduleIdNum, full.schema); + if (cancelled) return; + + const tableName = getPreviewTableName(full); + if (!tableName) return; + + const preview = await executePreviewQuery({ + schemaSql, + query: `SELECT * FROM ${tableName};`, }); - - const data = await response.json(); - - if (response.ok) { - setLevelData(data); - setSqlCode(data.initialCode || ''); - setTaskMessage(data.task); + if (cancelled) return; + + if (preview.error) { + setSqlError(preview.error); + setQueryResults([]); } else { - setTaskMessage(`Error: ${data.error || 'Failed to fetch level data.'}`); + setQueryResults(preview.output); } } catch (error) { - console.error('Error fetching level data:', error); - setTaskMessage(`Error fetching level data: ${error.message}`); + if (cancelled) return; + console.error('Error loading level:', error); + setLevelData(null); + setTaskMessage(`Error loading level: ${error.message}`); } }; - - fetchLevelData(); - }, [moduleIdNum, levelIdNum, levelsApiUrl]); + + load(); + return () => { + cancelled = true; + }; + }, [moduleIdNum, levelIdNum]); // Effect to persist fullscreen state useEffect(() => { @@ -180,52 +217,62 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN } }, [fullscreenState, moduleIdNum, levelIdNum]); - // Execute SQL query + // Execute SQL locally (sql.js) using schema + solution from curriculum files const handleExecute = async () => { setIsExecuting(true); setSqlError(null); - + setShowFailure(false); + + const level = levelDefinitionRef.current; + if (!level) { + setSqlError('Level is still loading.'); + setIsExecuting(false); + return; + } + + if (!sqlCode.trim()) { + setSqlError('SQL query cannot be empty'); + setIsExecuting(false); + return; + } + try { - const payload = { - moduleId: moduleIdNum, - levelId: levelIdNum, - sqlCode, - }; - - const response = await fetch(sqlSpellApiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), + const schemaSql = await loadSchemaSql(moduleIdNum, level.schema); + const result = await executeUserSql({ + schemaSql, + userQuery: sqlCode, + level, }); - - const responseData = await response.json(); - const result = responseData.body ? JSON.parse(responseData.body) : responseData; - - // In fullscreen mode, always show results panel regardless of success or error + if (isFullScreen) { setFsResultsVisible(true); } - + if (result.error) { + setCanShowSolution(true); setSqlError(result.error); setQueryResults([]); + handleFailure(); } else { const { output, passed, message } = result; setQueryResults(output); - + if (passed) { - setTaskMessage(message || 'You passed the level! 🎉'); + const nextSuccessMessage = message || level.successMessage || 'You passed the level! 🎉'; + setSuccessMessage(nextSuccessMessage); + setShowFailure(false); handleSuccess(); + } else if (message) { + setCanShowSolution(true); + handleFailure(); } } } catch (error) { console.error('Error executing query:', error); setSqlError(`Error executing query: ${error.message}`); setQueryResults([]); - - // Also show results panel for caught errors in fullscreen mode + handleFailure(); + if (isFullScreen) { setFsResultsVisible(true); } @@ -241,20 +288,39 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN setIsSidebarOpen(prev => !prev); }; - // Navigate between levels + // Navigate between levels (and from last level to the next module landing page) const handleNavigation = (direction) => { - // Save fullscreen state before navigation - if (isFullScreen) { - setFullscreenState({ - moduleId: moduleIdNum, - levelId: direction === 'next' ? levelIdNum + 1 : levelIdNum - 1 - }); + if (direction === 'next') { + if (levelIdNum < maxLevels) { + if (isFullScreen) { + setFullscreenState({ + moduleId: moduleIdNum, + levelId: levelIdNum + 1 + }); + } + window.location.href = `/module/${moduleIdNum}/${levelIdNum + 1}/`; + } else if (nextModuleId) { + if (isFullScreen) { + setFullscreenState(null); + } + window.location.href = `/module/${nextModuleId}/`; + } else if (onFinalSqlLevel) { + if (isFullScreen) { + setFullscreenState(null); + } + window.location.href = CURRICULUM_COMPLETION_PATH; + } + return; } - + if (direction === 'back' && levelIdNum > 1) { + if (isFullScreen) { + setFullscreenState({ + moduleId: moduleIdNum, + levelId: levelIdNum - 1 + }); + } window.location.href = `/module/${moduleIdNum}/${levelIdNum - 1}/`; - } else if (direction === 'next' && levelIdNum < maxLevels) { - window.location.href = `/module/${moduleIdNum}/${levelIdNum + 1}/`; } }; @@ -275,7 +341,13 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN // Success handling const handleSuccess = () => { setShowSuccess(true); - setTimeout(() => setShowSuccess(false), 3000); + setTimeout(() => setShowSuccess(false), 5000); + }; + + const handleFailure = () => { + setFailureMessage('Not quite right. Check out the hint or solution if needed.'); + setShowFailure(true); + setTimeout(() => setShowFailure(false), 3000); }; // Panel visibility toggles @@ -319,7 +391,6 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN levelId={levelIdNum} levelData={levelData} toggleSidebar={toggleSidebar} - toggleFullScreen={toggleFullScreen} />
@@ -330,6 +401,10 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN levelData={levelData} showHint={showHint} toggleHint={toggleHint} + canShowSolution={canShowSolution} + showSolution={showSolution} + toggleSolution={() => setShowSolution(prev => !prev)} + solutionText={levelDefinitionRef.current?.solution || ''} isFullScreen={true} width="300px" /> @@ -342,6 +417,7 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN isFullScreen={true} width={fsResultsVisible ? editorWidth : '100%'} editorRef={editorContainerRef} + toggleFullScreen={toggleFullScreen} /> {/* Resizable divider */} @@ -371,6 +447,7 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN moduleId={moduleIdNum} levelId={levelIdNum} maxLevels={maxLevels} + canGoNext={canGoNext} isExecuting={isExecuting} handleExecute={handleExecute} handleNavigation={handleNavigation} @@ -389,7 +466,6 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN levelId={levelIdNum} levelData={levelData} toggleSidebar={toggleSidebar} - toggleFullScreen={toggleFullScreen} />
@@ -400,6 +476,10 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN levelData={levelData} showHint={showHint} toggleHint={toggleHint} + canShowSolution={canShowSolution} + showSolution={showSolution} + toggleSolution={() => setShowSolution(prev => !prev)} + solutionText={levelDefinitionRef.current?.solution || ''} isMessageExpanded={isMessageExpanded} toggleMessageBox={toggleMessageBox} isFullScreen={false} @@ -411,6 +491,7 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN isFullScreen={false} isExecuting={isExecuting} handleExecute={handleExecute} + toggleFullScreen={toggleFullScreen} />
@@ -427,6 +508,7 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN moduleId={moduleIdNum} levelId={levelIdNum} maxLevels={maxLevels} + canGoNext={canGoNext} handleNavigation={handleNavigation} handleLevelClick={handleLevelClick} /> @@ -434,7 +516,8 @@ export function SQLEditorContainer({ moduleId, levelId, lesson, onComplete, hasN )} {/* Success Notification */} - + +
); } \ No newline at end of file diff --git a/src/components/sql-editor/SQLEditorPanel.jsx b/src/components/sql-editor/SQLEditorPanel.jsx index 958b1ec..ba6f379 100644 --- a/src/components/sql-editor/SQLEditorPanel.jsx +++ b/src/components/sql-editor/SQLEditorPanel.jsx @@ -3,7 +3,7 @@ import { Button } from '../ui/button' import CodeMirror from '@uiw/react-codemirror' import { sql } from '@codemirror/lang-sql' import { vscodeDark } from '@uiw/codemirror-theme-vscode' -import { PlayCircle, Loader2 } from 'lucide-react' +import { PlayCircle, Loader2, Maximize2, Minimize2 } from 'lucide-react' import { cn } from '../../lib/utils' export function SQLEditorPanel({ @@ -13,7 +13,8 @@ export function SQLEditorPanel({ width, editorRef, isExecuting, - handleExecute + handleExecute, + toggleFullScreen }) { // Common CodeMirror configuration const codeMirrorConfig = { @@ -53,8 +54,20 @@ export function SQLEditorPanel({ width: width, transition: 'width 0.1s ease-out' }} - className="h-full overflow-hidden flex flex-col min-w-0" + className="h-full overflow-hidden flex flex-col min-w-0 relative" > +
+ +
+
+ +
+ +
+
+ + Home + {[...Array(maxLevels)].map((_, index) => { const levelNumber = index + 1; const isActiveLvl = isActive && activeLevelId === levelNumber; @@ -69,16 +78,8 @@ const ModuleNavigationItem = ({ })}
-
- - - Overview - - - {parseInt(moduleId) < Object.keys(moduleConfig).length && ( + {parseInt(moduleId) < Object.keys(moduleConfig).length && ( +
Next Module - )} -
+
+ )}
)}
diff --git a/src/components/sql-editor/SuccessNotification.jsx b/src/components/sql-editor/SuccessNotification.jsx index 11a34e3..538e8ba 100644 --- a/src/components/sql-editor/SuccessNotification.jsx +++ b/src/components/sql-editor/SuccessNotification.jsx @@ -1,7 +1,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { CheckCircle2 } from 'lucide-react' -export function SuccessNotification({ isVisible }) { +export function SuccessNotification({ isVisible, message }) { if (!isVisible) return null; return ( @@ -17,7 +17,7 @@ export function SuccessNotification({ isVisible }) {

Success!

-

Ready to proceed to next level

+

{message || 'Ready to proceed to next level'}

diff --git a/src/components/sql-editor/index.js b/src/components/sql-editor/index.js index 583f564..84ea94e 100644 --- a/src/components/sql-editor/index.js +++ b/src/components/sql-editor/index.js @@ -1,12 +1 @@ -// Main container -export { SQLEditorContainer as SQLEditor } from './SQLEditorContainer'; - -// Individual components -export { Sidebar } from './Sidebar'; -export { EditorHeader } from './EditorHeader'; -export { InstructionsPanel } from './InstructionsPanel'; -export { SQLEditorPanel } from './SQLEditorPanel'; -export { ResultsPanel } from './ResultsPanel'; -export { FooterNavigation } from './FooterNavigation'; -export { LevelProgressIndicator } from './LevelProgressIndicator'; -export { SuccessNotification } from './SuccessNotification'; \ No newline at end of file +export { SQLEditorContainer as SQLEditor } from './SQLEditorContainer'; \ No newline at end of file diff --git a/src/components/ui/badge.jsx b/src/components/ui/badge.jsx deleted file mode 100644 index 7a85e05..0000000 --- a/src/components/ui/badge.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from "react" -import { cva } from "class-variance-authority" -import { cn } from "../../lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-[#2A6B70] focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-[#2A6B70] text-white", - secondary: - "border-transparent bg-[#EAAF56] text-white", - outline: "text-[#2A6B70] border-[#68A4A1]", - success: "border-transparent bg-[#68A4A1] text-white", - info: "border-transparent bg-[#E6F2F2] text-[#2A6B70]", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant, - ...props -}) { - return ( -
- ) -} - -export { Badge, badgeVariants } \ No newline at end of file diff --git a/src/components/ui/dialog.jsx b/src/components/ui/dialog.jsx deleted file mode 100644 index 1d0257e..0000000 --- a/src/components/ui/dialog.jsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { Cross2Icon } from "@radix-ui/react-icons" - -import { cn } from "../../lib/utils" - -const Dialog = DialogPrimitive.Root - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close - -const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName - -const DialogHeader = ({ - className, - ...props -}) => ( -
-) -DialogHeader.displayName = "DialogHeader" - -const DialogFooter = ({ - className, - ...props -}) => ( -
-) -DialogFooter.displayName = "DialogFooter" - -const DialogTitle = React.forwardRef(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName - -const DialogDescription = React.forwardRef(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, - DialogClose, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} diff --git a/src/components/ui/dropdown-menu.jsx b/src/components/ui/dropdown-menu.jsx deleted file mode 100644 index 72ef040..0000000 --- a/src/components/ui/dropdown-menu.jsx +++ /dev/null @@ -1,154 +0,0 @@ -"use client" - -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" - -import { cn } from "../../lib/utils" - -const DropdownMenu = DropdownMenuPrimitive.Root - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger - -const DropdownMenuGroup = DropdownMenuPrimitive.Group - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal - -const DropdownMenuSub = DropdownMenuPrimitive.Sub - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup - -const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)) -DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName - -const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => ( - -)) -DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName - -const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName - -const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => ( - -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName - -const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName - -const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName - -const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => ( - -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName - -const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => ( - -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName - -const DropdownMenuShortcut = ({ - className, - ...props -}) => { - return ( - - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -} \ No newline at end of file diff --git a/src/components/ui/progress.jsx b/src/components/ui/progress.jsx deleted file mode 100644 index 25703ec..0000000 --- a/src/components/ui/progress.jsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client" - -import * as React from "react" -import * as ProgressPrimitive from "@radix-ui/react-progress" - -import { cn } from "../../lib/utils" - -const Progress = React.forwardRef(({ className, value, indicatorClassName, ...props }, ref) => ( - - - -)) -Progress.displayName = ProgressPrimitive.Root.displayName - -export { Progress } diff --git a/src/components/ui/separator.jsx b/src/components/ui/separator.jsx deleted file mode 100644 index 28944ca..0000000 --- a/src/components/ui/separator.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" -import { cn } from "@/lib/utils" - -const Separator = React.forwardRef(( - { className, orientation = "horizontal", decorative = true, ...props }, - ref -) => ( - -)) -Separator.displayName = SeparatorPrimitive.Root.displayName - -export { Separator } \ No newline at end of file diff --git a/src/config/moduleConfig.js b/src/config/moduleConfig.js index 49b4cae..6ac9043 100644 --- a/src/config/moduleConfig.js +++ b/src/config/moduleConfig.js @@ -1,17 +1,21 @@ +/** + * Table names referenced in each module’s lesson JSON (`levels[].table`), + * for UI copy on the home page. Keep in sync with `public/curriculum/modules/*.json`. + */ export const moduleConfig = { '1': { title: "SELECT, DISTINCT, FROM, ORDER BY", - description: "Learn the basics of SQL queries and database manipulation", levels: 5, + practiceTables: ['top_songs'], storyline: { - text: "It's your first week at Stellar Sound Records, and you're eager to prove yourself. Your manager hands you a login to the music database and says, \"Let's see what you can do!\" Time to dig into the data and show them you've got star potential—even behind the scenes.", + text: "It's your first week at Stellar Sound Records, and you're eager to prove yourself. Your manager hands you a login to the music database and says, 'Let's see what you can do!' Time to dig into the data and show them you've got star potential—even behind the scenes.", image: "/images/storyline/module1.png" } }, '2': { title: "WHERE Statements", - description: "Master filtering data with WHERE clauses", levels: 5, + practiceTables: ['top_songs'], storyline: { text: "Now that you've mastered the basics, you're trusted with more specific tasks. The team needs help finding songs for marketing campaigns, playlists, and artist highlights. You're learning that asking the right questions—just like writing a hit song—is everything.", image: "/images/storyline/module2.png" @@ -19,8 +23,8 @@ export const moduleConfig = { }, '3': { title: "Advanced WHERE Statements", - description: "Explore complex filtering techniques", levels: 5, + practiceTables: ['top_songs'], storyline: { text: "The work is getting more detailed. You're analyzing patterns, filtering across different dates and keywords, and helping teams curate niche content. Every new skill gets you closer to understanding how data drives decisions in the music industry.", image: "/images/storyline/module3.png" @@ -28,43 +32,88 @@ export const moduleConfig = { }, '4': { title: "Aggregates and Group By", - description: "Learn to create summary statistics and group data", levels: 5, + practiceTables: ['top_songs'], storyline: { text: "Since you have been doing such great work, you've been promoted from intern to Junior Analyst. You're not just pulling data—you're uncovering insights. Whether it's finding the most popular genres or tracking artist releases, your work is influencing what gets produced, promoted, and played. You're becoming a true data star behind the stars.", image: "/images/storyline/module4.png" } }, '5': { - title: "Database Design", - description: "Master database schema design and relationships", + title: "Changing Data", levels: 5, + practiceTables: ['new_artists'], storyline: { - text: "As your role at Stellar Sound Records grows, the label entrusts you with designing a new database for an upcoming music festival. You'll need to model relationships between artists, venues, and performances, ensuring all the festival's data is perfectly organized.", + text: "The label is launching a new artist scouting program, and you're the first to get the chance to build the database from scratch. You'll create tables, insert rows, update records, and delete data—all the skills you've learned so far, in practice.", image: "/images/storyline/module5.png" } }, '6': { - title: "Advanced Joins and Subqueries", - description: "Master complex database operations with advanced joins and subqueries", + title: "Practice: Putting It All Together", levels: 5, + practiceTables: ['top_songs'], storyline: { - text: "Congratulations! You've been promoted to Senior Data Analyst. The CEO has a special project just for you: analyzing cross-platform performance and artist collaboration patterns. This complex analysis will require your most advanced SQL skills yet, connecting data across multiple systems.", + text: "You've picked up a real toolkit—filtering, sorting, grouping, and crunching numbers—and now your manager wants to see it all in action. The asks aren't simple anymore: they mix a filter here, a count there, a summary at the bottom. This is where the basics start to click as one connected skill.", image: "/images/storyline/module6.png" } + }, + '7': { + title: "Joins: Chart Data Meets Label Metadata", + levels: 5, + practiceTables: ['album_info', 'top_songs'], + storyline: { + text: "The team needs help finding songs for marketing campaigns, playlists, and artist highlights. You're learning that asking the right questions—just like writing a hit song—is everything.", + image: "/images/storyline/Module7.png" + } + }, + '8': { + title: "Data Transformations", + levels: 5, + practiceTables: ['album_info', 'top_songs'], + storyline: { + text: "Raw exports rarely match what stakeholders want to read. You're learning to reshape values in the query itself—swapping bad text for good text, turning numbers into friendly categories, and filling gaps—so every spreadsheet looks intentional before it leaves your desk.", + image: "/images/storyline/Module8.png" + } + }, + '9': { + title: "Filtering and Reporting with Joins", + levels: 5, + practiceTables: ['album_info', 'top_songs'], + storyline: { + text: "The catalog keeps growing, and teams need faster answers from their data. You’re learning how to combine tables, filter the right rows, and build reports that help Stellar Sound make decisions quickly.", + image: "/images/storyline/Module9.png" + } + }, + '10': { + title: "Reporting with CASE and HAVING", + levels: 5, + practiceTables: ['top_songs'], + storyline: { + text: "You're not just answering questions anymore—you're shaping the stories Stellar Sound tells. Your reports power newsletters, marketing posters, and the slides leadership shows in boardrooms. A few clever rows of SQL turn into the headlines everyone reads.", + image: "/images/storyline/Module10.png" + } } } -// Add the overall storyline context export const curriculumStoryline = { title: "TOP SONGS SQL CURRICULUM", - introduction: "You dream of becoming a famous singer, and to get your foot in the door, you've landed an internship at one of the world's top music labels: Stellar Sound Records. As a junior data analyst, you'll use SQL to explore hit songs, uncover trends, and help the label make decisions—all while secretly hoping your name ends up on this list one day." } export const getModuleLevels = (moduleId) => { return moduleConfig[moduleId]?.levels || 5 // default to 5 levels if not specified } -export const getAllModules = () => { - return Object.keys(moduleConfig).map(id => ({ moduleId: id })) -} \ No newline at end of file +/** Next module id in curriculum order, or null if `moduleId` is the last module. */ +export const getNextModuleId = (moduleId) => { + const ids = Object.keys(moduleConfig).sort((a, b) => Number(a) - Number(b)) + const idx = ids.indexOf(String(moduleId)) + if (idx === -1 || idx === ids.length - 1) return null + return ids[idx + 1] +} + +/** Static route shown after the final level of the final SQL module (module 10). */ +export const CURRICULUM_COMPLETION_PATH = '/congratulations/' + +export function isFinalCurriculumLevel(moduleId, levelId, maxLevels) { + return String(moduleId) === '10' && Number(levelId) === Number(maxLevels) +} diff --git a/src/lib/curriculum/executeUserSql.js b/src/lib/curriculum/executeUserSql.js new file mode 100644 index 0000000..5706a39 --- /dev/null +++ b/src/lib/curriculum/executeUserSql.js @@ -0,0 +1,144 @@ +import { sqlWasmUrl } from './paths' + +let sqlFactoryPromise + +async function getSqlFactory() { + if (!sqlFactoryPromise) { + const initSqlJs = (await import('sql.js')).default + sqlFactoryPromise = initSqlJs({ + locateFile: () => sqlWasmUrl(), + }) + } + return sqlFactoryPromise +} + +function splitStatements(userQuery) { + return userQuery + .split(';') + .map((q) => q.trim()) + .filter(Boolean) +} + +function rowsToObjects(columns, values) { + if (!values?.length) return [] + return values.map((row) => { + const obj = {} + columns.forEach((col, i) => { + obj[col] = row[i] ?? null + }) + return obj + }) +} + +function runSelect(db, query) { + const exec = db.exec(query) + if (exec.length === 0) return [] + const { columns, values = [] } = exec[0] + return rowsToObjects(columns, values) +} + +function runFinalStatement(db, query) { + const statements = splitStatements(query) + if (!statements.length) return { rows: [], columns: [] } + const finalQuery = statements[statements.length - 1] + for (let i = 0; i < statements.length - 1; i += 1) { + db.run(statements[i]) + } + const exec = db.exec(finalQuery) + if (exec.length === 0) return { rows: [], columns: [] } + return { + rows: exec[0].values || [], + columns: exec[0].columns || [], + } +} + +export async function executePreviewQuery({ schemaSql, query }) { + const SQL = await getSqlFactory() + const db = new SQL.Database() + try { + db.exec(schemaSql) + return { + output: runSelect(db, query), + error: null, + } + } catch (e) { + return { + output: [], + error: `SQL Error: ${e?.message || e}`, + } + } finally { + db.close() + } +} + +/** + * Mirrors backend/SQL-Code-Playground/lambda_function.py execute_query semantics. + */ +export async function executeUserSql({ schemaSql, userQuery, level }) { + const solutionQuery = String(level.solution || '').trim() + const hintMessage = level.hintMessage + const successMessage = level.successMessage + + if (!solutionQuery) { + return { + output: [], + passed: false, + error: 'Invalid level configuration', + } + } + + const SQL = await getSqlFactory() + const queries = splitStatements(userQuery) + if (!queries.length) { + return { + output: [], + passed: false, + error: 'No valid SQL statements found', + } + } + + try { + const userDb = new SQL.Database() + const solutionDb = new SQL.Database() + let userRows = [] + let userColumns = [] + let solutionRows = [] + try { + userDb.exec(schemaSql) + solutionDb.exec(schemaSql) + const userResult = runFinalStatement(userDb, userQuery) + const solutionResult = runFinalStatement(solutionDb, solutionQuery) + userRows = userResult.rows + userColumns = userResult.columns + solutionRows = solutionResult.rows + } finally { + userDb.close() + solutionDb.close() + } + + const passed = JSON.stringify(userRows) === JSON.stringify(solutionRows) + const output = rowsToObjects(userColumns, userRows) + + const response = { + output, + passed, + message: passed + ? successMessage || '🎉 Spell perfectly cast!' + : hintMessage || 'Not quite right. Try again!', + } + + if (!passed) { + response.hint = hintMessage + } + + return response + } catch (e) { + return { + output: [], + passed: false, + error: `SQL Error: ${e?.message || e}`, + hint: hintMessage, + showSolution: true, + } + } +} diff --git a/src/lib/curriculum/fetchLevel.js b/src/lib/curriculum/fetchLevel.js new file mode 100644 index 0000000..25a7016 --- /dev/null +++ b/src/lib/curriculum/fetchLevel.js @@ -0,0 +1,70 @@ +import { curriculumModuleUrl } from './paths' + +const REQUIRED = [ + 'title', + 'task', + 'initialCode', + 'solution', + 'hintMessage', + 'successMessage', +] + +/** Normalize `table` from JSON: string or string[] → trimmed non-empty names. */ +export function normalizeLessonTables(table) { + if (table == null) return null + if (typeof table === 'string') { + const t = table.trim() + return t ? [t] : null + } + if (Array.isArray(table)) { + const out = table.map((x) => String(x).trim()).filter(Boolean) + return out.length ? out : null + } + return null +} + +/** First table in the lesson’s `table` list — used for initial result preview. */ +export function getPreviewTableName(level) { + const list = normalizeLessonTables(level?.table) + return list?.[0] ?? null +} + +export function stripLevelForClient(level) { + if (!level) return null + const { solution: _s, ...rest } = level + return rest +} + +export async function fetchLevelDefinition(moduleId, levelId) { + const url = curriculumModuleUrl(moduleId) + const res = await fetch(url) + if (!res.ok) { + const err = new Error( + res.status === 404 + ? `No module file at curriculum/modules/${moduleId}.json` + : `Failed to load module (${res.status})` + ) + err.status = res.status + throw err + } + const moduleData = await res.json() + const levels = Array.isArray(moduleData.levels) ? moduleData.levels : [] + const data = levels.find((level) => Number(level.id) === Number(levelId)) + + if (!data) { + throw new Error(`No level ${levelId} found in module ${moduleId}`) + } + + for (const key of REQUIRED) { + if (data[key] === undefined || data[key] === null) { + throw new Error(`Level JSON missing required field "${key}"`) + } + } + + // `table` is optional: lessons that start with an empty schema + // (e.g. CREATE TABLE) have no existing table to preview. + const tables = normalizeLessonTables(data.table) + data.table = tables || [] + + return data +} diff --git a/src/lib/curriculum/paths.js b/src/lib/curriculum/paths.js new file mode 100644 index 0000000..94d3623 --- /dev/null +++ b/src/lib/curriculum/paths.js @@ -0,0 +1,59 @@ +/** + * Prefix for static assets when the app is served under a subpath (e.g. GitHub Pages). + * Set NEXT_PUBLIC_BASE_PATH to match next.config `basePath` if you use one. + */ +export function withBasePath(path) { + const base = process.env.NEXT_PUBLIC_BASE_PATH || '' + const normalized = path.startsWith('/') ? path : `/${path}` + if (!base) return normalized + return `${base.replace(/\/$/, '')}${normalized}` +} + +export function curriculumModuleUrl(moduleId) { + return withBasePath(`/curriculum/modules/${moduleId}.json`) +} + +/** + * Resolve the SQL schema files to load before running a level. + * + * A level JSON may set `schema` to a schema basename (or array of basenames) + * to override the module's default — useful when one lesson in a module needs + * a different starting state (e.g. an empty DB for a CREATE TABLE lesson + * while the rest of the module shares a pre-populated table). + */ +export function curriculumSchemaUrls(moduleId, levelSchemaOverride = null) { + if (levelSchemaOverride) { + const list = Array.isArray(levelSchemaOverride) + ? levelSchemaOverride + : [levelSchemaOverride] + const cleaned = list.map((name) => String(name).trim()).filter(Boolean) + if (cleaned.length) { + return cleaned.map((name) => + withBasePath(`/curriculum/schemas/${name}`) + ) + } + } + + const m = Number(moduleId) + if (m >= 1 && m <= 4) { + return [withBasePath('/curriculum/schemas/top_songs.sql')] + } + if (m === 5) { + // Module 5 lessons each declare their own per-lesson `schema` field + // (module5_lesson1.sql .. module5_lesson5.sql) so the per-lesson + // override path is the one that actually fires. This empty-DB fallback + // only matters if a new module-5 lesson ever forgets to set `schema`. + return [withBasePath('/curriculum/schemas/module5_lesson1.sql')] + } + if (m >= 6 && m <= 10) { + return [ + withBasePath('/curriculum/schemas/top_songs.sql'), + withBasePath('/curriculum/schemas/album_info.sql'), + ] + } + return [withBasePath('/curriculum/schemas/top_songs.sql')] +} + +export function sqlWasmUrl() { + return withBasePath('/sql-wasm.wasm') +} diff --git a/src/lib/curriculum/schemaCache.js b/src/lib/curriculum/schemaCache.js new file mode 100644 index 0000000..3f8d8ac --- /dev/null +++ b/src/lib/curriculum/schemaCache.js @@ -0,0 +1,22 @@ +import { curriculumSchemaUrls } from './paths' + +const cache = new Map() + +export async function loadSchemaSql(moduleId, levelSchemaOverride = null) { + const urls = curriculumSchemaUrls(moduleId, levelSchemaOverride) + const key = urls.join('|') + if (cache.has(key)) return cache.get(key) + + const parts = [] + for (const url of urls) { + const res = await fetch(url) + if (!res.ok) { + throw new Error(`Failed to load schema (${res.status}): ${url}`) + } + parts.push(await res.text()) + } + + const text = parts.join('\n\n') + cache.set(key, text) + return text +} diff --git a/src/styles/theme.js b/src/styles/theme.js deleted file mode 100644 index 3f88711..0000000 --- a/src/styles/theme.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Application theme based on a refined, modern color palette - * This ensures consistent colors and styling throughout the application - */ - -export const theme = { - colors: { - // Main brand color - Deep Teal - teal: { - DEFAULT: "#2A6B70", // Slightly darker and more sophisticated - hover: "#235458", // Even darker for hover states - foreground: "#FFFFFF", - medium: "#68A4A1", // More muted teal - light: "#E6F2F2", - veryLight: "#F5FAFA", - dark: "#1D4A4D", - }, - - // Accent color - Complementary Blue (replacing amber/gold) - accent: { - DEFAULT: "#5B8A9D", // Complementary blue - hover: "#4A7688", // Darker for hover states - foreground: "#FFFFFF", - light: "#E9F1F5", - bright: "#6FA3B8", - }, - - // Secondary accent - Extra blue tones - blue: { - DEFAULT: "#5B8A9D", - hover: "#4A7688", - light: "#E9F1F5", - dark: "#3A5E69" - }, - - // Neutrals - gray: { - DEFAULT: "#6E7780", - dark: "#2E3A45", - medium: "#4E5964", - light: "#F0F2F4", - lighter: "#F8F9FA", - darkest: "#1A2229", - }, - - // UI States - success: "#3D9D7C", // Softer green - warning: "#E6B05E", // Amber - error: "#D56262", // Softer red - info: "#5B8A9D", // Using our blue - - // Common color combinations for UI elements - ui: { - background: "#FFFFFF", - foreground: "#2E3A45", - headerBg: "#2A6B70", - headerText: "#FFFFFF", - cardBg: "#FFFFFF", - cardBorder: "#E5E7EB", - inputBorder: "#D1D5DB", - inputFocus: "rgba(42, 107, 112, 0.2)", - disabledBg: "#F0F2F4", - disabledText: "#9CA3AF", - footerBg: "#2E3A45", - footerText: "#FFFFFF" - }, - }, - - // Typography - typography: { - fontFamily: { - sans: 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', - heading: 'Raleway, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', - mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' - }, - sizes: { - xs: '0.75rem', // 12px - sm: '0.875rem', // 14px - base: '1rem', // 16px - lg: '1.125rem', // 18px - xl: '1.25rem', // 20px - '2xl': '1.5rem', // 24px - '3xl': '1.875rem',// 30px - '4xl': '2.25rem', // 36px - '5xl': '3rem', // 48px - } - }, - - // Spacing (matches Tailwind config) - spacing: { - 0: '0', - 1: '0.25rem', // 4px - 2: '0.5rem', // 8px - 3: '0.75rem', // 12px - 4: '1rem', // 16px - 5: '1.25rem', // 20px - 6: '1.5rem', // 24px - 8: '2rem', // 32px - 10: '2.5rem', // 40px - 12: '3rem', // 48px - 16: '4rem', // 64px - 20: '5rem', // 80px - 24: '6rem', // 96px - }, - - // Layout variables - layout: { - headerHeightMobile: '3.5rem', // 56px - headerHeight: '4rem', // 64px - headerOffsetMobile: '4.5rem', // Mobile header + safety margin - headerOffset: '5rem', // Header + safety margin - sectionPaddingYSm: '3rem', // 48px - sectionPaddingY: '4rem', // 64px - sectionPaddingYLg: '5rem', // 80px - }, - - // Border radius - borderRadius: { - none: '0', - sm: '0.125rem', // 2px - DEFAULT: '0.5rem',// 8px - default radius - md: '0.375rem', // 6px - lg: '0.5rem', // 8px - xl: '0.75rem', // 12px - '2xl': '1rem', // 16px - '3xl': '1.5rem', // 24px - full: '9999px', // Circle - }, - - // Shadows - shadows: { - sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', - DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', - md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', - lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', - xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', - soft: '0 2px 10px rgba(0, 0, 0, 0.06)', - hover: '0 10px 20px rgba(0, 0, 0, 0.08)', - glow: '0 0 20px rgba(42, 107, 112, 0.15)', // Using our deep teal color - } -} \ No newline at end of file