commit a198219bc09be03ad2c4bd1beedbd7cff72f214c Author: fightme-cmd <80396935+fightme-cmd@users.noreply.github.com> Date: Wed Jun 17 13:00:57 2026 +0200 Initial commit: Deklinationstrainer German declension trainer Vite + React app for daily German declension practice with localStorage-backed progress. Includes Coolify deploy instructions. Co-Authored-By: Claude Opus 4.8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70e69f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.DS_Store +*.log +.env +.env.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..668e596 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Deklinationstrainer + +A daily-practice German declension trainer. Self-hosted so your progress actually saves. + +## Deploy to Coolify + +### One-time setup + +1. Push these files to a git repo (GitHub / Gitea / whatever Coolify is connected to). + +2. In Coolify, create a new **Application**: + - Source: your git repo + - Build pack: **Nixpacks** (auto-detects Vite) or **Static** + - Build command: `npm run build` + - Publish directory: `dist` + - Port: not needed for static output (Coolify serves it directly) + +3. Add a domain (e.g. `deklination.yourdomain.com`). Coolify provisions HTTPS via Caddy/Traefik automatically. + +4. Deploy. Visit the URL on any device. + +### Local dev + +```bash +npm install +npm run dev # http://localhost:5173 +npm run build # outputs to ./dist +``` + +## On iPhone + +Open the URL in Safari, tap Share → **Add to Home Screen**. It opens fullscreen like a native app, and progress saves in localStorage across reboots, sessions, and app closes. + +## Storage + +The app tries `window.storage` first (Claude artifact environment), then falls back to `localStorage`. When self-hosted, only localStorage runs, which iOS Safari persists reliably. + +To wipe progress: open browser devtools → Application → Local Storage → delete the `gd-v10` key. Or on iPhone: Settings → Safari → Advanced → Website Data → search your domain → delete. diff --git a/assets/Dativpräpositionen - Blue Danube.m4a b/assets/Dativpräpositionen - Blue Danube.m4a new file mode 100644 index 0000000..031b7c2 Binary files /dev/null and b/assets/Dativpräpositionen - Blue Danube.m4a differ diff --git a/assets/Ode To Joy Beethoven Symphony No.9 Op.125 #classicalmusic #readingstudy #beethoven.mp3 b/assets/Ode To Joy Beethoven Symphony No.9 Op.125 #classicalmusic #readingstudy #beethoven.mp3 new file mode 100644 index 0000000..98cf948 Binary files /dev/null and b/assets/Ode To Joy Beethoven Symphony No.9 Op.125 #classicalmusic #readingstudy #beethoven.mp3 differ diff --git a/assets/The Blue Danube - Johann Strauss.mp3 b/assets/The Blue Danube - Johann Strauss.mp3 new file mode 100644 index 0000000..e57a188 Binary files /dev/null and b/assets/The Blue Danube - Johann Strauss.mp3 differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..2ce6aea --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + Beug + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b367195 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1680 @@ +{ + "name": "deklinationstrainer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deklinationstrainer", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "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/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "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": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d21da85 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "deklinationstrainer", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --host" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^5.4.11" + } +} diff --git a/public/audio/blue-danube.mp3 b/public/audio/blue-danube.mp3 new file mode 100644 index 0000000..e57a188 Binary files /dev/null and b/public/audio/blue-danube.mp3 differ diff --git a/public/audio/dativ-blue-danube-sung.m4a b/public/audio/dativ-blue-danube-sung.m4a new file mode 100644 index 0000000..031b7c2 Binary files /dev/null and b/public/audio/dativ-blue-danube-sung.m4a differ diff --git a/public/audio/ode-to-joy.mp3 b/public/audio/ode-to-joy.mp3 new file mode 100644 index 0000000..98cf948 Binary files /dev/null and b/public/audio/ode-to-joy.mp3 differ diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..f4ef2f1 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,1475 @@ +import { useState, useEffect, useCallback, useRef } from "react"; + +// ═══════════════════════════════════════════ +// STORAGE (window.storage in Claude, localStorage when deployed) +// ═══════════════════════════════════════════ + +const SKEY = 'gd-v10'; +const Storage = { + async get() { + try { + if (typeof window !== 'undefined' && window.storage?.get) { + const r = await window.storage.get(SKEY); + if (r?.value) return JSON.parse(r.value); + } + } catch (e) {} + try { + const v = localStorage.getItem(SKEY); + if (v) return JSON.parse(v); + } catch (e) {} + return null; + }, + async set(state) { + const json = JSON.stringify(state); + try { + if (typeof window !== 'undefined' && window.storage?.set) { + await window.storage.set(SKEY, json); + return; + } + } catch (e) {} + try { localStorage.setItem(SKEY, json); } catch (e) {} + } +}; + +// ═══════════════════════════════════════════ +// DATA +// ═══════════════════════════════════════════ + +const DEF = { + masculine:{nominative:'der',accusative:'den',dative:'dem',genitive:'des'}, + feminine: {nominative:'die',accusative:'die',dative:'der',genitive:'der'}, + neuter: {nominative:'das',accusative:'das',dative:'dem',genitive:'des'}, + plural: {nominative:'die',accusative:'die',dative:'den',genitive:'der'}, +}; +const INDEF = { + masculine:{nominative:'ein', accusative:'einen',dative:'einem',genitive:'eines'}, + feminine: {nominative:'eine',accusative:'eine', dative:'einer',genitive:'einer'}, + neuter: {nominative:'ein', accusative:'ein', dative:'einem',genitive:'eines'}, + plural: {nominative:'keine',accusative:'keine',dative:'keinen',genitive:'keiner'}, +}; +const STRONG = { + masculine:{nominative:'r',accusative:'n',dative:'m',genitive:'n'}, + feminine: {nominative:'e',accusative:'e',dative:'r',genitive:'r'}, + neuter: {nominative:'s',accusative:'s',dative:'m',genitive:'n'}, + plural: {nominative:'e',accusative:'e',dative:'n',genitive:'r'}, +}; +const WEAK = { + masculine:{nominative:'e',accusative:'n',dative:'n',genitive:'n'}, + feminine: {nominative:'e',accusative:'e',dative:'n',genitive:'n'}, + neuter: {nominative:'e',accusative:'e',dative:'n',genitive:'n'}, + plural: {nominative:'n',accusative:'n',dative:'n',genitive:'n'}, +}; + +const PREPS = { + durch:{c:'accusative',en:'through'}, für:{c:'accusative',en:'for'}, + gegen:{c:'accusative',en:'against'}, ohne:{c:'accusative',en:'without'}, + um:{c:'accusative',en:'around'}, bis:{c:'accusative',en:'until'}, + aus:{c:'dative',en:'out of'}, bei:{c:'dative',en:'at'}, + mit:{c:'dative',en:'with'}, nach:{c:'dative',en:'to/after'}, + seit:{c:'dative',en:'since'}, von:{c:'dative',en:'from'}, + zu:{c:'dative',en:'to'}, außer:{c:'dative',en:'besides'}, + gegenüber:{c:'dative',en:'opposite'}, + trotz:{c:'genitive',en:'despite'}, während:{c:'genitive',en:'during'}, + wegen:{c:'genitive',en:'because of'}, statt:{c:'genitive',en:'instead of'}, + anstatt:{c:'genitive',en:'instead of'}, außerhalb:{c:'genitive',en:'outside'}, + innerhalb:{c:'genitive',en:'inside'}, + an:{c:'wechsel',en:'at/on',m:'to',l:'at'}, + auf:{c:'wechsel',en:'on',m:'onto',l:'on'}, + hinter:{c:'wechsel',en:'behind',m:'behind',l:'behind'}, + in:{c:'wechsel',en:'in/into',m:'into',l:'in'}, + neben:{c:'wechsel',en:'next to',m:'next to',l:'next to'}, + über:{c:'wechsel',en:'over/above',m:'over',l:'above'}, + unter:{c:'wechsel',en:'under',m:'under',l:'under'}, + vor:{c:'wechsel',en:'in front of',m:'in front of',l:'in front of'}, + zwischen:{c:'wechsel',en:'between',m:'between',l:'between'}, +}; +const PREPS_BY_CASE = { + accusative:['durch','für','gegen','ohne','um','bis'], + dative:['aus','bei','mit','nach','seit','von','zu','außer','gegenüber'], + genitive:['trotz','während','wegen','statt','anstatt','außerhalb','innerhalb'], + wechsel:['an','auf','hinter','in','neben','über','unter','vor','zwischen'], +}; +const COMMON_PREPS = ['mit','von','zu','in','an','auf','für','durch','ohne','um','aus','bei','nach','vor','über','wegen']; +const RARE_PREPS = ['gegen','bis','seit','außer','gegenüber','hinter','neben','unter','zwischen','trotz','während','statt','anstatt','außerhalb','innerhalb']; +const ALL_PREPS = [...COMMON_PREPS, ...RARE_PREPS]; + +const WECHSEL_EX = { + an: {noun:'Tür', g:'feminine', en:'door', mot:'to', loc:'at'}, + auf: {noun:'Tisch', g:'masculine', en:'table', mot:'onto', loc:'on'}, + hinter: {noun:'Haus', g:'neuter', en:'house', mot:'behind', loc:'behind'}, + in: {noun:'Park', g:'masculine', en:'park', mot:'into', loc:'in'}, + neben: {noun:'Tisch', g:'masculine', en:'table', mot:'next to', loc:'next to'}, + über: {noun:'Brücke', g:'feminine', en:'bridge', mot:'over', loc:'above'}, + unter: {noun:'Brücke', g:'feminine', en:'bridge', mot:'under', loc:'under'}, + vor: {noun:'Haus', g:'neuter', en:'house', mot:'in front of', loc:'in front of'}, + zwischen: {noun:'Bäume', g:'plural', en:'trees', mot:'between', loc:'between', plDat:'n'}, +}; +function wechselExample(prepKey, isMotion) { + const ex = WECHSEL_EX[prepKey]; + const cas = isMotion ? 'accusative' : 'dative'; + const art = DEF[ex.g][cas]; + let n = ex.noun; + if (ex.g === 'plural' && !isMotion && ex.plDat) n += ex.plDat; + return { de: `${prepKey} ${art} ${n}`, en: `${isMotion ? ex.mot : ex.loc} the ${ex.en}` }; +} + +const NOUNS = [ + {w:'Hund',g:'masculine',en:'dog'},{w:'Mann',g:'masculine',en:'man'}, + {w:'Tisch',g:'masculine',en:'table'},{w:'Park',g:'masculine',en:'park'}, + {w:'Arzt',g:'masculine',en:'doctor'},{w:'Freund',g:'masculine',en:'friend'}, + {w:'Baum',g:'masculine',en:'tree'},{w:'Garten',g:'masculine',en:'garden'}, + {w:'Stuhl',g:'masculine',en:'chair'},{w:'Bruder',g:'masculine',en:'brother'}, + {w:'Lehrer',g:'masculine',en:'teacher'},{w:'Vater',g:'masculine',en:'father'}, + {w:'Frau',g:'feminine',en:'woman'},{w:'Katze',g:'feminine',en:'cat'}, + {w:'Schule',g:'feminine',en:'school'},{w:'Stadt',g:'feminine',en:'city'}, + {w:'Straße',g:'feminine',en:'street'},{w:'Tür',g:'feminine',en:'door'}, + {w:'Schwester',g:'feminine',en:'sister'},{w:'Mutter',g:'feminine',en:'mother'}, + {w:'Tochter',g:'feminine',en:'daughter'},{w:'Musik',g:'feminine',en:'music'}, + {w:'Nacht',g:'feminine',en:'night'},{w:'Woche',g:'feminine',en:'week'}, + {w:'Kind',g:'neuter',en:'child'},{w:'Haus',g:'neuter',en:'house'}, + {w:'Buch',g:'neuter',en:'book'},{w:'Auto',g:'neuter',en:'car'}, + {w:'Mädchen',g:'neuter',en:'girl'},{w:'Fenster',g:'neuter',en:'window'}, + {w:'Hotel',g:'neuter',en:'hotel'},{w:'Museum',g:'neuter',en:'museum'}, + {w:'Büro',g:'neuter',en:'office'},{w:'Restaurant',g:'neuter',en:'restaurant'}, +]; + +const PRONOUNS = [ + {key:'ich', nom:'ich', acc:'mich', dat:'mir', en:'I', enObj:'me', tag:'1st person', num:'sg'}, + {key:'du', nom:'du', acc:'dich', dat:'dir', en:'you', enObj:'you', tag:'2nd informal', num:'sg'}, + {key:'Sie_sg', nom:'Sie', acc:'Sie', dat:'Ihnen', en:'you', enObj:'you', tag:'2nd formal', num:'sg'}, + {key:'er', nom:'er', acc:'ihn', dat:'ihm', en:'he', enObj:'him', tag:'3rd masculine', num:'sg'}, + {key:'sie_sg', nom:'sie', acc:'sie', dat:'ihr', en:'she', enObj:'her', tag:'3rd feminine', num:'sg'}, + {key:'es', nom:'es', acc:'es', dat:'ihm', en:'it', enObj:'it', tag:'3rd neuter', num:'sg'}, + {key:'wir', nom:'wir', acc:'uns', dat:'uns', en:'we', enObj:'us', tag:'1st plural', num:'pl'}, + {key:'ihr', nom:'ihr', acc:'euch', dat:'euch', en:"y'all", enObj:"y'all", tag:'2nd inf plural', num:'pl'}, + {key:'Sie_pl', nom:'Sie', acc:'Sie', dat:'Ihnen', en:'you (formal)', enObj:'you (formal)', tag:'2nd formal pl', num:'pl'}, + {key:'sie_pl', nom:'sie', acc:'sie', dat:'ihnen', en:'they', enObj:'them', tag:'3rd plural', num:'pl'}, +]; + +const POSSESSIVES = [ + {stem:'mein', en:'my'}, + {stem:'dein', en:'your (informal)'}, + {stem:'sein', en:'his / its'}, + {stem:'ihr', en:'her / their'}, + {stem:'unser', en:'our'}, + {stem:'euer', en:"y'all's"}, +]; +const POSS_ENDINGS = { + masculine:{nominative:'', accusative:'en', dative:'em', genitive:'es'}, + feminine: {nominative:'e', accusative:'e', dative:'er', genitive:'er'}, + neuter: {nominative:'', accusative:'', dative:'em', genitive:'es'}, + plural: {nominative:'e', accusative:'e', dative:'en', genitive:'er'}, +}; +const combinePoss = (stem, ending) => (stem === 'euer' && ending) ? 'eur' + ending : stem + ending; + +const CL = {nominative:'Nominativ',accusative:'Akkusativ',dative:'Dativ',genitive:'Genitiv',wechsel:'Wechsel'}; +const CASE_C = {nominative:'#2563eb',accusative:'#dc2626',dative:'#ea580c',genitive:'#16a34a',wechsel:'#7c3aed'}; +const GENDER_C = {masculine:'#16a34a',feminine:'#db2777',neuter:'#2563eb',plural:'#7c3aed'}; + +const VERBS = { + sein: {de:{Er:'ist',Sie:'ist',Wir:'sind',Ich:'bin'}, en:{Er:'is',Sie:'is',Wir:'are',Ich:'am'}}, + stehen: {de:{Er:'steht',Sie:'steht',Wir:'stehen',Ich:'stehe'}, en:{Er:'is standing',Sie:'is standing',Wir:'are standing',Ich:'am standing'}}, + sitzen: {de:{Er:'sitzt',Sie:'sitzt',Wir:'sitzen',Ich:'sitze'}, en:{Er:'is sitting',Sie:'is sitting',Wir:'are sitting',Ich:'am sitting'}}, + warten: {de:{Er:'wartet',Sie:'wartet',Wir:'warten',Ich:'warte'}, en:{Er:'waits',Sie:'waits',Wir:'wait',Ich:'wait'}}, + wohnen: {de:{Er:'wohnt',Sie:'wohnt',Wir:'wohnen',Ich:'wohne'}, en:{Er:'lives',Sie:'lives',Wir:'live',Ich:'live'}}, + gehen: {de:{Er:'geht',Sie:'geht',Wir:'gehen',Ich:'gehe'}, en:{Er:'walks',Sie:'walks',Wir:'walk',Ich:'walk'}}, + laufen: {de:{Er:'läuft',Sie:'läuft',Wir:'laufen',Ich:'laufe'}, en:{Er:'runs',Sie:'runs',Wir:'run',Ich:'run'}}, + fahren: {de:{Er:'fährt',Sie:'fährt',Wir:'fahren',Ich:'fahre'}, en:{Er:'drives',Sie:'drives',Wir:'drive',Ich:'drive'}}, + kommen: {de:{Er:'kommt',Sie:'kommt',Wir:'kommen',Ich:'komme'}, en:{Er:'comes',Sie:'comes',Wir:'come',Ich:'come'}}, + reisen: {de:{Er:'reist',Sie:'reist',Wir:'reisen',Ich:'reise'}, en:{Er:'travels',Sie:'travels',Wir:'travel',Ich:'travel'}}, +}; +const SUBJ_EN = {Er:'He',Sie:'She',Wir:'We',Ich:'I'}; +const MOTION_VS = ['gehen','laufen','fahren','kommen','reisen']; +const LOCATION_VS = ['sein','stehen','sitzen','warten','wohnen']; + +function mkv(stems) { + return { + ich: stems.ich, du: stems.du, + er: stems.er, sie_sg: stems.er, es: stems.er, + wir: stems.wir, sie_pl: stems.wir, Sie_sg: stems.wir, Sie_pl: stems.wir, + ihr: stems.ihr, + }; +} +const VC = { + kommen: {de:mkv({ich:'komme',du:'kommst',er:'kommt',wir:'kommen',ihr:'kommt'}), en:mkv({ich:'come',du:'come',er:'comes',wir:'come',ihr:'come'})}, + sein: {de:mkv({ich:'bin',du:'bist',er:'ist',wir:'sind',ihr:'seid'}), en:mkv({ich:'am',du:'are',er:'is',wir:'are',ihr:'are'})}, + haben: {de:mkv({ich:'habe',du:'hast',er:'hat',wir:'haben',ihr:'habt'}), en:mkv({ich:'have',du:'have',er:'has',wir:'have',ihr:'have'})}, + sehen: {de:mkv({ich:'sehe',du:'siehst',er:'sieht',wir:'sehen',ihr:'seht'}), en:mkv({ich:'see',du:'see',er:'sees',wir:'see',ihr:'see'})}, + kennen: {de:mkv({ich:'kenne',du:'kennst',er:'kennt',wir:'kennen',ihr:'kennt'}), en:mkv({ich:'know',du:'know',er:'knows',wir:'know',ihr:'know'})}, + lieben: {de:mkv({ich:'liebe',du:'liebst',er:'liebt',wir:'lieben',ihr:'liebt'}), en:mkv({ich:'love',du:'love',er:'loves',wir:'love',ihr:'love'})}, + helfen: {de:mkv({ich:'helfe',du:'hilfst',er:'hilft',wir:'helfen',ihr:'helft'}), en:mkv({ich:'help',du:'help',er:'helps',wir:'help',ihr:'help'})}, + geben: {de:mkv({ich:'gebe',du:'gibst',er:'gibt',wir:'geben',ihr:'gebt'}), en:mkv({ich:'give',du:'give',er:'gives',wir:'give',ihr:'give'})}, +}; + +const SG_PRONOUN_KEYS = ['ich','du','er','sie_sg','es']; +const PL_PRONOUN_KEYS = ['wir','ihr','sie_pl']; +const FORMAL_PRONOUN_KEYS = ['Sie_sg','Sie_pl']; +const ALL_PRONOUN_KEYS = PRONOUNS.map(p=>p.key); +const SAFE_SUBJECT_KEYS = ['ich','du','er','wir','ihr']; + +const TABS = [ + {id:'prep', label:'Prepositions'}, + {id:'article', label:'Articles'}, + {id:'poss', label:'Possessives'}, + {id:'pronoun', label:'Pronouns'}, +]; + +const LEVELS = [ + {id:1, tab:'article', name:'Nominativ', desc:'Gender basics: der, die, or das?', cases:['nominative'], preps:[], type:'article', artType:'definite'}, + {id:2, tab:'article', name:'Akkusativ', desc:'durch, für, gegen, ohne, um, bis', cases:['accusative'], preps:PREPS_BY_CASE.accusative, type:'article', artType:'definite'}, + {id:3, tab:'article', name:'Dativ', desc:'mit, von, zu, aus, bei, nach, seit...', cases:['dative'], preps:PREPS_BY_CASE.dative, type:'article', artType:'definite'}, + {id:4, tab:'article', name:'Wechselpräpositionen', desc:'Two-way: motion (Akk) vs location (Dat)', cases:['accusative','dative'], preps:PREPS_BY_CASE.wechsel, type:'article', artType:'definite', twoWay:true}, + {id:4.5, tab:'article', name:'Kontraktionen', desc:'Fused forms in context: ins, am, zum…', cases:['accusative','dative'], type:'chunk', artType:'definite'}, + {id:5, tab:'article', name:'Genitiv', desc:'trotz, während, wegen, statt...', cases:['genitive'], preps:PREPS_BY_CASE.genitive, type:'article', artType:'definite'}, + {id:6, tab:'article', name:'Alles gemischt', desc:'All prepositions, definite articles', cases:['nominative','accusative','dative','genitive'], preps:[...PREPS_BY_CASE.accusative,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'definite', mixed:true}, + {id:7, tab:'article', name:'ein-Wörter', desc:'Everything with ein/eine/einem...', cases:['nominative','accusative','dative','genitive'], preps:[...PREPS_BY_CASE.accusative,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'indefinite', mixed:true}, + + {id:8, tab:'prep', name:'Häufige Präpositionen', desc:'16 most common prepositions', preps:COMMON_PREPS, type:'prep'}, + {id:9, tab:'prep', name:'Seltene Präpositionen', desc:'15 less common prepositions', preps:RARE_PREPS, type:'prep'}, + {id:10, tab:'prep', name:'Alle Präpositionen', desc:'All 31 prepositions mixed', preps:ALL_PREPS, type:'prep'}, + {id:18, tab:'prep', name:'Wechsel im Kontext', desc:'Real sentences: Akkusativ or Dativ?', preps:PREPS_BY_CASE.wechsel, type:'wechselContext'}, + {id:19, tab:'prep', name:'Kontraktionen', desc:'Contractions: am, ins, zum, beim…', type:'contraction'}, + + {id:11, tab:'poss', name:'Häufige Possessive', desc:'mein · dein · sein — all cases', possessives:['mein','dein','sein'], cases:['nominative','accusative','dative'], type:'poss'}, + {id:12, tab:'poss', name:'Schwierige Possessive', desc:'ihr · unser · euer — all cases', possessives:['ihr','unser','euer'], cases:['nominative','accusative','dative'], type:'poss'}, + {id:13, tab:'poss', name:'Alle gemischt', desc:'mein, dein, sein, ihr, unser, euer — incl. Genitiv', possessives:['mein','dein','sein','ihr','unser','euer'], cases:['nominative','accusative','dative','genitive'], type:'poss'}, + + {id:14, tab:'pronoun', name:'Singular Pronomen', desc:'ich, du, er, sie, es — all cases', pronouns:SG_PRONOUN_KEYS, cases:['nominative','accusative','dative'], type:'pronoun'}, + {id:15, tab:'pronoun', name:'Plural Pronomen', desc:'wir, ihr, sie — all cases', pronouns:PL_PRONOUN_KEYS, cases:['nominative','accusative','dative'], type:'pronoun'}, + {id:16, tab:'pronoun', name:'Sie (formal)', desc:'Formal Sie — singular and plural', pronouns:FORMAL_PRONOUN_KEYS, cases:['nominative','accusative','dative'], type:'pronoun'}, + {id:17, tab:'pronoun', name:'Alle gemischt', desc:'All 10 pronouns, all cases', pronouns:ALL_PRONOUN_KEYS, cases:['nominative','accusative','dative'], type:'pronoun'}, +]; + +// Audio served from /public/audio +const AUDIO = { + dativSung: '/audio/dativ-blue-danube-sung.m4a', + blueDanube: '/audio/blue-danube.mp3', + odeToJoy: '/audio/ode-to-joy.mp3', +}; + +// Chunked study cards for the Prepositions "Lernen" section. +// Mnemonics reproduced from the U-Michigan German Resources page. +const LEARN_CARDS = [ + { + case:'accusative', label:'Akkusativ', desc:'These prepositions always take the Accusative.', + words:[ + {de:'durch', en:'through'}, {de:'für', en:'for'}, {de:'gegen', en:'against'}, + {de:'ohne', en:'without'}, {de:'um', en:'around / at'}, {de:'bis', en:'until / up to'}, + ], + mnemonics:[ + {label:'Acronym', text:'“O Fudge” — ohne, für, um, durch, gegen.'}, + {label:'Rhyme', text:'“Durch-für-gegen-ohne-um, Deutsch zu lernen ist nicht dumm.”'}, + ], + audio:[], + }, + { + case:'dative', label:'Dativ', desc:'These prepositions always take the Dative.', + words:[ + {de:'aus', en:'out of'}, {de:'außer', en:'except for'}, {de:'bei', en:'at / near'}, + {de:'mit', en:'with'}, {de:'nach', en:'to / after'}, {de:'seit', en:'since'}, + {de:'von', en:'from'}, {de:'zu', en:'to'}, {de:'gegenüber', en:'opposite'}, + ], + mnemonics:[ + {label:'Sing it', text:'Sing “Aus-außer-bei-mit, nach-seit, von-zu” to the tune of the “Blue Danube” waltz.'}, + {label:'Rhyme', text:'“Roses are red, violets are blue, aus-außer-bei-mit, nach-seit, von-zu.”'}, + ], + audio:[ + {label:'🎤 Sung to the Blue Danube', src:AUDIO.dativSung}, + {label:'🎻 The melody — The Blue Danube', src:AUDIO.blueDanube}, + ], + }, + { + case:'wechsel', label:'Wechselpräpositionen', desc:'Two-way: motion → Akkusativ (wohin?), staying put → Dativ (wo?).', + words:[ + {de:'an', en:'at / on'}, {de:'auf', en:'on / onto'}, {de:'hinter', en:'behind'}, + {de:'in', en:'in / into'}, {de:'neben', en:'next to'}, {de:'über', en:'over / above'}, + {de:'unter', en:'under'}, {de:'vor', en:'in front of'}, {de:'zwischen', en:'between'}, + ], + mnemonics:[ + {label:'Sing it', text:'Sing “An, auf, hin-ter, ne-ben, un-ter/ü-ber, in, vor, zwi-i-schen” to the tune of “An die Freude” (“Ode to Joy”) from Beethoven’s 9th.'}, + ], + audio:[ + {label:'🎼 The melody — Ode to Joy', src:AUDIO.odeToJoy}, + ], + }, + { + case:'genitive', label:'Genitiv', desc:'These take the Genitive — only a handful, so just learn the list.', + words:[ + {de:'trotz', en:'despite'}, {de:'während', en:'during'}, + {de:'wegen', en:'because of'}, {de:'(an)statt', en:'instead of'}, + ], + mnemonics:[], + note:'No song for these — there are only a few. Formal writing uses the genitive; casual speech increasingly uses the dative (e.g. „wegen dem Wetter"). Michigan also lists außerhalb / innerhalb / diesseits / jenseits, which must stay genitive.', + audio:[], + }, +]; + +// Contractions (preposition + article). pref:true = preferred even in writing. +const CONTRACTIONS = [ + {prep:'an', art:'das', form:'ans', pref:true, c:'wechsel'}, + {prep:'an', art:'dem', form:'am', pref:true, c:'wechsel'}, + {prep:'in', art:'das', form:'ins', pref:true, c:'wechsel'}, + {prep:'in', art:'dem', form:'im', pref:true, c:'wechsel'}, + {prep:'auf',art:'das', form:'aufs',pref:true, c:'wechsel'}, + {prep:'bei',art:'dem', form:'beim',pref:true, c:'dative'}, + {prep:'von',art:'dem', form:'vom', pref:true, c:'dative'}, + {prep:'zu', art:'dem', form:'zum', pref:true, c:'dative'}, + {prep:'zu', art:'der', form:'zur', pref:true, c:'dative'}, + {prep:'durch',art:'das',form:'durchs',pref:false,c:'accusative'}, + {prep:'für',art:'das', form:'fürs',pref:false, c:'accusative'}, + {prep:'um', art:'das', form:'ums', pref:false, c:'accusative'}, + {prep:'über',art:'das',form:'übers',pref:false,c:'wechsel'}, + {prep:'unter',art:'das',form:'unters',pref:false,c:'wechsel'}, + {prep:'vor',art:'das', form:'vors',pref:false, c:'wechsel'}, + {prep:'hinter',art:'das',form:'hinters',pref:false,c:'wechsel'}, +]; + +// Only PREFERRED contractions drive the in-context tests, so the contracted +// form is unambiguously the best answer (the two-word form is the distractor). +const CONTRACTABLE = {}; +CONTRACTIONS.filter(c=>c.pref).forEach(c => { (CONTRACTABLE[c.prep] = CONTRACTABLE[c.prep] || {})[c.art] = c.form; }); +// Prepositions that get the fused "chunk" question in mixed/dedicated lessons. +const CHUNK_PREPS = new Set(Object.keys(CONTRACTABLE)); // an, in, auf, bei, von, zu +const WECHSEL_SET = new Set(PREPS_BY_CASE.wechsel); + +const pick = a => a[Math.floor(Math.random()*a.length)]; +const cap = s => s.charAt(0).toUpperCase() + s.slice(1); + +// ═══════════════════════════════════════════ +// QUESTION GENERATORS +// ═══════════════════════════════════════════ + +function genArticleQ(level) { + const lv = LEVELS.find(l=>l.id===level); + const noun = pick(NOUNS); + const tbl = lv.artType === 'indefinite' ? INDEF : DEF; + let cas, prepKey, tw = null, verb; + if (lv.id === 1) { cas='nominative'; prepKey=null; } + else if (lv.twoWay) { prepKey=pick(lv.preps); tw=Math.random()>0.5; cas=tw?'accusative':'dative'; } + else if (lv.mixed) { + if (Math.random()<0.12) { cas='nominative'; prepKey=null; } + else { prepKey=pick(lv.preps); const pc=PREPS[prepKey].c; + if (pc==='wechsel') { tw=Math.random()>0.5; cas=tw?'accusative':'dative'; } else cas=pc; } + } else { prepKey=pick(lv.preps); cas=lv.cases[0]; } + + // Mixed definite lessons: contraction-capable prep → fused "chunk" question. + if (lv.mixed && lv.artType === 'definite' && prepKey && CHUNK_PREPS.has(prepKey)) { + return makeChunkQ(prepKey, noun, cas, tw); + } + + const ans = tbl[noun.g][cas]; + const _pool = lv.artType === 'indefinite' ? ['ein','eine','einen','einem','eines','einer','keine','keinen','keiner'] : ['der','die','das','den','dem','des']; + const _rem = _pool.filter(a => a !== ans); + const _picked = []; + while (_picked.length < 3 && _rem.length) _picked.push(_rem.splice(Math.floor(Math.random()*_rem.length), 1)[0]); + const choices = [ans, ..._picked].sort(() => Math.random() - 0.5); + let sentence, translation, rule; + if (!prepKey) { + sentence = `_____ ${noun.w} ist hier.`; + translation = `The ${noun.en} is here.`; + rule = 'Subject of the sentence → Nominativ'; + } else { + const subj = pick(['Er','Sie','Wir']); + if (tw===true) verb=pick(MOTION_VS); + else if (tw===false) verb=pick(LOCATION_VS); + else if (cas==='genitive') verb=pick(['warten','wohnen']); + else verb=pick(MOTION_VS); + sentence = `${subj} ${VERBS[verb].de[subj]} ${prepKey} _____ ${noun.w}.`; + let pEn = PREPS[prepKey].en; + if (PREPS[prepKey].c==='wechsel') pEn = tw ? PREPS[prepKey].m : PREPS[prepKey].l; + translation = `${SUBJ_EN[subj]} ${VERBS[verb].en[subj]} ${pEn} the ${noun.en}.`; + rule = PREPS[prepKey].c==='wechsel' + ? (tw ? `„${prepKey}" + motion (wohin?) → Akkusativ` : `„${prepKey}" + location (wo?) → Dativ`) + : `„${prepKey}" → always ${CL[cas]}`; + } + return {type:'article', noun, cas, prepKey, ans, sentence, translation, rule, tw, artType:lv.artType, choices}; +} + +function genPrepQ(level) { + const lv = LEVELS.find(l=>l.id===level); + const prepKey = pick(lv.preps); + const p = PREPS[prepKey]; + const exampleNoun = pick(NOUNS); + const exCase = p.c==='wechsel' ? 'dative' : p.c; + return {type:'prep', prepKey, prepEn:p.en, ans:p.c, + example:`${prepKey} ${DEF[exampleNoun.g][exCase]} ${exampleNoun.w}`, exampleEn:exampleNoun.en}; +} + +function genWechselContextQ(level) { + const lv = LEVELS.find(l=>l.id===level); + const prepKey = pick(lv.preps); + const isMotion = Math.random() > 0.5; + const cas = isMotion ? 'accusative' : 'dative'; + const subj = pick(['Er','Sie','Wir','Ich']); + const verb = isMotion ? pick(MOTION_VS) : pick(LOCATION_VS); + const ex = WECHSEL_EX[prepKey]; + const art = DEF[ex.g][cas]; + let nounStr = ex.noun; + if (ex.g === 'plural' && !isMotion && ex.plDat) nounStr += ex.plDat; + const sentence = `${subj} ${VERBS[verb].de[subj]} ${prepKey} ${art} ${nounStr}.`; + const pEn = isMotion ? ex.mot : ex.loc; + const translation = `${SUBJ_EN[subj]} ${VERBS[verb].en[subj]} ${pEn} the ${ex.en}.`; + return {type:'wechselContext', prepKey, ans:cas, isMotion, verb, sentence, translation, nounEn: ex.en}; +} + +function genPossQ(level) { + const lv = LEVELS.find(l=>l.id===level); + const noun = pick(NOUNS); + const stem = pick(lv.possessives); + const possMeaning = POSSESSIVES.find(p=>p.stem===stem).en; + let cas, prepKey=null, tw=null, verb; + const wantPrep = Math.random() > 0.5; + if (wantPrep) { + const allPreps = lv.cases.includes('genitive') + ? [...PREPS_BY_CASE.accusative,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.wechsel,...PREPS_BY_CASE.genitive] + : [...PREPS_BY_CASE.accusative,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.wechsel]; + prepKey = pick(allPreps); + const pc = PREPS[prepKey].c; + if (pc==='wechsel') { tw=Math.random()>0.5; cas=tw?'accusative':'dative'; } else cas=pc; + if (!lv.cases.includes(cas)) { cas=pick(lv.cases); prepKey=null; tw=null; } + } else { + cas = pick(lv.cases.filter(c => c !== 'genitive')) || lv.cases[0]; + } + const ans = combinePoss(stem, POSS_ENDINGS[noun.g][cas]); + let sentence, translation, rule; + if (!prepKey) { + if (cas==='nominative') { + sentence = `_____ ${noun.w} ist hier.`; + translation = `${cap(possMeaning)} ${noun.en} is here.`; + } else if (cas==='accusative') { + const v = pick([['liebe','love'],['sehe','see'],['kenne','know'],['höre','hear']]); + sentence = `Ich ${v[0]} _____ ${noun.w}.`; + translation = `I ${v[1]} ${possMeaning} ${noun.en}.`; + } else if (cas==='dative') { + sentence = `Ich helfe _____ ${noun.w}.`; + translation = `I help ${possMeaning} ${noun.en}.`; + } + } else { + const subj='Ich'; + if (tw===true) verb='gehen'; + else if (tw===false) verb='sein'; + else if (PREPS[prepKey].c==='genitive') verb='warten'; + else verb='gehen'; + sentence = `${subj} ${VERBS[verb].de[subj]} ${prepKey} _____ ${noun.w}.`; + let pEn = PREPS[prepKey].en; + if (PREPS[prepKey].c==='wechsel') pEn = tw ? PREPS[prepKey].m : PREPS[prepKey].l; + translation = `${SUBJ_EN[subj]} ${VERBS[verb].en[subj]} ${pEn} ${possMeaning} ${noun.en}.`; + } + rule = `${stem}- + ${CL[cas]} + ${noun.g} → ${ans}`; + return {type:'poss', noun, stem, possMeaning, cas, ans, sentence, translation, rule, prepKey, tw}; +} + +function genPronounQ(level) { + const lv = LEVELS.find(l=>l.id===level); + const pronKey = pick(lv.pronouns); + const pron = PRONOUNS.find(p=>p.key===pronKey); + const cas = pick(lv.cases); + const rawAns = pron[{nominative:'nom',accusative:'acc',dative:'dat'}[cas]]; + + let sentence, translation, ans; + if (cas === 'nominative') { + const tpl = pick([ + {v:'kommen', dePart:' aus Berlin', enPart:' from Berlin'}, + {v:'sein', dePart:' müde', enPart:' tired'}, + {v:'haben', dePart:' einen Hund', enPart:' a dog'}, + ]); + const vDe = VC[tpl.v].de[pronKey]; + const vEn = VC[tpl.v].en[pronKey]; + sentence = `_____ ${vDe}${tpl.dePart}.`; + translation = `${cap(pron.en)} ${vEn}${tpl.enPart}.`; + ans = cap(rawAns); + } else { + const subjCandidates = SAFE_SUBJECT_KEYS.filter(k => k !== pronKey); + const subjKey = pick(subjCandidates); + const subj = PRONOUNS.find(p=>p.key===subjKey); + let tpl; + if (cas === 'accusative') { + tpl = pick([{v:'sehen'}, {v:'kennen'}, {v:'lieben'}]); + } else { + tpl = pick([{v:'helfen'}, {v:'geben', dePost:' das Buch', enPost:' the book'}]); + } + const sDe = VC[tpl.v].de[subjKey]; + const sEn = VC[tpl.v].en[subjKey]; + const dePost = tpl.dePost || ''; + const enPost = tpl.enPost || ''; + sentence = `${cap(subj.nom)} ${sDe} _____${dePost}.`; + translation = `${cap(subj.en)} ${sEn} ${pron.enObj}${enPost}.`; + ans = rawAns; + } + return {type:'pronoun', pronKey, pron, cas, ans, sentence, translation}; +} + +function genContractionQ() { + const c = pick(CONTRACTIONS); + return {type:'contraction', prep:c.prep, art:c.art, ans:c.form, pref:c.pref, cas:c.c}; +} + +// Build the 4 choices for a fused prep+article question: always a BLEND of +// contracted and two-word forms, so the shape never reveals whether it contracts. +function buildChunkChoices(prepKey, article) { + const map = CONTRACTABLE[prepKey] || {}; + const contracts = !!map[article]; + const correct = contracts ? map[article] : `${prepKey} ${article}`; + const cand = new Set(); + Object.values(map).forEach(f => cand.add(f)); // contraction shapes + Object.keys(map).forEach(a => cand.add(`${prepKey} ${a}`)); // their two-word forms + cand.add(`${prepKey} ${article}`); // two-word of the real article + cand.delete(correct); + const pool = [...cand].sort(() => Math.random() - 0.5); + const choices = [correct, ...pool.slice(0, 3)]; + const filler = ['das','dem','der','die','den'].map(a => `${prepKey} ${a}`); + for (let i = 0; i < filler.length && choices.length < 4; i++) { + if (!choices.includes(filler[i])) choices.push(filler[i]); + } + return { correct, contracts, choices: choices.slice(0, 4).sort(() => Math.random() - 0.5) }; +} + +function pickChunkVerb(prepKey, tw) { + if (PREPS[prepKey].c === 'wechsel') return tw ? pick(MOTION_VS) : pick(LOCATION_VS); + if (prepKey === 'bei') return pick(['sein','wohnen','warten']); + if (prepKey === 'von') return 'kommen'; + if (prepKey === 'zu') return pick(['gehen','fahren','kommen']); + return pick(['gehen','fahren']); +} + +function makeChunkQ(prepKey, noun, cas, tw) { + const article = DEF[noun.g][cas]; + const { correct, contracts, choices } = buildChunkChoices(prepKey, article); + const subj = pick(['Er','Sie','Wir','Ich']); + const verb = pickChunkVerb(prepKey, tw); + const sentence = `${subj} ${VERBS[verb].de[subj]} _____ ${noun.w}.`; + let pEn = PREPS[prepKey].en; + if (PREPS[prepKey].c === 'wechsel') pEn = tw ? PREPS[prepKey].m : PREPS[prepKey].l; + const translation = `${SUBJ_EN[subj]} ${VERBS[verb].en[subj]} ${pEn} the ${noun.en}.`; + const pref = contracts ? (CONTRACTIONS.find(x => x.prep === prepKey && x.art === article)?.pref ?? true) : null; + return { type:'chunk', prepKey, noun, cas, ans:correct, choices, sentence, translation, tw, contracts, pref }; +} + +// Dedicated "Kontraktionen" level: answer ALWAYS contracts (preferred forms only). +function genContractionContextQ() { + const c = pick(CONTRACTIONS.filter(x => x.pref && CHUNK_PREPS.has(x.prep))); + const wechsel = WECHSEL_SET.has(c.prep); + let cas, gender, tw = null; + if (c.art === 'das') { cas='accusative'; gender='neuter'; tw = wechsel ? true : null; } + else if (c.art === 'dem') { cas='dative'; gender=pick(['masculine','neuter']); tw = wechsel ? false : null; } + else { cas='dative'; gender='feminine'; tw = wechsel ? false : null; } + const noun = pick(NOUNS.filter(n => n.g === gender)); + return makeChunkQ(c.prep, noun, cas, tw); +} + +function gen(level) { + const lv = LEVELS.find(l=>l.id===level); + if (lv.type==='chunk') return genContractionContextQ(); + if (lv.type==='contraction') return genContractionQ(); + if (lv.type==='prep') return genPrepQ(level); + if (lv.type==='wechselContext') return genWechselContextQ(level); + if (lv.type==='poss') return genPossQ(level); + if (lv.type==='pronoun') return genPronounQ(level); + return genArticleQ(level); +} + +// ═══════════════════════════════════════════ +// MAIN APP +// ═══════════════════════════════════════════ + +const ROUND = 10; +const GREEN = '#58a700'; +const RED = '#dc2626'; + +export default function App() { + const [scr, setScr] = useState('menu'); + const [tab, setTab] = useState('prep'); + const [lv, setLv] = useState(1); + const [q, setQ] = useState(null); + const [inp, setInp] = useState(''); + const [rev, setRev] = useState(false); + const [ok, setOk] = useState(false); + const [res, setRes] = useState([]); + const [chart, setChart] = useState(false); + const [learnStep, setLearnStep] = useState(0); + const [st, setSt] = useState({sc:{},tc:0,ta:0,streak:0,ld:null}); + const [loaded, setLoaded] = useState(false); + const [shake, setShake] = useState(false); + const iref = useRef(null); + + useEffect(()=>{(async()=>{ + const data = await Storage.get(); + if (data) setSt(data); + setLoaded(true); + })();},[]); + + const save = useCallback(async s => { + setSt(s); + await Storage.set(s); + },[]); + + const ALWAYS_UNLOCKED = new Set([1, 2, 8, 11, 14]); + const unlocked = (id) => { + if (ALWAYS_UNLOCKED.has(id)) return true; + const lvObj = LEVELS.find(l => l.id === id); + if (!lvObj) return false; + const tabLevels = LEVELS.filter(l => l.tab === lvObj.tab).sort((a,b) => a.id - b.id); + const idx = tabLevels.findIndex(l => l.id === id); + if (idx === 0) return true; + const prev = tabLevels[idx-1]; + const p = st.sc[prev.id]; + return p && p.t >= 10 && (p.c/p.t) >= 0.7; + }; + + const nextLevelInTab = (id) => { + const lvObj = LEVELS.find(l => l.id === id); + if (!lvObj) return null; + const tabLevels = LEVELS.filter(l => l.tab === lvObj.tab).sort((a,b) => a.id - b.id); + const idx = tabLevels.findIndex(l => l.id === id); + if (idx === -1 || idx === tabLevels.length - 1) return null; + return tabLevels[idx + 1]; + }; + + const startLv = id => { + if (!unlocked(id)) return; + setLv(id); setRes([]); setInp(''); setRev(false); setQ(gen(id)); setScr('practice'); + }; + + const submitRef = useRef(null); + const nextRef = useRef(null); + + const doSubmit = useCallback((overrideInput) => { + const userAns = (overrideInput ?? inp).trim(); + if (!userAns) { setShake(true); setTimeout(()=>setShake(false),400); return; } + const correct = userAns.toLowerCase() === q.ans.toLowerCase(); + if (overrideInput !== undefined) setInp(overrideInput); + // Dismiss mobile keyboard so feedback is visible + try { iref.current?.blur(); } catch(e){} + setOk(correct); setRev(true); + const nr = [...res, correct]; setRes(nr); + const today = new Date().toISOString().slice(0,10); + const ns = {...st}; + ns.ta = (ns.ta||0) + 1; + if (correct) ns.tc = (ns.tc||0) + 1; + if (!ns.sc[lv]) ns.sc[lv] = {c:0,t:0}; + ns.sc[lv] = {c: ns.sc[lv].c + (correct?1:0), t: ns.sc[lv].t + 1}; + if (ns.ld !== today) { + const ld = ns.ld ? new Date(ns.ld) : null; + ns.streak = ld && Math.round((new Date(today)-ld)/864e5)===1 ? (ns.streak||0)+1 : 1; + ns.ld = today; + } + save(ns); + }, [inp, q, res, st, lv, save]); + + const doNext = useCallback(() => { + if (res.length >= ROUND) { setScr('summary'); return; } + setInp(''); setRev(false); setOk(false); setQ(gen(lv)); + }, [res, lv]); + + submitRef.current = doSubmit; + nextRef.current = doNext; + + useEffect(() => { + const h = e => { + if (chart) { if (e.key === 'Escape') setChart(false); return; } + if (scr === 'summary' && e.key === 'Enter') { e.preventDefault(); startLv(lv); return; } + if (scr !== 'practice') return; + if (!rev && (q?.type === 'article' || q?.type === 'chunk') && q.choices) { + const idx = ['1','2','3','4'].indexOf(e.key); + if (idx >= 0 && q.choices[idx]) { e.preventDefault(); submitRef.current(q.choices[idx]); return; } + } + if (!rev && q?.type === 'prep') { + if (e.key === '1') { e.preventDefault(); submitRef.current('accusative'); return; } + if (e.key === '2') { e.preventDefault(); submitRef.current('dative'); return; } + if (e.key === '3') { e.preventDefault(); submitRef.current('genitive'); return; } + if (e.key === '4') { e.preventDefault(); submitRef.current('wechsel'); return; } + } + if (!rev && q?.type === 'wechselContext') { + if (e.key === '1') { e.preventDefault(); submitRef.current('accusative'); return; } + if (e.key === '2') { e.preventDefault(); submitRef.current('dative'); return; } + } + if (e.key === 'Enter') { e.preventDefault(); if (rev) nextRef.current(); else if (q?.type !== 'article' && q?.type !== 'chunk') submitRef.current(); } + if (rev && (e.key === 'ArrowRight' || e.key === ' ')) { e.preventDefault(); nextRef.current(); } + }; + window.addEventListener('keydown', h); + return () => window.removeEventListener('keydown', h); + }); + + if (!loaded) return

Loading...

; + + const rc = res.filter(Boolean).length; + const cur = LEVELS.find(l=>l.id===lv); + const chartSection = scr === 'menu' ? tab : (q?.type === 'wechselContext' ? 'prep' : q?.type === 'chunk' ? 'article' : (q?.type || tab)); + const nextInTab = nextLevelInTab(lv); + + return ( +
+ + +
+
+ {scr !== 'menu' + ? + :
Beug
} +
+ {scr === 'practice' && } + 🔥 {st.streak||0} +
+
+ + {/* MENU */} + {scr === 'menu' && ( +
+
+ {TABS.map(t => ( + + ))} +
+ +
+ {tab === 'prep' && ( +
{ setLearnStep(0); setScr('learn'); }}> +
+
📚 Lernen
+
Chunk the lists first — Akkusativ, Dativ, Wechsel, Genitiv
+
+
+ )} + {LEVELS.filter(l => l.tab === tab).sort((a,b)=>a.id-b.id).map(l => { + const u = unlocked(l.id); + const s = st.sc[l.id]; + const p = s ? Math.round(s.c/s.t*100) : null; + return ( +
startLv(l.id)}> +
+
{l.name} {!u && locked}
+
{l.desc}
+ {s &&
{s.c}/{s.t}
} +
+ {p !== null &&
=70?GREEN:RED}}>{p}%
} +
+ ); + })} +
+ + +
+ )} + + {/* LEARN: chunked study cards for prepositions */} + {scr === 'learn' && (() => { + const card = LEARN_CARDS[learnStep]; + const isLast = learnStep === LEARN_CARDS.length - 1; + return ( +
+
+ {LEARN_CARDS.map((c,i)=>( +
+ ))} +
+ +
+
{card.label}
+
{card.desc}
+ +
+ {card.words.map(w=>( +
+ {w.de} + {w.en} +
+ ))} +
+ + {card.mnemonics.length > 0 && ( +
+
Merkhilfe
+ {card.mnemonics.map((m,i)=>( +
+ {m.label} + {m.text} +
+ ))} +
+ )} + + {card.note && ( +
+ {card.note} +
+ )} + + {card.audio.length > 0 && ( +
+ {card.audio.map(a=>( +
+
{a.label}
+
+ ))} +
+ )} +
+ +
+ {learnStep > 0 && ( + + )} + {!isLast + ? + : } +
+
+ ); + })()} + + {/* PRACTICE: sentence-based (article, poss, pronoun) */} + {scr === 'practice' && (q?.type === 'article' || q?.type === 'chunk' || q?.type === 'poss' || q?.type === 'pronoun') && ( +
+
+ {q.sentence.split('_____').map((part,i,a)=>( + + {part} + {i < a.length-1 && ( + + {rev ? q.ans : (inp || '\u00A0\u00A0\u00A0\u00A0\u00A0')} + + )} + + ))} +
+ +
{q.translation}
+ +
+ {(q.type === 'article' || q.type === 'chunk') && <> + {DEF[q.noun.g].nominative} + {' '}{q.noun.w} + ({q.noun.en}) + } + {q.type === 'poss' && <> + {q.stem}- + ({q.possMeaning}) + } + {q.type === 'pronoun' && <> + {q.pron.nom} + ({q.pron.en}) + · {q.pron.tag} + } +
+ +
+ {CL[q.cas]} + {q.prepKey && <> · {q.prepKey} ({PREPS[q.prepKey].en})} + {q.tw !== null && q.tw !== undefined && <> · {q.tw ? '→ motion' : '● location'}} + {q.type === 'poss' && <> · {DEF[q.noun.g].nominative} {q.noun.w}} +
+ + {!rev ? ( + (q.type === 'article' || q.type === 'chunk') ? ( +
+ {q.choices.map((choice, i) => ( + + ))} +
+ ) : ( +
+
+ setInp(e.target.value)} + style={S.input} + placeholder={q.type==='poss' ? 'form…' : q.type==='pronoun' ? 'pronoun…' : 'article…'} + autoComplete="off" autoCapitalize="off" spellCheck={false} + enterKeyHint="go" + inputMode="text"/> + +
+
or press Enter
+
+ ) + ) : ( + q.type === 'pronoun' + ? + : q.type === 'chunk' + ? + : + )} +
+ )} + + {/* PRACTICE: preposition (which case?) */} + {scr === 'practice' && q?.type === 'prep' && ( +
+
Which case does this preposition take?
+
{q.prepKey}
+
{q.prepEn}
+ {!rev ? ( +
+ {[ + ['accusative','Akkusativ','1'],['dative','Dativ','2'], + ['genitive','Genitiv','3'],['wechsel','Wechsel','4'], + ].map(([val,lab,key]) => ( + + ))} +
+ ) : ( + + )} +
+ )} + + {/* PRACTICE: wechsel in context */} + {scr === 'practice' && q?.type === 'wechselContext' && ( +
+
{q.sentence}
+
{q.translation}
+ +
+ {q.prepKey} + ({PREPS[q.prepKey].en}) +
+
Wechselpräposition · motion or location?
+ + {!rev ? ( +
+ {[['accusative','Akkusativ','1','motion'],['dative','Dativ','2','location']].map(([val,lab,key,hint]) => ( + + ))} +
+ ) : ( + + )} +
+ )} + + {/* PRACTICE: contractions */} + {scr === 'practice' && q?.type === 'contraction' && ( +
+
Contract the preposition and article
+
+ {q.prep} + {q.art} = ? +
+
+ {CL[q.cas]} +
+ {!rev ? ( +
+
+ setInp(e.target.value)} + style={S.input} placeholder="contraction…" + autoComplete="off" autoCapitalize="off" spellCheck={false} + enterKeyHint="go" inputMode="text"/> + +
+
or press Enter
+
+ ) : ( + + )} +
+ )} + + {scr === 'practice' && ( +
+ {Array.from({length:ROUND},(_,i)=>( +
+ ))} + {Math.min(res.length+(rev?0:1), ROUND)}/{ROUND} · {cur?.name} +
+ )} + + {scr === 'practice' && rev && ( +
+ +
+ )} + + {/* SUMMARY */} + {scr === 'summary' && ( +
+
Complete
+
+ {rc}/{ROUND} +
+
+ {res.map((r,i)=>
)} +
+
=7?GREEN:'#d97706', fontSize:15, fontWeight:600, margin:'24px 0'}}> + {rc>=9?'Ausgezeichnet!':rc>=7?'Gut gemacht!':rc>=5?'Keep practicing.':'Review the charts.'} +
+ {rc>=7 && nextInTab && unlocked(nextInTab.id) && ( +
{nextInTab.name} unlocked.
+ )} + + {rc>=7 && nextInTab && unlocked(nextInTab.id) && ( + + )} +
+ +
+
+ )} +
+ + {chart && setChart(false)}/>} + + +
+ ); +} + +// ═══════════════════════════════════════════ +// FEEDBACK COMPONENTS +// ═══════════════════════════════════════════ + +function FillFeedback({ q, inp, ok }) { + const genders = ['masculine','feminine','neuter','plural']; + const stripCells = genders.map(g => { + if (q.type === 'article') { + const tbl = q.artType === 'indefinite' ? INDEF : DEF; + return tbl[g][q.cas]; + } + return combinePoss(q.stem, POSS_ENDINGS[g][q.cas]); + }); + const stripLabel = q.type === 'article' + ? (q.artType === 'indefinite' ? 'ein…' : 'der/die') + : `${q.stem}-`; + + return ( +
+
+ {ok ? '✓ Correct' : <>✗ {inp} {q.ans}} +
+
{q.rule}
+ {!ok && ( +
+ {q.noun.w} is {q.noun.g}. {CL[q.cas]} + {q.noun.g} = {q.ans} +
+ )} + + + + {genders.map(g => )} + + + + {genders.map((g, i) => { + const hl = g === q.noun.g; + return ; + })} + +
{CL[q.cas]}{g.slice(0,4)}.
{stripLabel}{stripCells[i]}
+
+ ); +} + +function PrepFeedback({ q, inp, ok }) { + const motEx = q.ans === 'wechsel' ? wechselExample(q.prepKey, true) : null; + const locEx = q.ans === 'wechsel' ? wechselExample(q.prepKey, false) : null; + + return ( +
+
+ {ok ? <>✓ {CL[q.ans]} : <>✗ {CL[inp]||inp} {CL[q.ans]}} +
+
+ „{q.prepKey}" → {q.ans === 'wechsel' ? 'Wechselpräposition (Akkusativ for motion, Dativ for location)' : `always ${CL[q.ans]}`} +
+ {q.ans !== 'wechsel' && ( +
+ Example: {q.example} ({q.prepEn} the {q.exampleEn}) +
+ )} + {q.ans === 'wechsel' && ( +
+
Motion: Er geht {motEx.de} (Akk) — "{motEx.en}"
+
Location: Er ist {locEx.de} (Dat) — "{locEx.en}"
+
+ )} +
+ ); +} + +function WechselContextFeedback({ q, inp, ok }) { + return ( +
+
+ {ok ? <>✓ {CL[q.ans]} : <>✗ {CL[inp]||inp} {CL[q.ans]}} +
+
+ „{q.verb}" {q.isMotion ? 'implies motion (wohin?)' : 'implies location (wo?)'} → {CL[q.ans]} +
+
+ Wechselpräpositionen take Akkusativ with motion verbs (gehen, fahren, laufen, kommen) and Dativ with location verbs (sein, stehen, sitzen, warten). +
+
+ ); +} + +function ChunkFeedback({ q, inp, ok }) { + const article = DEF[q.noun.g][q.cas]; + return ( +
+
+ {ok + ? <>✓ {q.ans} + : <>✗ {inp} {q.ans}} +
+
+ {q.contracts + ? <>{q.prepKey} + {article} = {q.ans} + : <>„{q.prepKey}" + {CL[q.cas]}{q.ans}} +
+
+ {q.contracts + ? 'Preferred — these fuse, so use the contraction in speech and writing.' + : <>{article} doesn’t fuse with „{q.prepKey}" — keep them as two words.} +
+
+ ); +} + +function ContractionFeedback({ q, inp, ok }) { + return ( +
+
+ {ok + ? <>✓ {q.prep} + {q.art} = {q.ans} + : <>✗ {inp} {q.ans}} +
+
+ {q.prep} + {q.art} = {q.ans} +
+
+ {q.pref + ? 'Preferred — use this contraction in both speech and writing.' + : 'Informal — common when speaking, but the two-word form is more usual in writing.'} +
+
+ ); +} + +function PronounFeedback({ q, inp, ok }) { + const cases = ['nominative','accusative','dative']; + const labels = { nominative:'Nom', accusative:'Acc', dative:'Dat' }; + const keys = { nominative:'nom', accusative:'acc', dative:'dat' }; + return ( +
+
+ {ok ? '✓ Correct' : <>✗ {inp} {q.ans}} +
+
+ {q.pron.en} ({q.pron.tag}) — {CL[q.cas]} form +
+ + + + {cases.map(c => )} + + + + {cases.map(c => { + const hl = c === q.cas; + return ; + })} + +
{q.pron.nom}{labels[c]}
{q.pron.en}{q.pron[keys[c]]}
+
+ ); +} + +// ═══════════════════════════════════════════ +// CHART MODAL +// ═══════════════════════════════════════════ + +function ChartModal({ section, q, onClose }) { + const genders = ['masculine','feminine','neuter','plural']; + const cases = ['nominative','accusative','dative','genitive']; + + return ( +
+
e.stopPropagation()}> +
+
+ {section==='article'?'Articles':section==='prep'?'Prepositions':section==='poss'?'Possessives':section==='pronoun'?'Personal Pronouns':'Charts'} +
+ +
+ + {section === 'article' && <> +
+
All-in-One Declension Chart
+
Strong (S): no article carries the case. Weak (W): after a definite article.
+ + + + + {genders.map(g => )} + + + {genders.map(g => ( + + + + + ))} + + + + {cases.map(c => ( + + + {genders.map(g => { + const hl = q && (q.type==='article'||q.type==='poss') && c === q.cas && g === q.noun?.g; + return ( + + + + + ); + })} + + ))} + +
{g}
SW
{CL[c]}{STRONG[g][c]}{WEAK[g][c]}
+
+
+
Unbestimmter Artikel (ein/eine/kein)
+ +
+
+
Bestimmter Artikel (der/die/das)
+ +
+ } + + {section === 'prep' && ( +
+
Prepositions by Case
+
+
Akkusativ: durch, für, gegen, ohne, um, bis
+
Dativ: aus, außer, bei, gegenüber, mit, nach, seit, von, zu
+
Genitiv: trotz, während, wegen, statt, anstatt, außerhalb, innerhalb
+
Wechsel: an, auf, hinter, in, neben, über, unter, vor, zwischen
+
+
+ Wechsel: accusative for motion (wohin?), dative for location (wo?). +
+
+ )} + + {section === 'poss' && ( +
+
Possessive Determiners
+
+ {POSSESSIVES.map(p =>
{p.stem}- {p.en}
)} +
+
Endings follow the ein/kein pattern. euer drops to eur- when an ending is added.
+ +
+ )} + + {section === 'pronoun' && ( +
+
Personal Pronouns
+ +
+ )} + + +
+
+ ); +} + +function Frag2({ children }) { return <>{children}; } + +function SimpleChart({ table, highlightCase, highlightGender, active }) { + const genders = ['masculine','feminine','neuter','plural']; + const cases = ['nominative','accusative','dative','genitive']; + return ( + + {genders.map(g => )} + + {cases.map(c => ( + + + {genders.map(g => { + const hl = active && c === highlightCase && g === highlightGender; + return ; + })} + + ))} + +
{g}
{CL[c]}{table[g][c]}
+ ); +} + +function PossessiveChart({ highlightCase, highlightGender, active, highlightStem }) { + const genders = ['masculine','feminine','neuter','plural']; + const cases = ['nominative','accusative','dative','genitive']; + const stem = highlightStem || 'mein'; + return ( +
+ + + + {genders.map(g => )} + + + {cases.map(c => ( + + + {genders.map(g => { + const hl = active && c === highlightCase && g === highlightGender; + const display = combinePoss(stem, POSS_ENDINGS[g][c]); + return ; + })} + + ))} + +
{g}
{CL[c]}{display}
+
Showing forms for {stem}-. Same endings apply to all possessive bases.
+
+ ); +} + +function PronounsChartCombined({ highlightCase, highlightKey, active }) { + const cases = ['nominative','accusative','dative']; + const labels = { nominative:'Nom', accusative:'Acc', dative:'Dat' }; + const keys = { nominative:'nom', accusative:'acc', dative:'dat' }; + return ( + + + + + {cases.map(c => )} + + + {PRONOUNS.map((p, idx) => { + const sep = idx === 6; + return ( + + + + {cases.map(c => { + const hl = active && c === highlightCase && p.key === highlightKey; + return ; + })} + + ); + })} + +
englishperson{labels[c]}
{p.en}{p.tag}{p[keys[c]]}
+ ); +} + +// ═══════════════════════════════════════════ +// STYLES +// ═══════════════════════════════════════════ + +const S = { + wrap: { background:'#f7f7f7', minHeight:'100vh', color:'#1a1a1a', fontFamily:'"DM Sans",system-ui,sans-serif' }, + inner: { maxWidth:520, margin:'0 auto', padding:'16px 20px 100px' }, + + hdr: { display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 0 8px', position:'sticky', top:0, background:'#f7f7f7', zIndex:10 }, + logo: { fontSize:17, fontWeight:700, color:'#1a1a1a' }, + linkBtn: { background:'none', border:'none', color:'#999', fontSize:13, cursor:'pointer', padding:0, fontWeight:500, fontFamily:'inherit' }, + stat: { fontSize:13, color:'#999', fontWeight:500 }, + + tabs: { display:'flex', gap:0, borderBottom:'1px solid #eee', marginTop:6, marginBottom:12 }, + tab: { flex:1, background:'none', border:'none', padding:'12px 8px', cursor:'pointer', color:'#999', fontSize:13, fontWeight:600, fontFamily:'inherit', borderBottom:'2px solid transparent', marginBottom:-1, transition:'all 0.15s' }, + tabActive: { color:'#1a1a1a', borderBottomColor:GREEN }, + + lvRow: { display:'flex', alignItems:'center', gap:14, padding:'13px 16px', marginBottom:6, background:'#fff', borderRadius:10, border:'1px solid #eee' }, + lvName: { fontSize:14, fontWeight:700, color:'#1a1a1a' }, + lvDesc: { fontSize:12, color:'#999', marginTop:1 }, + lvPct: { fontSize:14, fontWeight:700 }, + chartLinkBtn: { width:'100%', padding:12, marginTop:20, background:'#fff', border:'1px solid #eee', borderRadius:10, color:'#888', fontSize:13, cursor:'pointer', fontFamily:'inherit' }, + + sentence: { fontSize:26, fontWeight:600, color:'#1a1a1a', lineHeight:1.5, margin:'0 0 10px', letterSpacing:'-0.3px' }, + blank: { display:'inline-block', minWidth:80, borderBottom:'2px solid', textAlign:'center', padding:'0 6px', margin:'0 2px', fontWeight:700, transition:'all 0.15s' }, + translation: { fontSize:14, color:'#999', marginBottom:10 }, + + keyInfo: { fontSize:16, color:'#333', fontWeight:600, marginBottom:4 }, + keyInfoMute: { color:'#aaa', fontWeight:400 }, + secondaryContext: { fontSize:12, color:'#aaa', marginBottom:16, lineHeight:1.5 }, + + inputWrap: { display:'flex', flexDirection:'column', alignItems:'center', gap:6 }, + inputRow: { display:'flex', gap:8, alignItems:'center' }, + input: { width:170, padding:'12px 14px', borderRadius:8, border:'1.5px solid #ddd', background:'#fff', color:'#1a1a1a', fontSize:18, fontWeight:600, outline:'none', textAlign:'center', fontFamily:'inherit' }, + checkBtn: { padding:'12px 20px', borderRadius:8, background:GREEN, color:'#fff', border:'none', fontSize:14, fontWeight:700, cursor:'pointer', fontFamily:'inherit' }, + hintText: { fontSize:11, color:'#bbb' }, + kbd: { background:'#f0f0f0', padding:'1px 5px', borderRadius:3, fontSize:10, color:'#888', border:'1px solid #e0e0e0' }, + kbdSmall: { background:'#f0f0f0', padding:'1px 4px', borderRadius:3, fontSize:9, color:'#888', border:'1px solid #e0e0e0' }, + + articleGrid: { display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, maxWidth:320, margin:'12px auto 0' }, + articleBtn: { padding:'18px 16px', borderRadius:10, border:'1.5px solid #ddd', background:'#fff', color:'#1a1a1a', cursor:'pointer', fontFamily:'inherit', textAlign:'center', transition:'all 0.12s' }, + + choiceBtn: { flex:'1 1 auto', minWidth:90, padding:'14px 16px', borderRadius:10, border:'1.5px solid', background:'#fff', color:'#1a1a1a', cursor:'pointer', fontSize:14, fontFamily:'inherit', textAlign:'center', transition:'all 0.12s' }, + + feedback: { textAlign:'left', background:'#fff', borderRadius:10, padding:'14px 18px', margin:'0 auto', maxWidth:400, border:'1px solid #eee' }, + + stripTbl: { width:'100%', borderCollapse:'collapse', marginTop:10 }, + stripH: { padding:'4px 6px', fontSize:10, fontWeight:600, textTransform:'uppercase', letterSpacing:0.3, color:'#bbb', textAlign:'center' }, + stripD: { padding:'6px', textAlign:'center', fontSize:13 }, + + dotsRow: { display:'flex', gap:5, justifyContent:'center', alignItems:'center', position:'fixed', bottom:20, left:0, right:0 }, + dotsLabel: { fontSize:11, color:'#aaa', marginLeft:10 }, + dot: { width:7, height:7, borderRadius:4, transition:'background 0.2s' }, + + nextBtn: { padding:'11px 22px', borderRadius:8, border:'none', background:GREEN, color:'#fff', fontSize:14, fontWeight:700, cursor:'pointer', fontFamily:'inherit' }, + + overlay: { position:'fixed', inset:0, background:'rgba(0,0,0,0.35)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:100, padding:16 }, + modal: { background:'#fff', borderRadius:14, padding:'22px 22px', maxWidth:520, width:'100%', maxHeight:'88vh', overflow:'auto', boxShadow:'0 8px 30px rgba(0,0,0,0.12)' }, + + chartH: { fontSize:13, fontWeight:700, color:'#1a1a1a', marginBottom:6 }, + fullChart: { width:'100%', borderCollapse:'collapse', fontSize:14 }, + fcTh: { padding:'6px 4px', fontSize:11, fontWeight:600, textAlign:'center', color:'#888', textTransform:'capitalize' }, + fcTd: { padding:'7px 4px', textAlign:'center', fontSize:14, borderBottom:'1px solid #f3f3f3' }, +}; diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..3d9da8a --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..9ffcc67 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +})