Compare commits
2 Commits
35e632e9e0
...
0acece98dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0acece98dc | ||
|
|
6b2d0024ed |
1
.env
1
.env
@@ -1,2 +1,3 @@
|
||||
VITE_SUPABASE_URL=https://vzudjibddwcvfdnyvhhs.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZ6dWRqaWJkZHdjdmZkbnl2aGhzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjUwMjk4MDksImV4cCI6MjA4MDYwNTgwOX0.wE8R1mpd2FY6m6d5-Hf3hlCG8OfeCkra6SjUvbt1mD0
|
||||
SUPABASE_DB_PASSWORD=xDsHrwqC9VpifPTU
|
||||
@@ -1,6 +1,10 @@
|
||||
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_DB_URL=postgresql://postgres:password@db.your-project.supabase.co:5432/postgres
|
||||
|
||||
# Für Migrations: Datenbankpasswort aus Supabase → Project Settings → Database → Database password
|
||||
SUPABASE_DB_PASSWORD=your-db-password
|
||||
# Alternativ: volle Postgres-URL (überschreibt SUPABASE_DB_PASSWORD)
|
||||
# SUPABASE_DB_URL=postgresql://postgres:password@db.your-project.supabase.co:5432/postgres
|
||||
RESEND_API_KEY=your-resend-api-key
|
||||
REMINDER_FROM_EMAIL=PAT Stats <noreply@example.com>
|
||||
REMINDER_APP_URL=https://your-app.example.com
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
Offen
|
||||
Rangliste
|
||||
|
||||
code splitting
|
||||
Admin Oberfläche (anzahl user mit Mail Adressen/ anelegten Tests / anbindung von Werbung Links und rechts von den Test Übersichten
|
||||
2fa
|
||||
|
||||
@@ -12,7 +10,7 @@ Final Speichern do
|
||||
Max Punkte bei Bewertung PAT1
|
||||
User Profil mit Land und Sprach Auswahl
|
||||
Übersetzen in gängigge Sprachen Englisch Italienisch Spanisch Portugisisch
|
||||
|
||||
code splitting
|
||||
|
||||
Termninal
|
||||
vercel deploy
|
||||
|
||||
285
package-lock.json
generated
285
package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"supabase": "^2.83.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.4.2",
|
||||
"vitest": "^1.6.1"
|
||||
@@ -725,6 +726,19 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/schemas": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||
@@ -1482,6 +1496,16 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/all": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/all/-/all-0.0.0.tgz",
|
||||
@@ -1597,6 +1621,23 @@
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bin-links": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.0.tgz",
|
||||
"integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cmd-shim": "^8.0.0",
|
||||
"npm-normalize-package-bin": "^5.0.0",
|
||||
"proc-log": "^6.0.0",
|
||||
"read-cmd-shim": "^6.0.0",
|
||||
"write-file-atomic": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -1801,6 +1842,26 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cmd-shim": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz",
|
||||
"integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
@@ -1909,6 +1970,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -2115,6 +2186,30 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
@@ -2134,6 +2229,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
@@ -2255,6 +2363,20 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
@@ -2562,6 +2684,29 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
|
||||
@@ -2620,6 +2765,46 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
@@ -2647,6 +2832,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-normalize-package-bin": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz",
|
||||
"integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
@@ -3140,6 +3335,16 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
|
||||
"integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -3223,6 +3428,16 @@
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cmd-shim": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz",
|
||||
"integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -3524,6 +3739,26 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
"version": "2.83.0",
|
||||
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.83.0.tgz",
|
||||
"integrity": "sha512-80j5YeYMkJqVc5gZGySMxiUJSUcwES6mNqi1nCa9q40qnPftff+LevcdZGLct4xtpaefkTtgmZArPSdq+H2RZQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bin-links": "^6.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"tar": "7.5.11"
|
||||
},
|
||||
"bin": {
|
||||
"supabase": "bin/supabase"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
@@ -3585,6 +3820,33 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
@@ -3939,6 +4201,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -3990,6 +4262,19 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz",
|
||||
"integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"migrate": "node scripts/applyMigrations.js",
|
||||
"migrate": "node --env-file=.env scripts/applyMigrations.js",
|
||||
"migrate:status": "node --env-file=.env scripts/applyMigrations.js --status",
|
||||
"migrate:rollback": "node --env-file=.env scripts/applyMigrations.js --rollback",
|
||||
"send-finalization-reminders": "node scripts/sendFinalizationReminders.js",
|
||||
"test": "vitest run"
|
||||
},
|
||||
@@ -29,6 +31,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"supabase": "^2.83.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.4.2",
|
||||
"vitest": "^1.6.1"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Lightweight migration runner for Supabase.
|
||||
* Reads SQL files in supabase/migrations (sorted) and applies any that have not been recorded.
|
||||
* Similar to Laravel's `artisan migrate`.
|
||||
*
|
||||
* Env:
|
||||
* - SUPABASE_DB_URL or DATABASE_URL: postgres connection string (service role / connection string)
|
||||
* Usage:
|
||||
* npm run migrate - Apply all pending migrations
|
||||
* npm run migrate:status - Show status of all migrations
|
||||
* npm run migrate:rollback - Rollback the last applied migration
|
||||
*
|
||||
* Env (loaded from .env automatically via --env-file):
|
||||
* SUPABASE_DB_URL or DATABASE_URL postgres connection string
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -15,15 +20,43 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const migrationsDir = path.resolve(__dirname, '..', 'supabase', 'migrations');
|
||||
const connectionString = process.env.SUPABASE_DB_URL || process.env.DATABASE_URL;
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0]; // --status | --rollback | undefined (= migrate)
|
||||
|
||||
function buildConnectionString() {
|
||||
// Direct override takes precedence
|
||||
if (process.env.SUPABASE_DB_URL) return process.env.SUPABASE_DB_URL;
|
||||
if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
|
||||
|
||||
// Derive from VITE_SUPABASE_URL + SUPABASE_DB_PASSWORD
|
||||
const supabaseUrl = process.env.VITE_SUPABASE_URL;
|
||||
const dbPassword = process.env.SUPABASE_DB_PASSWORD;
|
||||
|
||||
if (supabaseUrl && dbPassword) {
|
||||
// Extract project ref from https://[ref].supabase.co
|
||||
const match = supabaseUrl.match(/https:\/\/([^.]+)\.supabase\.co/);
|
||||
if (!match) {
|
||||
console.error('❌ VITE_SUPABASE_URL hat ein unerwartetes Format.');
|
||||
process.exit(1);
|
||||
}
|
||||
const ref = match[1];
|
||||
return `postgresql://postgres:${encodeURIComponent(dbPassword)}@db.${ref}.supabase.co:5432/postgres`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const connectionString = buildConnectionString();
|
||||
|
||||
if (!connectionString) {
|
||||
console.error('Missing SUPABASE_DB_URL (or DATABASE_URL) environment variable. Aborting migrations.');
|
||||
console.error('❌ Keine Datenbankverbindung konfiguriert.');
|
||||
console.error(' Füge SUPABASE_DB_PASSWORD zu deiner .env hinzu (Passwort aus Supabase → Settings → Database).');
|
||||
console.error(' Alternativ: SUPABASE_DB_URL=postgresql://... direkt setzen.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
console.error(`Migrations directory not found: ${migrationsDir}`);
|
||||
console.error(`❌ Migrations directory not found: ${migrationsDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -32,38 +65,127 @@ const pool = new Pool({ connectionString });
|
||||
async function ensureMigrationsTable(client) {
|
||||
await client.query(`
|
||||
create table if not exists public.schema_migrations (
|
||||
id serial primary key,
|
||||
filename text not null unique,
|
||||
id serial primary key,
|
||||
filename text not null unique,
|
||||
applied_at timestamptz not null default now()
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
async function getApplied(client) {
|
||||
const { rows } = await client.query('select filename from public.schema_migrations');
|
||||
return new Set(rows.map((r) => r.filename));
|
||||
const { rows } = await client.query(
|
||||
'select filename, applied_at from public.schema_migrations order by filename'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
function listMigrations() {
|
||||
function listMigrationFiles() {
|
||||
return fs
|
||||
.readdirSync(migrationsDir)
|
||||
.filter((file) => file.endsWith('.sql'))
|
||||
.filter((f) => f.endsWith('.sql'))
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function applyMigration(client, filename) {
|
||||
const filePath = path.join(migrationsDir, filename);
|
||||
const sql = fs.readFileSync(filePath, 'utf8');
|
||||
console.log(`Applying migration: ${filename}`);
|
||||
console.log(` → Applying: ${filename}`);
|
||||
await client.query('begin');
|
||||
try {
|
||||
await client.query(sql);
|
||||
await client.query('insert into public.schema_migrations (filename) values ($1)', [filename]);
|
||||
await client.query(
|
||||
'insert into public.schema_migrations (filename) values ($1)',
|
||||
[filename]
|
||||
);
|
||||
await client.query('commit');
|
||||
console.log(`✓ Applied ${filename}`);
|
||||
console.log(` ✓ Done: ${filename}`);
|
||||
} catch (err) {
|
||||
await client.query('rollback');
|
||||
console.error(`✗ Failed ${filename}:`, err.message);
|
||||
console.error(` ✗ Failed: ${filename}\n ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdMigrate(client) {
|
||||
await ensureMigrationsTable(client);
|
||||
const applied = new Set((await getApplied(client)).map((r) => r.filename));
|
||||
const pending = listMigrationFiles().filter((f) => !applied.has(f));
|
||||
|
||||
if (pending.length === 0) {
|
||||
console.log('✔ Nothing to migrate – all migrations are up to date.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\nRunning ${pending.length} migration(s)...\n`);
|
||||
for (const file of pending) {
|
||||
await applyMigration(client, file);
|
||||
}
|
||||
console.log(`\n✔ ${pending.length} migration(s) applied successfully.`);
|
||||
}
|
||||
|
||||
async function cmdStatus(client) {
|
||||
await ensureMigrationsTable(client);
|
||||
const appliedRows = await getApplied(client);
|
||||
const appliedMap = new Map(appliedRows.map((r) => [r.filename, r.applied_at]));
|
||||
const files = listMigrationFiles();
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No migration files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n Status Migration');
|
||||
console.log(' ───────── ' + '─'.repeat(50));
|
||||
for (const file of files) {
|
||||
if (appliedMap.has(file)) {
|
||||
const ts = new Date(appliedMap.get(file)).toLocaleString('de-AT');
|
||||
console.log(` ✓ applied ${file} (${ts})`);
|
||||
} else {
|
||||
console.log(` ○ pending ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
const pendingCount = files.filter((f) => !appliedMap.has(f)).length;
|
||||
console.log(`\n ${appliedMap.size} applied, ${pendingCount} pending.\n`);
|
||||
}
|
||||
|
||||
async function cmdRollback(client) {
|
||||
await ensureMigrationsTable(client);
|
||||
const applied = await getApplied(client);
|
||||
|
||||
if (applied.length === 0) {
|
||||
console.log('Nothing to rollback – no migrations have been applied.');
|
||||
return;
|
||||
}
|
||||
|
||||
const last = applied[applied.length - 1];
|
||||
console.log(`\nRolling back: ${last.filename}`);
|
||||
|
||||
// Check for a corresponding .down.sql file
|
||||
const downFile = path.join(
|
||||
migrationsDir,
|
||||
last.filename.replace('.sql', '.down.sql')
|
||||
);
|
||||
|
||||
await client.query('begin');
|
||||
try {
|
||||
if (fs.existsSync(downFile)) {
|
||||
const sql = fs.readFileSync(downFile, 'utf8');
|
||||
await client.query(sql);
|
||||
console.log(` ✓ Ran down-migration: ${path.basename(downFile)}`);
|
||||
} else {
|
||||
console.warn(` ⚠ No down-migration file found (${path.basename(downFile)})`);
|
||||
console.warn(' Only removing the migration record, schema changes are NOT reverted.');
|
||||
}
|
||||
await client.query(
|
||||
'delete from public.schema_migrations where filename = $1',
|
||||
[last.filename]
|
||||
);
|
||||
await client.query('commit');
|
||||
console.log(`✔ Rolled back: ${last.filename}\n`);
|
||||
} catch (err) {
|
||||
await client.query('rollback');
|
||||
console.error(`✗ Rollback failed: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -71,20 +193,13 @@ async function applyMigration(client, filename) {
|
||||
async function run() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await ensureMigrationsTable(client);
|
||||
const applied = await getApplied(client);
|
||||
const migrations = listMigrations();
|
||||
|
||||
const pending = migrations.filter((m) => !applied.has(m));
|
||||
if (pending.length === 0) {
|
||||
console.log('No pending migrations.');
|
||||
return;
|
||||
if (command === '--status') {
|
||||
await cmdStatus(client);
|
||||
} else if (command === '--rollback') {
|
||||
await cmdRollback(client);
|
||||
} else {
|
||||
await cmdMigrate(client);
|
||||
}
|
||||
|
||||
for (const migration of pending) {
|
||||
await applyMigration(client, migration);
|
||||
}
|
||||
console.log('All pending migrations applied.');
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
@@ -92,6 +207,6 @@ async function run() {
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error('Migration run failed:', err);
|
||||
console.error('\n❌ Migration runner failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
133
src/lib/assessmentShareService.test.js
Normal file
133
src/lib/assessmentShareService.test.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const importService = async (supabaseMock) => {
|
||||
vi.resetModules();
|
||||
vi.doMock('./supabaseClient', () => ({
|
||||
supabase: supabaseMock
|
||||
}));
|
||||
return import('./assessmentShareService');
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
vi.doUnmock('./supabaseClient');
|
||||
delete globalThis.window;
|
||||
});
|
||||
|
||||
describe('assessmentShareService', () => {
|
||||
it('creates a share link with the current browser URL', async () => {
|
||||
globalThis.window = {
|
||||
location: {
|
||||
origin: 'https://pat.example.com',
|
||||
pathname: '/manager'
|
||||
}
|
||||
};
|
||||
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
data: { share_token: 'token-123' },
|
||||
error: null
|
||||
});
|
||||
|
||||
const { createAssessmentShareLink } = await importService({ rpc });
|
||||
const result = await createAssessmentShareLink({ id: 'assessment-1' });
|
||||
|
||||
expect(rpc).toHaveBeenCalledWith('create_or_get_assessment_share', {
|
||||
p_assessment_id: 'assessment-1'
|
||||
});
|
||||
expect(result).toEqual({
|
||||
token: 'token-123',
|
||||
url: 'https://pat.example.com/manager?share=token-123'
|
||||
});
|
||||
});
|
||||
|
||||
it('maps profile visibility RPC errors to a user-facing German message', async () => {
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
data: null,
|
||||
error: { message: 'Tests are hidden in the profile' }
|
||||
});
|
||||
|
||||
const { createAssessmentShareLink } = await importService({ rpc });
|
||||
|
||||
await expect(createAssessmentShareLink({ id: 'assessment-1' })).rejects.toThrow(
|
||||
'Deine Tests sind im Profil aktuell nicht sichtbar.'
|
||||
);
|
||||
});
|
||||
|
||||
it('lists existing shares by assessment id', async () => {
|
||||
const inMock = vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{ assessment_id: 'a1', share_token: 'share-a1' },
|
||||
{ assessment_id: 'a2', share_token: 'share-a2' }
|
||||
],
|
||||
error: null
|
||||
});
|
||||
const selectMock = vi.fn(() => ({ in: inMock }));
|
||||
const fromMock = vi.fn(() => ({ select: selectMock }));
|
||||
|
||||
const { listAssessmentShares } = await importService({ from: fromMock });
|
||||
const result = await listAssessmentShares(['a1', 'a2']);
|
||||
|
||||
expect(fromMock).toHaveBeenCalledWith('assessment_shares');
|
||||
expect(selectMock).toHaveBeenCalledWith('assessment_id, share_token');
|
||||
expect(inMock).toHaveBeenCalledWith('assessment_id', ['a1', 'a2']);
|
||||
expect(result).toEqual({
|
||||
a1: 'share-a1',
|
||||
a2: 'share-a2'
|
||||
});
|
||||
});
|
||||
|
||||
it('hydrates shared assessments and recalculates derived exercise values', async () => {
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'shared-1',
|
||||
pat_type: 'PAT 2',
|
||||
datum: '2026-03-28',
|
||||
name: 'Max Mustermann',
|
||||
exercises: [
|
||||
{
|
||||
name: 'Gerade Einsteiger',
|
||||
soll: 6,
|
||||
faktor: 10,
|
||||
values: ['2', '4', '']
|
||||
},
|
||||
{
|
||||
name: 'Split Drill',
|
||||
soll: 4,
|
||||
faktor: 5,
|
||||
subA: ['1', '3'],
|
||||
subB: ['5', ''],
|
||||
subLabels: ['A', 'B']
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
error: null
|
||||
});
|
||||
|
||||
const { getSharedAssessment } = await importService({ rpc });
|
||||
const result = await getSharedAssessment('share-token');
|
||||
|
||||
expect(rpc).toHaveBeenCalledWith('get_shared_assessment', {
|
||||
p_share_token: 'share-token'
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
id: 'shared-1',
|
||||
patType: 'PAT 2',
|
||||
datum: '2026-03-28',
|
||||
name: 'Max Mustermann'
|
||||
});
|
||||
expect(result.exercises[0]).toMatchObject({
|
||||
name: 'Gerade Einsteiger',
|
||||
durchschnitt: 3,
|
||||
points: 30
|
||||
});
|
||||
expect(result.exercises[1]).toMatchObject({
|
||||
name: 'Split Drill',
|
||||
durchschnitt: 3,
|
||||
points: 15,
|
||||
subLabels: ['A', 'B']
|
||||
});
|
||||
});
|
||||
});
|
||||
171
src/lib/trainingPlanService.test.js
Normal file
171
src/lib/trainingPlanService.test.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const importService = async (supabaseMock) => {
|
||||
vi.resetModules();
|
||||
vi.doMock('./supabaseClient', () => ({
|
||||
supabase: supabaseMock
|
||||
}));
|
||||
return import('./trainingPlanService');
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
vi.doUnmock('./supabaseClient');
|
||||
});
|
||||
|
||||
describe('trainingPlanService', () => {
|
||||
it('maps sessions to the RPC shape and normalizes task defaults', async () => {
|
||||
const { mapSessionToRpc } = await importService(null);
|
||||
|
||||
const result = mapSessionToRpc({
|
||||
week_no: '2',
|
||||
session_no: '3',
|
||||
main_exercise: 'Break-Aufbau',
|
||||
secondary_exercises: ['Safety', 'Pattern Play'],
|
||||
tasks: [
|
||||
{
|
||||
type: 'main',
|
||||
title: 'Hauptblock',
|
||||
durationMin: '15',
|
||||
repetitions: 12,
|
||||
intensity: 'hoch',
|
||||
instructions: 'Volle Konzentration'
|
||||
},
|
||||
{
|
||||
durationMin: '',
|
||||
instructions: null
|
||||
}
|
||||
],
|
||||
state: 'done',
|
||||
notes: 'Saubere Linie'
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
weekNo: 2,
|
||||
sessionNo: 3,
|
||||
mainExercise: 'Break-Aufbau',
|
||||
secondaryExercises: ['Safety', 'Pattern Play'],
|
||||
tasks: [
|
||||
{
|
||||
type: 'main',
|
||||
title: 'Hauptblock',
|
||||
durationMin: 15,
|
||||
repetitions: 12,
|
||||
intensity: 'hoch',
|
||||
instructions: 'Volle Konzentration'
|
||||
},
|
||||
{
|
||||
type: 'task',
|
||||
title: '',
|
||||
durationMin: 0,
|
||||
repetitions: null,
|
||||
intensity: null,
|
||||
instructions: ''
|
||||
}
|
||||
],
|
||||
state: 'done',
|
||||
notes: 'Saubere Linie'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error result when supabase is not configured', async () => {
|
||||
const { saveTrainingPlanWithSessions } = await importService(null);
|
||||
|
||||
const result = await saveTrainingPlanWithSessions({
|
||||
patType: 'PAT 1',
|
||||
analysisSnapshot: {},
|
||||
durationWeeks: 4,
|
||||
sessionsPerWeek: 2,
|
||||
sessions: []
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error.message).toBe('Supabase ist nicht konfiguriert.');
|
||||
});
|
||||
|
||||
it('sends a normalized RPC payload and returns the created plan id', async () => {
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
data: 'plan-123',
|
||||
error: null
|
||||
});
|
||||
|
||||
const { saveTrainingPlanWithSessions } = await importService({ rpc });
|
||||
const result = await saveTrainingPlanWithSessions({
|
||||
patType: 'PAT 3',
|
||||
analysisSnapshot: { score: 87 },
|
||||
durationWeeks: '6',
|
||||
sessionsPerWeek: '4',
|
||||
sessions: [
|
||||
{
|
||||
weekNo: '1',
|
||||
sessionNo: '2',
|
||||
mainExercise: 'Lochen lang',
|
||||
secondaryExercises: ['Positionsspiel', 'Safety'],
|
||||
tasks: [
|
||||
{
|
||||
type: 'main',
|
||||
title: 'Serie',
|
||||
durationMin: '25',
|
||||
repetitions: 20,
|
||||
intensity: 'mittel',
|
||||
instructions: 'Tempo konstant'
|
||||
}
|
||||
],
|
||||
state: 'open',
|
||||
notes: 'Tag 1'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(rpc).toHaveBeenCalledWith('create_training_plan_with_sessions', {
|
||||
p_pat_type: 'PAT 3',
|
||||
p_analysis_snapshot: { score: 87 },
|
||||
p_duration_weeks: 6,
|
||||
p_sessions_per_week: 4,
|
||||
p_sessions: [
|
||||
{
|
||||
weekNo: 1,
|
||||
sessionNo: 2,
|
||||
mainExercise: 'Lochen lang',
|
||||
secondaryExercises: ['Positionsspiel', 'Safety'],
|
||||
tasks: [
|
||||
{
|
||||
type: 'main',
|
||||
title: 'Serie',
|
||||
durationMin: 25,
|
||||
repetitions: 20,
|
||||
intensity: 'mittel',
|
||||
instructions: 'Tempo konstant'
|
||||
}
|
||||
],
|
||||
state: 'open',
|
||||
notes: 'Tag 1'
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
planId: 'plan-123'
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps RPC failures into a non-throwing error result', async () => {
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
data: null,
|
||||
error: new Error('Datenbank nicht erreichbar')
|
||||
});
|
||||
|
||||
const { saveTrainingPlanWithSessions } = await importService({ rpc });
|
||||
const result = await saveTrainingPlanWithSessions({
|
||||
patType: 'PAT 2',
|
||||
analysisSnapshot: {},
|
||||
durationWeeks: 2,
|
||||
sessionsPerWeek: 2,
|
||||
sessions: []
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error.message).toBe('Datenbank nicht erreichbar');
|
||||
});
|
||||
});
|
||||
65
src/utils/patCalculations.test.js
Normal file
65
src/utils/patCalculations.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
calculateExercise,
|
||||
calculateTotal,
|
||||
getAchievement,
|
||||
getPatTypeColor
|
||||
} from './patCalculations';
|
||||
|
||||
describe('patCalculations', () => {
|
||||
it('calculates average and points for regular exercises and ignores invalid values', () => {
|
||||
const result = calculateExercise({
|
||||
values: ['2', '4', 'x', ''],
|
||||
faktor: 10
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
durchschnitt: 3,
|
||||
points: 30
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates average and points across split exercise rows', () => {
|
||||
const result = calculateExercise({
|
||||
subA: ['1', '3'],
|
||||
subB: ['5', ''],
|
||||
faktor: 4
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
durchschnitt: 3,
|
||||
points: 12
|
||||
});
|
||||
});
|
||||
|
||||
it('sums points across exercises and treats missing values as zero', () => {
|
||||
const total = calculateTotal([
|
||||
{ points: 120 },
|
||||
{ points: 0 },
|
||||
{},
|
||||
{ points: 35 }
|
||||
]);
|
||||
|
||||
expect(total).toBe(155);
|
||||
});
|
||||
|
||||
it('returns configured colors and a gray fallback for unknown PAT types', () => {
|
||||
expect(getPatTypeColor('PAT 1')).toBe('bg-blue-100 text-blue-800');
|
||||
expect(getPatTypeColor('Unbekannt')).toBe('bg-gray-100 text-gray-800');
|
||||
});
|
||||
|
||||
it('returns the correct achievement by threshold and falls back to PAT Start rules', () => {
|
||||
expect(getAchievement('PAT 3', 850)).toMatchObject({
|
||||
name: '9. Gold',
|
||||
subtitle: 'internationaler Topsportler'
|
||||
});
|
||||
|
||||
expect(getAchievement('PAT 3', 549)).toMatchObject({
|
||||
name: 'Nicht bestanden'
|
||||
});
|
||||
|
||||
expect(getAchievement('Nicht definiert', 800)).toMatchObject({
|
||||
name: '1. Weiss'
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user