Add deck generation and study flow improvements
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m22s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m22s
This commit is contained in:
parent
d6f3502cb1
commit
7b90409f2e
36 changed files with 8683 additions and 31 deletions
840
package-lock.json
generated
840
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
|||
"react-dom": "19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"ts-fsrs": "^5.4.1",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -33,7 +34,8 @@
|
|||
"eslint-config-next": "16.2.9",
|
||||
"prisma": "^7.8.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
|
@ -1349,6 +1351,16 @@
|
|||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.139.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
|
|
@ -1737,6 +1749,293 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
||||
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
||||
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
||||
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
||||
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
||||
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
||||
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.11.1",
|
||||
"@emnapi/runtime": "1.11.1",
|
||||
"@napi-rs/wasm-runtime": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
||||
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
|
|
@ -2042,6 +2341,17 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||
|
|
@ -2051,6 +2361,13 @@
|
|||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
|
|
@ -2110,6 +2427,7 @@
|
|||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
|
|
@ -2790,6 +3108,119 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
|
||||
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.1.10",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
|
||||
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
|
||||
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.10",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
|
||||
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
|
||||
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
|
||||
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.10",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
|
|
@ -3040,6 +3471,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types-flow": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||
|
|
@ -3395,6 +3836,16 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
|
@ -4061,6 +4512,13 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
|
||||
"integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
|
|
@ -4555,6 +5013,16 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
|
|
@ -4574,6 +5042,16 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz",
|
||||
|
|
@ -4797,6 +5275,21 @@
|
|||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -7675,6 +8168,20 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
|
||||
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||
|
|
@ -8407,6 +8914,40 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.139.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
|
@ -8739,6 +9280,13 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
|
|
@ -8833,6 +9381,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
|
|
@ -9130,6 +9685,23 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
|
|
@ -9179,6 +9751,16 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
|
@ -9225,6 +9807,15 @@
|
|||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-fsrs": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-fsrs/-/ts-fsrs-5.4.1.tgz",
|
||||
"integrity": "sha512-mOp9+oexJexBTkwjg/jQI1aSUQRLIAvbimeKHLSmVdNJPwObugFNKmZkoggH5d6kZ0uaWLboP1Al1DnXAfIb9w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
|
|
@ -9646,6 +10237,236 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.16",
|
||||
"rolldown": "~1.1.4",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.3.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/postcss": {
|
||||
"version": "8.5.19",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
||||
"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/vitest": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
|
||||
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.10",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
"@vitest/pretty-format": "4.1.10",
|
||||
"@vitest/runner": "4.1.10",
|
||||
"@vitest/snapshot": "4.1.10",
|
||||
"@vitest/spy": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.1.0",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"@vitest/browser-preview": "4.1.10",
|
||||
"@vitest/browser-webdriverio": "4.1.10",
|
||||
"@vitest/coverage-istanbul": "4.1.10",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"@vitest/ui": "4.1.10",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-istanbul": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/std-env": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
@ -9750,6 +10571,23 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
"react-dom": "19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"ts-fsrs": "^5.4.1",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -34,6 +36,7 @@
|
|||
"eslint-config-next": "16.2.9",
|
||||
"prisma": "^7.8.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "SpacedRepetitionSet" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"classId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"newCardsPerDay" INTEGER NOT NULL DEFAULT 30,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "SpacedRepetitionSet_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SpacedRepetitionSetDeck" (
|
||||
"setId" TEXT NOT NULL,
|
||||
"deckId" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"addedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("setId", "deckId"),
|
||||
CONSTRAINT "SpacedRepetitionSetDeck_setId_fkey" FOREIGN KEY ("setId") REFERENCES "SpacedRepetitionSet" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "SpacedRepetitionSetDeck_deckId_fkey" FOREIGN KEY ("deckId") REFERENCES "Deck" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SpacedRepetitionCardState" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"setId" TEXT NOT NULL,
|
||||
"flashcardId" TEXT NOT NULL,
|
||||
"due" DATETIME NOT NULL,
|
||||
"stability" REAL NOT NULL,
|
||||
"difficulty" REAL NOT NULL,
|
||||
"elapsedDays" INTEGER NOT NULL,
|
||||
"scheduledDays" INTEGER NOT NULL,
|
||||
"learningSteps" INTEGER NOT NULL DEFAULT 0,
|
||||
"reps" INTEGER NOT NULL,
|
||||
"lapses" INTEGER NOT NULL,
|
||||
"state" INTEGER NOT NULL,
|
||||
"firstReviewedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastReview" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "SpacedRepetitionCardState_setId_fkey" FOREIGN KEY ("setId") REFERENCES "SpacedRepetitionSet" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "SpacedRepetitionCardState_flashcardId_fkey" FOREIGN KEY ("flashcardId") REFERENCES "Flashcard" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SpacedRepetitionSet_classId_sortOrder_idx" ON "SpacedRepetitionSet"("classId", "sortOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SpacedRepetitionSetDeck_deckId_idx" ON "SpacedRepetitionSetDeck"("deckId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SpacedRepetitionCardState_setId_due_idx" ON "SpacedRepetitionCardState"("setId", "due");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SpacedRepetitionCardState_setId_firstReviewedAt_idx" ON "SpacedRepetitionCardState"("setId", "firstReviewedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SpacedRepetitionCardState_flashcardId_idx" ON "SpacedRepetitionCardState"("flashcardId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SpacedRepetitionCardState_setId_flashcardId_key" ON "SpacedRepetitionCardState"("setId", "flashcardId");
|
||||
|
|
@ -17,6 +17,7 @@ model Class {
|
|||
decks Deck[]
|
||||
quizSets QuizSet[]
|
||||
materialGroups MaterialGroup[]
|
||||
spacedRepetitionSets SpacedRepetitionSet[]
|
||||
}
|
||||
|
||||
model Deck {
|
||||
|
|
@ -33,6 +34,7 @@ model Deck {
|
|||
cards Flashcard[]
|
||||
shareLink ShareLink?
|
||||
progress StudyProgress[]
|
||||
spacedRepetitionMemberships SpacedRepetitionSetDeck[]
|
||||
}
|
||||
|
||||
model Flashcard {
|
||||
|
|
@ -43,6 +45,7 @@ model Flashcard {
|
|||
sortOrder Int @default(0)
|
||||
|
||||
deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)
|
||||
spacedRepetitionStates SpacedRepetitionCardState[]
|
||||
}
|
||||
|
||||
model QuizSet {
|
||||
|
|
@ -162,3 +165,60 @@ model MaterialGroup {
|
|||
quizSets QuizSet[]
|
||||
shareLink ShareLink?
|
||||
}
|
||||
|
||||
model SpacedRepetitionSet {
|
||||
id String @id @default(uuid())
|
||||
classId String
|
||||
name String
|
||||
description String?
|
||||
newCardsPerDay Int @default(30)
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||||
decks SpacedRepetitionSetDeck[]
|
||||
cardStates SpacedRepetitionCardState[]
|
||||
|
||||
@@index([classId, sortOrder])
|
||||
}
|
||||
|
||||
model SpacedRepetitionSetDeck {
|
||||
setId String
|
||||
deckId String
|
||||
sortOrder Int @default(0)
|
||||
addedAt DateTime @default(now())
|
||||
|
||||
set SpacedRepetitionSet @relation(fields: [setId], references: [id], onDelete: Cascade)
|
||||
deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([setId, deckId])
|
||||
@@index([deckId])
|
||||
}
|
||||
|
||||
model SpacedRepetitionCardState {
|
||||
id String @id @default(uuid())
|
||||
setId String
|
||||
flashcardId String
|
||||
due DateTime
|
||||
stability Float
|
||||
difficulty Float
|
||||
elapsedDays Int
|
||||
scheduledDays Int
|
||||
learningSteps Int @default(0)
|
||||
reps Int
|
||||
lapses Int
|
||||
state Int
|
||||
firstReviewedAt DateTime @default(now())
|
||||
lastReview DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
set SpacedRepetitionSet @relation(fields: [setId], references: [id], onDelete: Cascade)
|
||||
flashcard Flashcard @relation(fields: [flashcardId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([setId, flashcardId])
|
||||
@@index([setId, due])
|
||||
@@index([setId, firstReviewedAt])
|
||||
@@index([flashcardId])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { SpacedRepetitionViewer } from "@/components/spaced-repetition/SpacedRepetitionViewer";
|
||||
|
||||
export default function SpacedRepetitionStudyPage() {
|
||||
return <SpacedRepetitionViewer />;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { SpacedRepetitionSets } from "@/components/spaced-repetition/SpacedRepetitionSets";
|
||||
|
||||
export default function SpacedRepetitionPage() {
|
||||
return <SpacedRepetitionSets />;
|
||||
}
|
||||
|
|
@ -9,6 +9,10 @@ interface ClassItem {
|
|||
slug: string;
|
||||
name: string;
|
||||
_count: { decks: number; quizSets: number };
|
||||
spacedRepetitionDueCards: number;
|
||||
spacedRepetitionLearningCards: number;
|
||||
spacedRepetitionNewCards: number;
|
||||
spacedRepetitionReadyCards: number;
|
||||
}
|
||||
|
||||
const accents = ["#4f46e5", "#f97360", "#25845f", "#c27718", "#8b5cf6", "#0284c7"];
|
||||
|
|
@ -114,6 +118,14 @@ export default function HomePage() {
|
|||
<div className="mt-3 flex flex-wrap gap-2 text-xs font-semibold text-text-secondary">
|
||||
<span className="rounded-full bg-bg-surface-alt px-2.5 py-1">{cls._count.decks} {cls._count.decks === 1 ? "deck" : "decks"}</span>
|
||||
<span className="rounded-full bg-bg-surface-alt px-2.5 py-1">{cls._count.quizSets} {cls._count.quizSets === 1 ? "quiz" : "quizzes"}</span>
|
||||
{cls.spacedRepetitionReadyCards > 0 && (
|
||||
<span
|
||||
className="rounded-full bg-error-bg px-2.5 py-1 text-error"
|
||||
title={`${cls.spacedRepetitionDueCards} due, ${cls.spacedRepetitionLearningCards} learning, ${cls.spacedRepetitionNewCards} new`}
|
||||
>
|
||||
{cls.spacedRepetitionReadyCards} {cls.spacedRepetitionReadyCards === 1 ? "card" : "cards"} ready
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="absolute right-3 top-4 flex gap-1 rounded-xl border border-border-light bg-bg-surface p-1 opacity-100 shadow-sm sm:opacity-0 sm:group-hover:opacity-100">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { spacedRepetitionErrorResponse } from "@/lib/spacedRepetitionApi";
|
||||
import * as spacedRepetitionService from "@/services/spacedRepetitionService";
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
context: { params: Promise<{ id: string; deckId: string }> }
|
||||
) {
|
||||
const { id, deckId } = await context.params;
|
||||
try {
|
||||
await spacedRepetitionService.removeDeck(id, deckId);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
43
src/app/api/spaced-repetition-sets/[id]/decks/route.ts
Normal file
43
src/app/api/spaced-repetition-sets/[id]/decks/route.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
addSpacedRepetitionDeckSchema,
|
||||
reorderSpacedRepetitionDecksSchema,
|
||||
} from "@/lib/validation/spacedRepetitionSchemas";
|
||||
import { spacedRepetitionErrorResponse } from "@/lib/spacedRepetitionApi";
|
||||
import * as spacedRepetitionService from "@/services/spacedRepetitionService";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
const parsed = addSpacedRepetitionDeckSchema.safeParse(
|
||||
await request.json().catch(() => null)
|
||||
);
|
||||
if (!parsed.success) return NextResponse.json({ error: "Invalid deck" }, { status: 400 });
|
||||
try {
|
||||
return NextResponse.json(
|
||||
await spacedRepetitionService.addDeck(id, parsed.data.deckId),
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
const parsed = reorderSpacedRepetitionDecksSchema.safeParse(
|
||||
await request.json().catch(() => null)
|
||||
);
|
||||
if (!parsed.success) return NextResponse.json({ error: "Invalid deck order" }, { status: 400 });
|
||||
try {
|
||||
await spacedRepetitionService.reorderDecks(id, parsed.data.deckIds);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
22
src/app/api/spaced-repetition-sets/[id]/reviews/route.ts
Normal file
22
src/app/api/spaced-repetition-sets/[id]/reviews/route.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { reviewSpacedRepetitionCardSchema } from "@/lib/validation/spacedRepetitionSchemas";
|
||||
import { spacedRepetitionErrorResponse } from "@/lib/spacedRepetitionApi";
|
||||
import * as spacedRepetitionService from "@/services/spacedRepetitionService";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
const parsed = reviewSpacedRepetitionCardSchema.safeParse(
|
||||
await request.json().catch(() => null)
|
||||
);
|
||||
if (!parsed.success) return NextResponse.json({ error: "Invalid review" }, { status: 400 });
|
||||
try {
|
||||
return NextResponse.json(
|
||||
await spacedRepetitionService.reviewCard({ setId: id, ...parsed.data })
|
||||
);
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
45
src/app/api/spaced-repetition-sets/[id]/route.ts
Normal file
45
src/app/api/spaced-repetition-sets/[id]/route.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { updateSpacedRepetitionSetSchema } from "@/lib/validation/spacedRepetitionSchemas";
|
||||
import { spacedRepetitionErrorResponse } from "@/lib/spacedRepetitionApi";
|
||||
import * as spacedRepetitionService from "@/services/spacedRepetitionService";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
try {
|
||||
return NextResponse.json(await spacedRepetitionService.getSet(id));
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
const parsed = updateSpacedRepetitionSetSchema.safeParse(
|
||||
await request.json().catch(() => null)
|
||||
);
|
||||
if (!parsed.success) return NextResponse.json({ error: "Invalid update" }, { status: 400 });
|
||||
try {
|
||||
return NextResponse.json(await spacedRepetitionService.updateSet(id, parsed.data));
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
try {
|
||||
await spacedRepetitionService.deleteSet(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
15
src/app/api/spaced-repetition-sets/[id]/study/route.ts
Normal file
15
src/app/api/spaced-repetition-sets/[id]/study/route.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { spacedRepetitionErrorResponse } from "@/lib/spacedRepetitionApi";
|
||||
import * as spacedRepetitionService from "@/services/spacedRepetitionService";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params;
|
||||
try {
|
||||
return NextResponse.json(await spacedRepetitionService.getStudyState(id));
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
30
src/app/api/spaced-repetition-sets/route.ts
Normal file
30
src/app/api/spaced-repetition-sets/route.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createSpacedRepetitionSetSchema } from "@/lib/validation/spacedRepetitionSchemas";
|
||||
import { spacedRepetitionErrorResponse } from "@/lib/spacedRepetitionApi";
|
||||
import * as spacedRepetitionService from "@/services/spacedRepetitionService";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const classId = new URL(request.url).searchParams.get("classId");
|
||||
if (!classId) return NextResponse.json({ error: "classId is required" }, { status: 400 });
|
||||
try {
|
||||
return NextResponse.json(await spacedRepetitionService.listSetsByClass(classId));
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const parsed = createSpacedRepetitionSetSchema.safeParse(
|
||||
await request.json().catch(() => null)
|
||||
);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid repetition set" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
return NextResponse.json(await spacedRepetitionService.createSet(parsed.data), {
|
||||
status: 201,
|
||||
});
|
||||
} catch (error) {
|
||||
return spacedRepetitionErrorResponse(error);
|
||||
}
|
||||
}
|
||||
365
src/components/spaced-repetition/SpacedRepetitionSets.tsx
Normal file
365
src/components/spaced-repetition/SpacedRepetitionSets.tsx
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
"use client";
|
||||
|
||||
/* eslint-disable react-hooks/refs -- @dnd-kit intentionally returns render-time ref callbacks and drag state. */
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
pointerWithin,
|
||||
useDraggable,
|
||||
useDndContext,
|
||||
useDroppable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
interface DeckSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
sortOrder?: number;
|
||||
_count: { cards: number };
|
||||
}
|
||||
|
||||
interface SetSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
newCardsPerDay: number;
|
||||
decks: DeckSummary[];
|
||||
stats: {
|
||||
totalCards: number;
|
||||
reviewedCards: number;
|
||||
unseenCards: number;
|
||||
dueCards: number;
|
||||
learningAheadCards: number;
|
||||
newCardsAvailable: number;
|
||||
availableCards: number;
|
||||
nextDue: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface EditState {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
newCardsPerDay: number;
|
||||
}
|
||||
|
||||
function notifyStudyAvailabilityChanged() {
|
||||
window.dispatchEvent(new Event("spaced-repetition-updated"));
|
||||
}
|
||||
|
||||
function SourceDeck({ deck }: { deck: DeckSummary }) {
|
||||
const drag = useDraggable({
|
||||
id: `source:${deck.id}`,
|
||||
data: { deckId: deck.id, sourceSetId: null },
|
||||
});
|
||||
return (
|
||||
<div
|
||||
ref={drag.setNodeRef}
|
||||
style={{ transform: CSS.Translate.toString(drag.transform), opacity: drag.isDragging ? 0.45 : 1 }}
|
||||
className="flex min-w-0 items-center gap-3 rounded-xl border border-border-light bg-bg-surface p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
{...drag.attributes}
|
||||
{...drag.listeners}
|
||||
className="grid h-10 w-10 shrink-0 place-items-center rounded-lg text-text-muted hover:bg-bg-surface-alt hover:text-text-heading"
|
||||
aria-label={`Drag ${deck.name} into a repetition set`}
|
||||
>
|
||||
<span aria-hidden>☷</span>
|
||||
</button>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-bold text-text-heading">{deck.name}</p>
|
||||
<p className="text-xs text-text-muted">{deck._count.cards} cards</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MembershipDeck({ deck, setId, onRemove }: {
|
||||
deck: DeckSummary;
|
||||
setId: string;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
const sortable = useSortable({
|
||||
id: `set:${setId}:deck:${deck.id}`,
|
||||
data: { deckId: deck.id, sourceSetId: setId, setId },
|
||||
});
|
||||
return (
|
||||
<div
|
||||
ref={sortable.setNodeRef}
|
||||
style={{
|
||||
transform: CSS.Transform.toString(sortable.transform),
|
||||
transition: sortable.transition,
|
||||
opacity: sortable.isDragging ? 0.4 : 1,
|
||||
}}
|
||||
className="flex min-w-0 items-center gap-2 rounded-xl border border-border-light bg-bg-surface-alt/55 p-3"
|
||||
>
|
||||
<button
|
||||
{...sortable.attributes}
|
||||
{...sortable.listeners}
|
||||
className="grid h-9 w-9 shrink-0 place-items-center rounded-lg text-text-muted hover:bg-bg-surface hover:text-text-heading"
|
||||
aria-label={`Reorder or copy ${deck.name}`}
|
||||
>
|
||||
<span aria-hidden>☷</span>
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-bold text-text-heading">{deck.name}</p>
|
||||
<p className="text-xs text-text-muted">{deck._count.cards} cards</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="grid h-9 w-9 shrink-0 place-items-center rounded-lg text-text-muted hover:bg-error-bg hover:text-error"
|
||||
aria-label={`Remove ${deck.name} from this repetition set`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SetDropZone({ set, children }: { set: SetSummary; children: React.ReactNode }) {
|
||||
const drop = useDroppable({ id: `set:${set.id}`, data: { setId: set.id } });
|
||||
const { over } = useDndContext();
|
||||
const isOver =
|
||||
over?.id === `set:${set.id}` || over?.data.current?.setId === set.id;
|
||||
return (
|
||||
<div
|
||||
ref={drop.setNodeRef}
|
||||
className={`grid min-h-20 grid-cols-1 gap-3 rounded-xl border-2 border-dashed p-3 sm:grid-cols-2 ${
|
||||
isOver ? "border-primary bg-primary/5" : "border-border-light"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpacedRepetitionSets() {
|
||||
const { classSlug } = useParams<{ classSlug: string }>();
|
||||
const [classId, setClassId] = useState("");
|
||||
const [sets, setSets] = useState<SetSummary[]>([]);
|
||||
const [decks, setDecks] = useState<DeckSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState("");
|
||||
const [editing, setEditing] = useState<EditState | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||
);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const classesResponse = await fetch("/api/classes");
|
||||
const classes = (await classesResponse.json()) as Array<{ id: string; slug: string }>;
|
||||
const currentClass = classes.find((item) => item.slug === classSlug);
|
||||
if (!currentClass) throw new Error("Class not found");
|
||||
setClassId(currentClass.id);
|
||||
const [setsResponse, decksResponse] = await Promise.all([
|
||||
fetch(`/api/spaced-repetition-sets?classId=${currentClass.id}`),
|
||||
fetch(`/api/decks/list?classId=${currentClass.id}`),
|
||||
]);
|
||||
if (!setsResponse.ok || !decksResponse.ok) throw new Error("Unable to load repetition sets");
|
||||
setSets(await setsResponse.json());
|
||||
setDecks(await decksResponse.json());
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : "Unable to load repetition sets");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [classSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => void load(), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [load]);
|
||||
|
||||
async function createSet() {
|
||||
if (!newName.trim()) return;
|
||||
const response = await fetch("/api/spaced-repetition-sets", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ classId, name: newName, newCardsPerDay: 30 }),
|
||||
});
|
||||
if (!response.ok) return setMessage("Could not create repetition set");
|
||||
setNewName("");
|
||||
setCreating(false);
|
||||
await load();
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editing) return;
|
||||
const response = await fetch(`/api/spaced-repetition-sets/${editing.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: editing.name,
|
||||
description: editing.description,
|
||||
newCardsPerDay: editing.newCardsPerDay,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) return setMessage("Could not update repetition set");
|
||||
setEditing(null);
|
||||
notifyStudyAvailabilityChanged();
|
||||
await load();
|
||||
}
|
||||
|
||||
async function deleteSet(set: SetSummary) {
|
||||
if (!confirm(`Delete “${set.name}”? Its spaced-repetition schedules will be permanently removed.`)) return;
|
||||
const response = await fetch(`/api/spaced-repetition-sets/${set.id}`, { method: "DELETE" });
|
||||
if (!response.ok) return setMessage("Could not delete repetition set");
|
||||
notifyStudyAvailabilityChanged();
|
||||
await load();
|
||||
}
|
||||
|
||||
async function removeDeck(set: SetSummary, deck: DeckSummary) {
|
||||
if (!confirm(`Remove “${deck.name}” from “${set.name}”? Its scheduling history in this set will be reset.`)) return;
|
||||
const response = await fetch(`/api/spaced-repetition-sets/${set.id}/decks/${deck.id}`, { method: "DELETE" });
|
||||
if (!response.ok) return setMessage("Could not remove deck");
|
||||
notifyStudyAvailabilityChanged();
|
||||
await load();
|
||||
}
|
||||
|
||||
async function addDeck(setId: string, deckId: string) {
|
||||
const target = sets.find((set) => set.id === setId);
|
||||
const deck = decks.find((item) => item.id === deckId);
|
||||
if (target?.decks.some((item) => item.id === deckId)) {
|
||||
setMessage(`${deck?.name ?? "That deck"} is already in ${target.name}.`);
|
||||
return;
|
||||
}
|
||||
const response = await fetch(`/api/spaced-repetition-sets/${setId}/decks`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ deckId }),
|
||||
});
|
||||
if (!response.ok) return setMessage((await response.json()).error ?? "Could not add deck");
|
||||
setMessage(`${deck?.name ?? "Deck"} added.`);
|
||||
notifyStudyAvailabilityChanged();
|
||||
await load();
|
||||
}
|
||||
|
||||
async function handleDragEnd(event: DragEndEvent) {
|
||||
const activeData = event.active.data.current as { deckId?: string; sourceSetId?: string | null } | undefined;
|
||||
const overData = event.over?.data.current as { setId?: string; deckId?: string } | undefined;
|
||||
if (!activeData?.deckId || !overData?.setId) return;
|
||||
const targetSetId = overData.setId;
|
||||
|
||||
if (activeData.sourceSetId === targetSetId && overData.deckId) {
|
||||
const target = sets.find((set) => set.id === targetSetId);
|
||||
if (!target) return;
|
||||
const oldIndex = target.decks.findIndex((deck) => deck.id === activeData.deckId);
|
||||
const newIndex = target.decks.findIndex((deck) => deck.id === overData.deckId);
|
||||
if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return;
|
||||
const ordered = arrayMove(target.decks, oldIndex, newIndex);
|
||||
setSets((current) => current.map((set) => set.id === targetSetId ? { ...set, decks: ordered } : set));
|
||||
const response = await fetch(`/api/spaced-repetition-sets/${targetSetId}/decks`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ deckIds: ordered.map((deck) => deck.id) }),
|
||||
});
|
||||
if (!response.ok) await load();
|
||||
return;
|
||||
}
|
||||
await addDeck(targetSetId, activeData.deckId);
|
||||
}
|
||||
|
||||
if (loading) return <div className="animate-subtle-pulse py-16 text-center text-text-muted">Loading repetition sets…</div>;
|
||||
|
||||
return (
|
||||
<div className="pb-20">
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div><p className="text-xs font-bold uppercase tracking-[0.16em] text-primary">Adaptive review</p><h2 className="editorial-title mt-1 text-3xl text-text-heading">Spaced repetition</h2></div>
|
||||
<button onClick={() => setCreating(true)} className="min-h-12 rounded-xl bg-primary px-5 font-bold text-white shadow-sm hover:bg-primary-hover">New repetition set</button>
|
||||
</div>
|
||||
|
||||
{message && <div className="mb-5 flex items-center justify-between rounded-xl border border-primary/20 bg-bg-callout px-4 py-3 text-sm text-text-heading"><span>{message}</span><button onClick={() => setMessage(null)} aria-label="Dismiss message">×</button></div>}
|
||||
|
||||
{creating && (
|
||||
<div className="mb-6 flex flex-col gap-3 rounded-2xl border border-primary/20 bg-bg-surface p-5 sm:flex-row">
|
||||
<input autoFocus value={newName} onChange={(event) => setNewName(event.target.value)} onKeyDown={(event) => event.key === "Enter" && createSet()} placeholder="e.g. Patho comprehensive review" className="min-h-11 flex-1 rounded-lg border border-border bg-bg-surface-alt px-3 text-text-heading" />
|
||||
<button onClick={createSet} className="rounded-lg bg-primary px-5 font-bold text-white">Create</button>
|
||||
<button onClick={() => { setCreating(false); setNewName(""); }} className="rounded-lg border border-border px-5 font-bold text-text-heading">Cancel</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DndContext sensors={sensors} collisionDetection={pointerWithin} onDragEnd={handleDragEnd}>
|
||||
<div className="space-y-6">
|
||||
{sets.length === 0 && <div className="rounded-2xl border border-dashed border-border p-10 text-center"><h3 className="font-bold text-text-heading">No repetition sets yet</h3><p className="mt-1 text-sm text-text-secondary">Create one, then drag flashcard decks into it.</p></div>}
|
||||
{sets.map((set) => (
|
||||
<section key={set.id} className="rounded-2xl border border-border-light bg-bg-surface/75 p-4 shadow-[var(--shadow-card)] sm:p-5">
|
||||
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-xl font-extrabold text-text-heading">{set.name}</h3>
|
||||
{set.description && <p className="mt-1 text-sm text-text-secondary">{set.description}</p>}
|
||||
<div className="mt-3 flex flex-wrap gap-2 text-xs">
|
||||
<span className="rounded-full bg-badge-bg px-2.5 py-1 text-badge-text">{set.stats.totalCards} cards</span>
|
||||
<span className="rounded-full bg-error-bg px-2.5 py-1 text-error">{set.stats.dueCards} due</span>
|
||||
{set.stats.learningAheadCards > 0 && <span className="rounded-full bg-bg-callout px-2.5 py-1 text-primary">{set.stats.learningAheadCards} learning</span>}
|
||||
<span className="rounded-full bg-success-bg px-2.5 py-1 text-success">{set.stats.newCardsAvailable} new today</span>
|
||||
<span className="rounded-full bg-bg-surface-alt px-2.5 py-1 text-text-muted">Limit {set.newCardsPerDay}/day</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{set.stats.totalCards === 0 ? <button disabled className="min-h-10 rounded-lg bg-bg-surface-alt px-5 text-sm font-bold text-text-muted">Add decks to study</button> : set.stats.availableCards > 0 ? (
|
||||
<Link href={`/${classSlug}/spaced-repetition/${set.id}`} className="inline-flex min-h-10 items-center rounded-lg bg-primary px-5 text-sm font-bold text-white hover:bg-primary-hover">{set.stats.reviewedCards === 0 ? "Study" : "Continue"} · {set.stats.availableCards}</Link>
|
||||
) : <Link href={`/${classSlug}/spaced-repetition/${set.id}`} className="inline-flex min-h-10 items-center rounded-lg bg-success-bg px-5 text-sm font-bold text-success">Caught up</Link>}
|
||||
<button onClick={() => setEditing({ id: set.id, name: set.name, description: set.description ?? "", newCardsPerDay: set.newCardsPerDay })} className="grid h-10 w-10 place-items-center rounded-lg text-text-muted hover:bg-bg-surface-alt hover:text-text-heading" aria-label={`Edit ${set.name}`}>✎</button>
|
||||
<button onClick={() => deleteSet(set)} className="grid h-10 w-10 place-items-center rounded-lg text-text-muted hover:bg-error-bg hover:text-error" aria-label={`Delete ${set.name}`}>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SortableContext items={set.decks.map((deck) => `set:${set.id}:deck:${deck.id}`)} strategy={verticalListSortingStrategy}>
|
||||
<SetDropZone set={set}>
|
||||
{set.decks.length === 0 ? <p className="col-span-full py-4 text-center text-sm text-text-muted">Drop flashcard decks here</p> : set.decks.map((deck) => <MembershipDeck key={deck.id} deck={deck} setId={set.id} onRemove={() => removeDeck(set, deck)} />)}
|
||||
</SetDropZone>
|
||||
</SortableContext>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section className="mt-10 border-t border-border-light pt-8">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.16em] text-primary">Source library</p>
|
||||
<h3 className="editorial-title mt-1 text-2xl text-text-heading">Flashcard decks</h3>
|
||||
<p className="mb-5 mt-1 text-sm text-text-secondary">Drag a deck into any repetition set. The original deck stays here and can be reused.</p>
|
||||
{decks.length ? <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">{decks.map((deck) => <SourceDeck key={deck.id} deck={deck} />)}</div> : <div className="rounded-xl border border-dashed border-border p-8 text-center text-sm text-text-muted">Create flashcard decks before building a repetition set.</div>}
|
||||
</section>
|
||||
</div>
|
||||
</DndContext>
|
||||
|
||||
{editing && (
|
||||
<div className="fixed inset-0 z-50 grid place-items-center bg-black/45 p-4 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-3xl border border-border bg-bg-surface p-6 shadow-[var(--shadow-modal)]">
|
||||
<h3 className="editorial-title text-2xl text-text-heading">Edit repetition set</h3>
|
||||
<label className="mt-5 block text-sm font-bold text-text-heading">Name</label>
|
||||
<input value={editing.name} onChange={(event) => setEditing({ ...editing, name: event.target.value })} className="mt-1 min-h-11 w-full rounded-lg border border-border bg-bg-surface-alt px-3 text-text-heading" />
|
||||
<label className="mt-4 block text-sm font-bold text-text-heading">Description</label>
|
||||
<textarea value={editing.description} onChange={(event) => setEditing({ ...editing, description: event.target.value })} rows={3} className="mt-1 w-full resize-none rounded-lg border border-border bg-bg-surface-alt p-3 text-text-heading" />
|
||||
<label className="mt-4 block text-sm font-bold text-text-heading">New cards per day</label>
|
||||
<input type="number" min={0} max={999} value={editing.newCardsPerDay} onChange={(event) => setEditing({ ...editing, newCardsPerDay: Number(event.target.value) })} className="mt-1 min-h-11 w-full rounded-lg border border-border bg-bg-surface-alt px-3 text-text-heading" />
|
||||
<p className="mt-1 text-xs text-text-muted">Due reviews are never capped. Set this to 0 to pause new cards.</p>
|
||||
<div className="mt-6 flex justify-end gap-2"><button onClick={() => setEditing(null)} className="min-h-10 rounded-lg border border-border px-4 font-bold text-text-heading">Cancel</button><button onClick={saveEdit} className="min-h-10 rounded-lg bg-primary px-4 font-bold text-white">Save</button></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
220
src/components/spaced-repetition/SpacedRepetitionViewer.tsx
Normal file
220
src/components/spaced-repetition/SpacedRepetitionViewer.tsx
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { SpacedRepetitionRating } from "@/lib/spacedRepetition";
|
||||
|
||||
interface StudyState {
|
||||
set: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
class: { slug: string; name: string };
|
||||
stats: {
|
||||
totalCards: number;
|
||||
reviewedCards: number;
|
||||
unseenCards: number;
|
||||
dueCards: number;
|
||||
learningAheadCards: number;
|
||||
newCardsAvailable: number;
|
||||
availableCards: number;
|
||||
nextDue: string | null;
|
||||
};
|
||||
};
|
||||
currentCard: {
|
||||
id: string;
|
||||
front: string;
|
||||
back: string;
|
||||
sourceDeckName: string;
|
||||
stateVersion: string | null;
|
||||
previews: Array<{
|
||||
rating: SpacedRepetitionRating;
|
||||
due: string;
|
||||
scheduledDays: number;
|
||||
}>;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const ratingStyles: Record<SpacedRepetitionRating, string> = {
|
||||
AGAIN: "border-error/35 text-error hover:bg-error-bg",
|
||||
HARD: "border-accent/40 text-accent hover:bg-bg-surface-alt",
|
||||
GOOD: "border-success/35 text-success hover:bg-success-bg",
|
||||
EASY: "border-primary/35 text-primary hover:bg-bg-callout",
|
||||
};
|
||||
|
||||
function intervalLabel(due: string, scheduledDays: number) {
|
||||
if (scheduledDays >= 1) {
|
||||
if (scheduledDays < 30) return `${scheduledDays}d`;
|
||||
const months = Math.round(scheduledDays / 30);
|
||||
if (months < 12) return `${months}mo`;
|
||||
return `${Math.round(months / 12)}y`;
|
||||
}
|
||||
|
||||
const minutes = Math.max(1, Math.round((new Date(due).getTime() - Date.now()) / 60000));
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
return `${hours}h`;
|
||||
}
|
||||
|
||||
function dueLabel(due: string | null) {
|
||||
if (!due) return "No reviews scheduled";
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(due));
|
||||
}
|
||||
|
||||
export function SpacedRepetitionViewer() {
|
||||
const { classSlug, setId } = useParams<{ classSlug: string; setId: string }>();
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<StudyState | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [flipped, setFlipped] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/spaced-repetition-sets/${setId}/study`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) throw new Error("Repetition set not found");
|
||||
const nextData = (await response.json()) as StudyState;
|
||||
if (nextData.set.class.slug !== classSlug) throw new Error("Repetition set not found");
|
||||
setData(nextData);
|
||||
setFlipped(false);
|
||||
setError(null);
|
||||
} catch {
|
||||
router.push(`/${classSlug}/spaced-repetition`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [classSlug, router, setId]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => void load(), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [load]);
|
||||
|
||||
const submitRating = useCallback(async (rating: SpacedRepetitionRating) => {
|
||||
if (!data?.currentCard || saving) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/spaced-repetition-sets/${setId}/reviews`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
flashcardId: data.currentCard.id,
|
||||
rating,
|
||||
expectedStateVersion: data.currentCard.stateVersion,
|
||||
}),
|
||||
});
|
||||
if (response.status === 409) {
|
||||
await load();
|
||||
setError("That review was already saved. The queue has been refreshed.");
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => null);
|
||||
throw new Error(body?.error ?? "Unable to save review");
|
||||
}
|
||||
setData(await response.json());
|
||||
window.dispatchEvent(new Event("spaced-repetition-updated"));
|
||||
setFlipped(false);
|
||||
} catch (reviewError) {
|
||||
setError(reviewError instanceof Error ? reviewError.message : "Unable to save review");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [data, load, saving, setId]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (!data?.currentCard || saving) return;
|
||||
if (event.key === " " || event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
setFlipped((current) => !current);
|
||||
return;
|
||||
}
|
||||
if (!flipped) return;
|
||||
const keyboardRatings: Record<string, SpacedRepetitionRating> = {
|
||||
"1": "AGAIN", "2": "HARD", "3": "GOOD", "4": "EASY",
|
||||
};
|
||||
const rating = keyboardRatings[event.key];
|
||||
if (rating) submitRating(rating);
|
||||
}
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [data, flipped, saving, submitRating]);
|
||||
|
||||
if (loading || !data) {
|
||||
return <div className="mx-auto max-w-5xl animate-subtle-pulse py-16 text-center text-text-muted">Loading review queue…</div>;
|
||||
}
|
||||
|
||||
const card = data.currentCard;
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl py-1 pb-16 md:py-3">
|
||||
<header className="mb-6 rounded-2xl border border-border-light bg-bg-surface/75 p-4 shadow-[var(--shadow-card)] sm:p-5">
|
||||
<button onClick={() => router.push(`/${classSlug}/spaced-repetition`)} className="mb-2 inline-flex min-h-10 items-center gap-2 text-sm text-text-muted hover:text-text-heading"><span aria-hidden>←</span> Back to repetition sets</button>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div><p className="text-xs font-bold uppercase tracking-[0.16em] text-primary">Spaced repetition</p><h1 className="editorial-title mt-1 text-3xl text-text-heading">{data.set.name}</h1>{data.set.description && <p className="mt-1 text-sm text-text-secondary">{data.set.description}</p>}</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<span className="rounded-full bg-error-bg px-3 py-1.5 text-error">{data.set.stats.dueCards} due</span>
|
||||
{data.set.stats.learningAheadCards > 0 && <span className="rounded-full bg-bg-callout px-3 py-1.5 text-primary">{data.set.stats.learningAheadCards} learning</span>}
|
||||
<span className="rounded-full bg-success-bg px-3 py-1.5 text-success">{data.set.stats.newCardsAvailable} new</span>
|
||||
<span className="rounded-full bg-bg-surface-alt px-3 py-1.5 text-text-muted">{data.set.stats.reviewedCards}/{data.set.stats.totalCards} introduced</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <div role="alert" className="mb-5 flex items-center justify-between rounded-xl border border-error/30 bg-error-bg px-4 py-3 text-sm text-error"><span>{error}</span><button onClick={() => setError(null)} aria-label="Dismiss error">×</button></div>}
|
||||
|
||||
{!card ? (
|
||||
<div className="rounded-3xl border border-success/25 bg-bg-surface p-8 text-center shadow-[var(--shadow-card)] sm:p-12">
|
||||
<div className="mx-auto grid h-16 w-16 place-items-center rounded-full bg-success-bg text-3xl text-success">✓</div>
|
||||
<h2 className="editorial-title mt-5 text-3xl text-text-heading">You’re caught up</h2>
|
||||
<p className="mt-2 text-text-secondary">There are no due cards and today’s new-card allowance is complete.</p>
|
||||
<p className="mt-5 text-sm font-semibold text-text-muted">Next review: {dueLabel(data.set.stats.nextDue)}</p>
|
||||
<button onClick={() => router.push(`/${classSlug}/spaced-repetition`)} className="mt-7 min-h-11 rounded-xl bg-primary px-6 font-bold text-white hover:bg-primary-hover">Back to sets</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-4 flex w-full items-center justify-between text-sm text-text-muted"><span>From {card.sourceDeckName}</span><span>{data.set.stats.availableCards} available</span></div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={flipped ? "Showing card answer. Press Space to show front." : "Showing card front. Press Space to reveal answer."}
|
||||
onClick={() => !saving && setFlipped((current) => !current)}
|
||||
className="perspective-1000 relative w-full cursor-pointer select-none"
|
||||
>
|
||||
<div className={`preserve-3d relative min-h-[420px] w-full transition-transform duration-300 md:min-h-[560px] ${flipped ? "rotate-x-180" : ""}`}>
|
||||
<div className="paper-grid backface-hidden absolute inset-0 flex flex-col items-center justify-center rounded-[2rem] border border-border-light bg-bg-surface p-7 shadow-[var(--shadow-card-hover)] md:p-12">
|
||||
<span className="absolute right-5 top-4 text-xs font-bold uppercase tracking-wider text-text-muted">Front</span>
|
||||
<div className="markdown-content w-full text-center font-serif text-2xl font-semibold leading-relaxed text-text-heading md:text-4xl"><ReactMarkdown remarkPlugins={[remarkGfm]}>{card.front}</ReactMarkdown></div>
|
||||
<p className="mt-7 text-xs text-text-muted">Tap or press Space to reveal the answer</p>
|
||||
</div>
|
||||
<div className="paper-grid backface-hidden absolute inset-0 flex rotate-x-180 flex-col items-center justify-center rounded-[2rem] border border-primary/25 bg-bg-surface p-7 shadow-[var(--shadow-card-hover)] md:p-12">
|
||||
<span className="absolute right-5 top-4 text-xs font-bold uppercase tracking-wider text-text-muted">Back</span>
|
||||
<div className="markdown-content max-h-[390px] w-full overflow-y-auto text-center text-lg leading-relaxed text-text-body md:max-h-[450px] md:text-xl"><ReactMarkdown remarkPlugins={[remarkGfm]}>{card.back}</ReactMarkdown></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 grid w-full grid-cols-2 gap-2 transition-opacity sm:grid-cols-4 ${flipped ? "opacity-100" : "pointer-events-none opacity-0"}`} aria-hidden={!flipped}>
|
||||
{card.previews.map((preview, index) => (
|
||||
<button key={preview.rating} disabled={saving || !flipped} onClick={() => submitRating(preview.rating)} className={`min-h-16 rounded-xl border-2 bg-bg-surface px-3 py-2 text-sm font-extrabold transition-colors disabled:opacity-50 ${ratingStyles[preview.rating]}`}>
|
||||
<span className="block">{preview.rating[0]}{preview.rating.slice(1).toLowerCase()}</span>
|
||||
<span className="mt-0.5 block text-xs font-semibold opacity-75">{index + 1} · {intervalLabel(preview.due, preview.scheduledDays)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-center text-xs text-text-muted">{flipped ? "Choose 1–4 based on how well you remembered" : "Space to flip"}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
|
@ -19,6 +19,37 @@ export function Navbar() {
|
|||
const [open, setOpen] = useState(false);
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const classSlug = parts[0] && parts[0] !== "shared" ? parts[0] : null;
|
||||
const [hasReadyCards, setHasReadyCards] = useState(false);
|
||||
|
||||
const refreshDueCards = useCallback(async () => {
|
||||
if (!classSlug) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/classes");
|
||||
if (!response.ok) return;
|
||||
const classes = await response.json() as Array<{
|
||||
slug: string;
|
||||
spacedRepetitionReadyCards: number;
|
||||
}>;
|
||||
const currentClass = classes.find((classItem) => classItem.slug === classSlug);
|
||||
setHasReadyCards((currentClass?.spacedRepetitionReadyCards ?? 0) > 0);
|
||||
} catch {
|
||||
// Keep the last known state when a background refresh fails.
|
||||
}
|
||||
}, [classSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialRefresh = window.setTimeout(() => void refreshDueCards(), 0);
|
||||
const interval = window.setInterval(refreshDueCards, 60_000);
|
||||
window.addEventListener("focus", refreshDueCards);
|
||||
window.addEventListener("spaced-repetition-updated", refreshDueCards);
|
||||
return () => {
|
||||
window.clearTimeout(initialRefresh);
|
||||
window.clearInterval(interval);
|
||||
window.removeEventListener("focus", refreshDueCards);
|
||||
window.removeEventListener("spaced-repetition-updated", refreshDueCards);
|
||||
};
|
||||
}, [refreshDueCards]);
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
|
|
@ -61,6 +92,22 @@ export function Navbar() {
|
|||
>
|
||||
<span aria-hidden>✓</span> Quizzes
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${classSlug}/spaced-repetition`}
|
||||
onClick={() => setOpen(false)}
|
||||
className={`mt-1 flex min-h-11 items-center gap-3 rounded-xl px-3 text-sm font-semibold transition-colors ${pathname.includes("/spaced-repetition") ? "bg-bg-callout text-primary" : "text-text-secondary hover:bg-bg-surface-alt hover:text-text-heading"}`}
|
||||
>
|
||||
<span aria-hidden>↻</span> Spaced Repetition
|
||||
{classSlug && hasReadyCards && (
|
||||
<span
|
||||
className="ml-auto grid h-5 w-5 place-items-center rounded-full bg-error-bg text-xs font-extrabold text-error"
|
||||
title="Spaced repetition cards are ready to study"
|
||||
>
|
||||
<span aria-hidden>!</span>
|
||||
<span className="sr-only">Cards ready to study</span>
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const STUDY_MODES = [
|
||||
{ key: "flashcards", label: "Flashcards", path: "flashcards" },
|
||||
{ key: "quizzes", label: "Quizzes", path: "quizzes" },
|
||||
{ key: "spaced-repetition", label: "Spaced Repetition", path: "spaced-repetition" },
|
||||
// Future modes get added here — the layout, tab bar, and auth middleware need no changes.
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -82,3 +82,18 @@ export type StudyActivity = Prisma.StudyActivityModel
|
|||
*
|
||||
*/
|
||||
export type MaterialGroup = Prisma.MaterialGroupModel
|
||||
/**
|
||||
* Model SpacedRepetitionSet
|
||||
*
|
||||
*/
|
||||
export type SpacedRepetitionSet = Prisma.SpacedRepetitionSetModel
|
||||
/**
|
||||
* Model SpacedRepetitionSetDeck
|
||||
*
|
||||
*/
|
||||
export type SpacedRepetitionSetDeck = Prisma.SpacedRepetitionSetDeckModel
|
||||
/**
|
||||
* Model SpacedRepetitionCardState
|
||||
*
|
||||
*/
|
||||
export type SpacedRepetitionCardState = Prisma.SpacedRepetitionCardStateModel
|
||||
|
|
|
|||
|
|
@ -106,3 +106,18 @@ export type StudyActivity = Prisma.StudyActivityModel
|
|||
*
|
||||
*/
|
||||
export type MaterialGroup = Prisma.MaterialGroupModel
|
||||
/**
|
||||
* Model SpacedRepetitionSet
|
||||
*
|
||||
*/
|
||||
export type SpacedRepetitionSet = Prisma.SpacedRepetitionSetModel
|
||||
/**
|
||||
* Model SpacedRepetitionSetDeck
|
||||
*
|
||||
*/
|
||||
export type SpacedRepetitionSetDeck = Prisma.SpacedRepetitionSetDeckModel
|
||||
/**
|
||||
* Model SpacedRepetitionCardState
|
||||
*
|
||||
*/
|
||||
export type SpacedRepetitionCardState = Prisma.SpacedRepetitionCardStateModel
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -396,7 +396,10 @@ export const ModelName = {
|
|||
AuthSecurity: 'AuthSecurity',
|
||||
Setting: 'Setting',
|
||||
StudyActivity: 'StudyActivity',
|
||||
MaterialGroup: 'MaterialGroup'
|
||||
MaterialGroup: 'MaterialGroup',
|
||||
SpacedRepetitionSet: 'SpacedRepetitionSet',
|
||||
SpacedRepetitionSetDeck: 'SpacedRepetitionSetDeck',
|
||||
SpacedRepetitionCardState: 'SpacedRepetitionCardState'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
|
@ -412,7 +415,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "class" | "deck" | "flashcard" | "quizSet" | "question" | "answerOption" | "studyProgress" | "quizAttempt" | "shareLink" | "authSecurity" | "setting" | "studyActivity" | "materialGroup"
|
||||
modelProps: "class" | "deck" | "flashcard" | "quizSet" | "question" | "answerOption" | "studyProgress" | "quizAttempt" | "shareLink" | "authSecurity" | "setting" | "studyActivity" | "materialGroup" | "spacedRepetitionSet" | "spacedRepetitionSetDeck" | "spacedRepetitionCardState"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
|
|
@ -1378,6 +1381,228 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||
}
|
||||
}
|
||||
}
|
||||
SpacedRepetitionSet: {
|
||||
payload: Prisma.$SpacedRepetitionSetPayload<ExtArgs>
|
||||
fields: Prisma.SpacedRepetitionSetFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.SpacedRepetitionSetFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.SpacedRepetitionSetFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.SpacedRepetitionSetFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.SpacedRepetitionSetFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.SpacedRepetitionSetFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.SpacedRepetitionSetCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.SpacedRepetitionSetCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.SpacedRepetitionSetCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.SpacedRepetitionSetDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.SpacedRepetitionSetUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.SpacedRepetitionSetDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.SpacedRepetitionSetUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.SpacedRepetitionSetUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.SpacedRepetitionSetUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.SpacedRepetitionSetAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateSpacedRepetitionSet>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.SpacedRepetitionSetGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SpacedRepetitionSetGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.SpacedRepetitionSetCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SpacedRepetitionSetCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacedRepetitionSetDeck: {
|
||||
payload: Prisma.$SpacedRepetitionSetDeckPayload<ExtArgs>
|
||||
fields: Prisma.SpacedRepetitionSetDeckFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.SpacedRepetitionSetDeckFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.SpacedRepetitionSetDeckFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.SpacedRepetitionSetDeckFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.SpacedRepetitionSetDeckFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.SpacedRepetitionSetDeckFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.SpacedRepetitionSetDeckCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.SpacedRepetitionSetDeckCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.SpacedRepetitionSetDeckCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.SpacedRepetitionSetDeckDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.SpacedRepetitionSetDeckUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.SpacedRepetitionSetDeckDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.SpacedRepetitionSetDeckUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.SpacedRepetitionSetDeckUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.SpacedRepetitionSetDeckUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionSetDeckPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.SpacedRepetitionSetDeckAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateSpacedRepetitionSetDeck>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.SpacedRepetitionSetDeckGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SpacedRepetitionSetDeckGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.SpacedRepetitionSetDeckCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SpacedRepetitionSetDeckCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacedRepetitionCardState: {
|
||||
payload: Prisma.$SpacedRepetitionCardStatePayload<ExtArgs>
|
||||
fields: Prisma.SpacedRepetitionCardStateFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.SpacedRepetitionCardStateFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.SpacedRepetitionCardStateFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.SpacedRepetitionCardStateFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.SpacedRepetitionCardStateFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.SpacedRepetitionCardStateFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.SpacedRepetitionCardStateCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.SpacedRepetitionCardStateCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.SpacedRepetitionCardStateCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.SpacedRepetitionCardStateDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.SpacedRepetitionCardStateUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.SpacedRepetitionCardStateDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.SpacedRepetitionCardStateUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.SpacedRepetitionCardStateUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.SpacedRepetitionCardStateUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SpacedRepetitionCardStatePayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.SpacedRepetitionCardStateAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateSpacedRepetitionCardState>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.SpacedRepetitionCardStateGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SpacedRepetitionCardStateGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.SpacedRepetitionCardStateCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SpacedRepetitionCardStateCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} & {
|
||||
other: {
|
||||
|
|
@ -1566,6 +1791,52 @@ export const MaterialGroupScalarFieldEnum = {
|
|||
export type MaterialGroupScalarFieldEnum = (typeof MaterialGroupScalarFieldEnum)[keyof typeof MaterialGroupScalarFieldEnum]
|
||||
|
||||
|
||||
export const SpacedRepetitionSetScalarFieldEnum = {
|
||||
id: 'id',
|
||||
classId: 'classId',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
newCardsPerDay: 'newCardsPerDay',
|
||||
sortOrder: 'sortOrder',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type SpacedRepetitionSetScalarFieldEnum = (typeof SpacedRepetitionSetScalarFieldEnum)[keyof typeof SpacedRepetitionSetScalarFieldEnum]
|
||||
|
||||
|
||||
export const SpacedRepetitionSetDeckScalarFieldEnum = {
|
||||
setId: 'setId',
|
||||
deckId: 'deckId',
|
||||
sortOrder: 'sortOrder',
|
||||
addedAt: 'addedAt'
|
||||
} as const
|
||||
|
||||
export type SpacedRepetitionSetDeckScalarFieldEnum = (typeof SpacedRepetitionSetDeckScalarFieldEnum)[keyof typeof SpacedRepetitionSetDeckScalarFieldEnum]
|
||||
|
||||
|
||||
export const SpacedRepetitionCardStateScalarFieldEnum = {
|
||||
id: 'id',
|
||||
setId: 'setId',
|
||||
flashcardId: 'flashcardId',
|
||||
due: 'due',
|
||||
stability: 'stability',
|
||||
difficulty: 'difficulty',
|
||||
elapsedDays: 'elapsedDays',
|
||||
scheduledDays: 'scheduledDays',
|
||||
learningSteps: 'learningSteps',
|
||||
reps: 'reps',
|
||||
lapses: 'lapses',
|
||||
state: 'state',
|
||||
firstReviewedAt: 'firstReviewedAt',
|
||||
lastReview: 'lastReview',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type SpacedRepetitionCardStateScalarFieldEnum = (typeof SpacedRepetitionCardStateScalarFieldEnum)[keyof typeof SpacedRepetitionCardStateScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
|
|
@ -1745,6 +2016,9 @@ export type GlobalOmitConfig = {
|
|||
setting?: Prisma.SettingOmit
|
||||
studyActivity?: Prisma.StudyActivityOmit
|
||||
materialGroup?: Prisma.MaterialGroupOmit
|
||||
spacedRepetitionSet?: Prisma.SpacedRepetitionSetOmit
|
||||
spacedRepetitionSetDeck?: Prisma.SpacedRepetitionSetDeckOmit
|
||||
spacedRepetitionCardState?: Prisma.SpacedRepetitionCardStateOmit
|
||||
}
|
||||
|
||||
/* Types for Logging */
|
||||
|
|
|
|||
|
|
@ -63,7 +63,10 @@ export const ModelName = {
|
|||
AuthSecurity: 'AuthSecurity',
|
||||
Setting: 'Setting',
|
||||
StudyActivity: 'StudyActivity',
|
||||
MaterialGroup: 'MaterialGroup'
|
||||
MaterialGroup: 'MaterialGroup',
|
||||
SpacedRepetitionSet: 'SpacedRepetitionSet',
|
||||
SpacedRepetitionSetDeck: 'SpacedRepetitionSetDeck',
|
||||
SpacedRepetitionCardState: 'SpacedRepetitionCardState'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
|
@ -231,6 +234,52 @@ export const MaterialGroupScalarFieldEnum = {
|
|||
export type MaterialGroupScalarFieldEnum = (typeof MaterialGroupScalarFieldEnum)[keyof typeof MaterialGroupScalarFieldEnum]
|
||||
|
||||
|
||||
export const SpacedRepetitionSetScalarFieldEnum = {
|
||||
id: 'id',
|
||||
classId: 'classId',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
newCardsPerDay: 'newCardsPerDay',
|
||||
sortOrder: 'sortOrder',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type SpacedRepetitionSetScalarFieldEnum = (typeof SpacedRepetitionSetScalarFieldEnum)[keyof typeof SpacedRepetitionSetScalarFieldEnum]
|
||||
|
||||
|
||||
export const SpacedRepetitionSetDeckScalarFieldEnum = {
|
||||
setId: 'setId',
|
||||
deckId: 'deckId',
|
||||
sortOrder: 'sortOrder',
|
||||
addedAt: 'addedAt'
|
||||
} as const
|
||||
|
||||
export type SpacedRepetitionSetDeckScalarFieldEnum = (typeof SpacedRepetitionSetDeckScalarFieldEnum)[keyof typeof SpacedRepetitionSetDeckScalarFieldEnum]
|
||||
|
||||
|
||||
export const SpacedRepetitionCardStateScalarFieldEnum = {
|
||||
id: 'id',
|
||||
setId: 'setId',
|
||||
flashcardId: 'flashcardId',
|
||||
due: 'due',
|
||||
stability: 'stability',
|
||||
difficulty: 'difficulty',
|
||||
elapsedDays: 'elapsedDays',
|
||||
scheduledDays: 'scheduledDays',
|
||||
learningSteps: 'learningSteps',
|
||||
reps: 'reps',
|
||||
lapses: 'lapses',
|
||||
state: 'state',
|
||||
firstReviewedAt: 'firstReviewedAt',
|
||||
lastReview: 'lastReview',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type SpacedRepetitionCardStateScalarFieldEnum = (typeof SpacedRepetitionCardStateScalarFieldEnum)[keyof typeof SpacedRepetitionCardStateScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
|
|
|
|||
|
|
@ -21,4 +21,7 @@ export type * from './models/AuthSecurity'
|
|||
export type * from './models/Setting'
|
||||
export type * from './models/StudyActivity'
|
||||
export type * from './models/MaterialGroup'
|
||||
export type * from './models/SpacedRepetitionSet'
|
||||
export type * from './models/SpacedRepetitionSetDeck'
|
||||
export type * from './models/SpacedRepetitionCardState'
|
||||
export type * from './commonInputTypes'
|
||||
|
|
@ -219,6 +219,7 @@ export type ClassWhereInput = {
|
|||
decks?: Prisma.DeckListRelationFilter
|
||||
quizSets?: Prisma.QuizSetListRelationFilter
|
||||
materialGroups?: Prisma.MaterialGroupListRelationFilter
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetListRelationFilter
|
||||
}
|
||||
|
||||
export type ClassOrderByWithRelationInput = {
|
||||
|
|
@ -230,6 +231,7 @@ export type ClassOrderByWithRelationInput = {
|
|||
decks?: Prisma.DeckOrderByRelationAggregateInput
|
||||
quizSets?: Prisma.QuizSetOrderByRelationAggregateInput
|
||||
materialGroups?: Prisma.MaterialGroupOrderByRelationAggregateInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetOrderByRelationAggregateInput
|
||||
}
|
||||
|
||||
export type ClassWhereUniqueInput = Prisma.AtLeast<{
|
||||
|
|
@ -244,6 +246,7 @@ export type ClassWhereUniqueInput = Prisma.AtLeast<{
|
|||
decks?: Prisma.DeckListRelationFilter
|
||||
quizSets?: Prisma.QuizSetListRelationFilter
|
||||
materialGroups?: Prisma.MaterialGroupListRelationFilter
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetListRelationFilter
|
||||
}, "id" | "slug">
|
||||
|
||||
export type ClassOrderByWithAggregationInput = {
|
||||
|
|
@ -279,6 +282,7 @@ export type ClassCreateInput = {
|
|||
decks?: Prisma.DeckCreateNestedManyWithoutClassInput
|
||||
quizSets?: Prisma.QuizSetCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedCreateInput = {
|
||||
|
|
@ -290,6 +294,7 @@ export type ClassUncheckedCreateInput = {
|
|||
decks?: Prisma.DeckUncheckedCreateNestedManyWithoutClassInput
|
||||
quizSets?: Prisma.QuizSetUncheckedCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassUpdateInput = {
|
||||
|
|
@ -301,6 +306,7 @@ export type ClassUpdateInput = {
|
|||
decks?: Prisma.DeckUpdateManyWithoutClassNestedInput
|
||||
quizSets?: Prisma.QuizSetUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedUpdateInput = {
|
||||
|
|
@ -312,6 +318,7 @@ export type ClassUncheckedUpdateInput = {
|
|||
decks?: Prisma.DeckUncheckedUpdateManyWithoutClassNestedInput
|
||||
quizSets?: Prisma.QuizSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassCreateManyInput = {
|
||||
|
|
@ -433,6 +440,20 @@ export type ClassUpdateOneRequiredWithoutMaterialGroupsNestedInput = {
|
|||
update?: Prisma.XOR<Prisma.XOR<Prisma.ClassUpdateToOneWithWhereWithoutMaterialGroupsInput, Prisma.ClassUpdateWithoutMaterialGroupsInput>, Prisma.ClassUncheckedUpdateWithoutMaterialGroupsInput>
|
||||
}
|
||||
|
||||
export type ClassCreateNestedOneWithoutSpacedRepetitionSetsInput = {
|
||||
create?: Prisma.XOR<Prisma.ClassCreateWithoutSpacedRepetitionSetsInput, Prisma.ClassUncheckedCreateWithoutSpacedRepetitionSetsInput>
|
||||
connectOrCreate?: Prisma.ClassCreateOrConnectWithoutSpacedRepetitionSetsInput
|
||||
connect?: Prisma.ClassWhereUniqueInput
|
||||
}
|
||||
|
||||
export type ClassUpdateOneRequiredWithoutSpacedRepetitionSetsNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ClassCreateWithoutSpacedRepetitionSetsInput, Prisma.ClassUncheckedCreateWithoutSpacedRepetitionSetsInput>
|
||||
connectOrCreate?: Prisma.ClassCreateOrConnectWithoutSpacedRepetitionSetsInput
|
||||
upsert?: Prisma.ClassUpsertWithoutSpacedRepetitionSetsInput
|
||||
connect?: Prisma.ClassWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ClassUpdateToOneWithWhereWithoutSpacedRepetitionSetsInput, Prisma.ClassUpdateWithoutSpacedRepetitionSetsInput>, Prisma.ClassUncheckedUpdateWithoutSpacedRepetitionSetsInput>
|
||||
}
|
||||
|
||||
export type ClassCreateWithoutDecksInput = {
|
||||
id?: string
|
||||
slug: string
|
||||
|
|
@ -441,6 +462,7 @@ export type ClassCreateWithoutDecksInput = {
|
|||
createdAt?: Date | string
|
||||
quizSets?: Prisma.QuizSetCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedCreateWithoutDecksInput = {
|
||||
|
|
@ -451,6 +473,7 @@ export type ClassUncheckedCreateWithoutDecksInput = {
|
|||
createdAt?: Date | string
|
||||
quizSets?: Prisma.QuizSetUncheckedCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassCreateOrConnectWithoutDecksInput = {
|
||||
|
|
@ -477,6 +500,7 @@ export type ClassUpdateWithoutDecksInput = {
|
|||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
quizSets?: Prisma.QuizSetUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedUpdateWithoutDecksInput = {
|
||||
|
|
@ -487,6 +511,7 @@ export type ClassUncheckedUpdateWithoutDecksInput = {
|
|||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
quizSets?: Prisma.QuizSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassCreateWithoutQuizSetsInput = {
|
||||
|
|
@ -497,6 +522,7 @@ export type ClassCreateWithoutQuizSetsInput = {
|
|||
createdAt?: Date | string
|
||||
decks?: Prisma.DeckCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedCreateWithoutQuizSetsInput = {
|
||||
|
|
@ -507,6 +533,7 @@ export type ClassUncheckedCreateWithoutQuizSetsInput = {
|
|||
createdAt?: Date | string
|
||||
decks?: Prisma.DeckUncheckedCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassCreateOrConnectWithoutQuizSetsInput = {
|
||||
|
|
@ -533,6 +560,7 @@ export type ClassUpdateWithoutQuizSetsInput = {
|
|||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
decks?: Prisma.DeckUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedUpdateWithoutQuizSetsInput = {
|
||||
|
|
@ -543,6 +571,7 @@ export type ClassUncheckedUpdateWithoutQuizSetsInput = {
|
|||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
decks?: Prisma.DeckUncheckedUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassCreateWithoutMaterialGroupsInput = {
|
||||
|
|
@ -553,6 +582,7 @@ export type ClassCreateWithoutMaterialGroupsInput = {
|
|||
createdAt?: Date | string
|
||||
decks?: Prisma.DeckCreateNestedManyWithoutClassInput
|
||||
quizSets?: Prisma.QuizSetCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedCreateWithoutMaterialGroupsInput = {
|
||||
|
|
@ -563,6 +593,7 @@ export type ClassUncheckedCreateWithoutMaterialGroupsInput = {
|
|||
createdAt?: Date | string
|
||||
decks?: Prisma.DeckUncheckedCreateNestedManyWithoutClassInput
|
||||
quizSets?: Prisma.QuizSetUncheckedCreateNestedManyWithoutClassInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassCreateOrConnectWithoutMaterialGroupsInput = {
|
||||
|
|
@ -589,6 +620,7 @@ export type ClassUpdateWithoutMaterialGroupsInput = {
|
|||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
decks?: Prisma.DeckUpdateManyWithoutClassNestedInput
|
||||
quizSets?: Prisma.QuizSetUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedUpdateWithoutMaterialGroupsInput = {
|
||||
|
|
@ -599,6 +631,67 @@ export type ClassUncheckedUpdateWithoutMaterialGroupsInput = {
|
|||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
decks?: Prisma.DeckUncheckedUpdateManyWithoutClassNestedInput
|
||||
quizSets?: Prisma.QuizSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
spacedRepetitionSets?: Prisma.SpacedRepetitionSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassCreateWithoutSpacedRepetitionSetsInput = {
|
||||
id?: string
|
||||
slug: string
|
||||
name: string
|
||||
sortOrder?: number
|
||||
createdAt?: Date | string
|
||||
decks?: Prisma.DeckCreateNestedManyWithoutClassInput
|
||||
quizSets?: Prisma.QuizSetCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedCreateWithoutSpacedRepetitionSetsInput = {
|
||||
id?: string
|
||||
slug: string
|
||||
name: string
|
||||
sortOrder?: number
|
||||
createdAt?: Date | string
|
||||
decks?: Prisma.DeckUncheckedCreateNestedManyWithoutClassInput
|
||||
quizSets?: Prisma.QuizSetUncheckedCreateNestedManyWithoutClassInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedCreateNestedManyWithoutClassInput
|
||||
}
|
||||
|
||||
export type ClassCreateOrConnectWithoutSpacedRepetitionSetsInput = {
|
||||
where: Prisma.ClassWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.ClassCreateWithoutSpacedRepetitionSetsInput, Prisma.ClassUncheckedCreateWithoutSpacedRepetitionSetsInput>
|
||||
}
|
||||
|
||||
export type ClassUpsertWithoutSpacedRepetitionSetsInput = {
|
||||
update: Prisma.XOR<Prisma.ClassUpdateWithoutSpacedRepetitionSetsInput, Prisma.ClassUncheckedUpdateWithoutSpacedRepetitionSetsInput>
|
||||
create: Prisma.XOR<Prisma.ClassCreateWithoutSpacedRepetitionSetsInput, Prisma.ClassUncheckedCreateWithoutSpacedRepetitionSetsInput>
|
||||
where?: Prisma.ClassWhereInput
|
||||
}
|
||||
|
||||
export type ClassUpdateToOneWithWhereWithoutSpacedRepetitionSetsInput = {
|
||||
where?: Prisma.ClassWhereInput
|
||||
data: Prisma.XOR<Prisma.ClassUpdateWithoutSpacedRepetitionSetsInput, Prisma.ClassUncheckedUpdateWithoutSpacedRepetitionSetsInput>
|
||||
}
|
||||
|
||||
export type ClassUpdateWithoutSpacedRepetitionSetsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
decks?: Prisma.DeckUpdateManyWithoutClassNestedInput
|
||||
quizSets?: Prisma.QuizSetUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
export type ClassUncheckedUpdateWithoutSpacedRepetitionSetsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
decks?: Prisma.DeckUncheckedUpdateManyWithoutClassNestedInput
|
||||
quizSets?: Prisma.QuizSetUncheckedUpdateManyWithoutClassNestedInput
|
||||
materialGroups?: Prisma.MaterialGroupUncheckedUpdateManyWithoutClassNestedInput
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -610,12 +703,14 @@ export type ClassCountOutputType = {
|
|||
decks: number
|
||||
quizSets: number
|
||||
materialGroups: number
|
||||
spacedRepetitionSets: number
|
||||
}
|
||||
|
||||
export type ClassCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
decks?: boolean | ClassCountOutputTypeCountDecksArgs
|
||||
quizSets?: boolean | ClassCountOutputTypeCountQuizSetsArgs
|
||||
materialGroups?: boolean | ClassCountOutputTypeCountMaterialGroupsArgs
|
||||
spacedRepetitionSets?: boolean | ClassCountOutputTypeCountSpacedRepetitionSetsArgs
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -649,6 +744,13 @@ export type ClassCountOutputTypeCountMaterialGroupsArgs<ExtArgs extends runtime.
|
|||
where?: Prisma.MaterialGroupWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* ClassCountOutputType without action
|
||||
*/
|
||||
export type ClassCountOutputTypeCountSpacedRepetitionSetsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.SpacedRepetitionSetWhereInput
|
||||
}
|
||||
|
||||
|
||||
export type ClassSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
|
|
@ -659,6 +761,7 @@ export type ClassSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||
decks?: boolean | Prisma.Class$decksArgs<ExtArgs>
|
||||
quizSets?: boolean | Prisma.Class$quizSetsArgs<ExtArgs>
|
||||
materialGroups?: boolean | Prisma.Class$materialGroupsArgs<ExtArgs>
|
||||
spacedRepetitionSets?: boolean | Prisma.Class$spacedRepetitionSetsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ClassCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["class"]>
|
||||
|
||||
|
|
@ -691,6 +794,7 @@ export type ClassInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||
decks?: boolean | Prisma.Class$decksArgs<ExtArgs>
|
||||
quizSets?: boolean | Prisma.Class$quizSetsArgs<ExtArgs>
|
||||
materialGroups?: boolean | Prisma.Class$materialGroupsArgs<ExtArgs>
|
||||
spacedRepetitionSets?: boolean | Prisma.Class$spacedRepetitionSetsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ClassCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type ClassIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||
|
|
@ -702,6 +806,7 @@ export type $ClassPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||
decks: Prisma.$DeckPayload<ExtArgs>[]
|
||||
quizSets: Prisma.$QuizSetPayload<ExtArgs>[]
|
||||
materialGroups: Prisma.$MaterialGroupPayload<ExtArgs>[]
|
||||
spacedRepetitionSets: Prisma.$SpacedRepetitionSetPayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
|
|
@ -1106,6 +1211,7 @@ export interface Prisma__ClassClient<T, Null = never, ExtArgs extends runtime.Ty
|
|||
decks<T extends Prisma.Class$decksArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Class$decksArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$DeckPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
quizSets<T extends Prisma.Class$quizSetsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Class$quizSetsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$QuizSetPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
materialGroups<T extends Prisma.Class$materialGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Class$materialGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MaterialGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
spacedRepetitionSets<T extends Prisma.Class$spacedRepetitionSetsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Class$spacedRepetitionSetsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SpacedRepetitionSetPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
|
|
@ -1602,6 +1708,30 @@ export type Class$materialGroupsArgs<ExtArgs extends runtime.Types.Extensions.In
|
|||
distinct?: Prisma.MaterialGroupScalarFieldEnum | Prisma.MaterialGroupScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Class.spacedRepetitionSets
|
||||
*/
|
||||
export type Class$spacedRepetitionSetsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the SpacedRepetitionSet
|
||||
*/
|
||||
select?: Prisma.SpacedRepetitionSetSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the SpacedRepetitionSet
|
||||
*/
|
||||
omit?: Prisma.SpacedRepetitionSetOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.SpacedRepetitionSetInclude<ExtArgs> | null
|
||||
where?: Prisma.SpacedRepetitionSetWhereInput
|
||||
orderBy?: Prisma.SpacedRepetitionSetOrderByWithRelationInput | Prisma.SpacedRepetitionSetOrderByWithRelationInput[]
|
||||
cursor?: Prisma.SpacedRepetitionSetWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.SpacedRepetitionSetScalarFieldEnum | Prisma.SpacedRepetitionSetScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Class without action
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ export type DeckWhereInput = {
|
|||
cards?: Prisma.FlashcardListRelationFilter
|
||||
shareLink?: Prisma.XOR<Prisma.ShareLinkNullableScalarRelationFilter, Prisma.ShareLinkWhereInput> | null
|
||||
progress?: Prisma.StudyProgressListRelationFilter
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckListRelationFilter
|
||||
}
|
||||
|
||||
export type DeckOrderByWithRelationInput = {
|
||||
|
|
@ -252,6 +253,7 @@ export type DeckOrderByWithRelationInput = {
|
|||
cards?: Prisma.FlashcardOrderByRelationAggregateInput
|
||||
shareLink?: Prisma.ShareLinkOrderByWithRelationInput
|
||||
progress?: Prisma.StudyProgressOrderByRelationAggregateInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckOrderByRelationAggregateInput
|
||||
}
|
||||
|
||||
export type DeckWhereUniqueInput = Prisma.AtLeast<{
|
||||
|
|
@ -270,6 +272,7 @@ export type DeckWhereUniqueInput = Prisma.AtLeast<{
|
|||
cards?: Prisma.FlashcardListRelationFilter
|
||||
shareLink?: Prisma.XOR<Prisma.ShareLinkNullableScalarRelationFilter, Prisma.ShareLinkWhereInput> | null
|
||||
progress?: Prisma.StudyProgressListRelationFilter
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckListRelationFilter
|
||||
}, "id">
|
||||
|
||||
export type DeckOrderByWithAggregationInput = {
|
||||
|
|
@ -311,6 +314,7 @@ export type DeckCreateInput = {
|
|||
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedCreateInput = {
|
||||
|
|
@ -324,6 +328,7 @@ export type DeckUncheckedCreateInput = {
|
|||
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUpdateInput = {
|
||||
|
|
@ -337,6 +342,7 @@ export type DeckUpdateInput = {
|
|||
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateInput = {
|
||||
|
|
@ -350,6 +356,7 @@ export type DeckUncheckedUpdateInput = {
|
|||
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckCreateManyInput = {
|
||||
|
|
@ -572,6 +579,20 @@ export type DeckUncheckedUpdateManyWithoutGroupNestedInput = {
|
|||
deleteMany?: Prisma.DeckScalarWhereInput | Prisma.DeckScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type DeckCreateNestedOneWithoutSpacedRepetitionMembershipsInput = {
|
||||
create?: Prisma.XOR<Prisma.DeckCreateWithoutSpacedRepetitionMembershipsInput, Prisma.DeckUncheckedCreateWithoutSpacedRepetitionMembershipsInput>
|
||||
connectOrCreate?: Prisma.DeckCreateOrConnectWithoutSpacedRepetitionMembershipsInput
|
||||
connect?: Prisma.DeckWhereUniqueInput
|
||||
}
|
||||
|
||||
export type DeckUpdateOneRequiredWithoutSpacedRepetitionMembershipsNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.DeckCreateWithoutSpacedRepetitionMembershipsInput, Prisma.DeckUncheckedCreateWithoutSpacedRepetitionMembershipsInput>
|
||||
connectOrCreate?: Prisma.DeckCreateOrConnectWithoutSpacedRepetitionMembershipsInput
|
||||
upsert?: Prisma.DeckUpsertWithoutSpacedRepetitionMembershipsInput
|
||||
connect?: Prisma.DeckWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.DeckUpdateToOneWithWhereWithoutSpacedRepetitionMembershipsInput, Prisma.DeckUpdateWithoutSpacedRepetitionMembershipsInput>, Prisma.DeckUncheckedUpdateWithoutSpacedRepetitionMembershipsInput>
|
||||
}
|
||||
|
||||
export type DeckCreateWithoutClassInput = {
|
||||
id?: string
|
||||
name: string
|
||||
|
|
@ -582,6 +603,7 @@ export type DeckCreateWithoutClassInput = {
|
|||
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedCreateWithoutClassInput = {
|
||||
|
|
@ -594,6 +616,7 @@ export type DeckUncheckedCreateWithoutClassInput = {
|
|||
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckCreateOrConnectWithoutClassInput = {
|
||||
|
|
@ -644,6 +667,7 @@ export type DeckCreateWithoutCardsInput = {
|
|||
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
|
||||
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedCreateWithoutCardsInput = {
|
||||
|
|
@ -656,6 +680,7 @@ export type DeckUncheckedCreateWithoutCardsInput = {
|
|||
groupId?: string | null
|
||||
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckCreateOrConnectWithoutCardsInput = {
|
||||
|
|
@ -684,6 +709,7 @@ export type DeckUpdateWithoutCardsInput = {
|
|||
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
|
||||
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateWithoutCardsInput = {
|
||||
|
|
@ -696,6 +722,7 @@ export type DeckUncheckedUpdateWithoutCardsInput = {
|
|||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckCreateWithoutProgressInput = {
|
||||
|
|
@ -708,6 +735,7 @@ export type DeckCreateWithoutProgressInput = {
|
|||
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
|
||||
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedCreateWithoutProgressInput = {
|
||||
|
|
@ -720,6 +748,7 @@ export type DeckUncheckedCreateWithoutProgressInput = {
|
|||
groupId?: string | null
|
||||
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckCreateOrConnectWithoutProgressInput = {
|
||||
|
|
@ -748,6 +777,7 @@ export type DeckUpdateWithoutProgressInput = {
|
|||
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
|
||||
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateWithoutProgressInput = {
|
||||
|
|
@ -760,6 +790,7 @@ export type DeckUncheckedUpdateWithoutProgressInput = {
|
|||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckCreateWithoutShareLinkInput = {
|
||||
|
|
@ -772,6 +803,7 @@ export type DeckCreateWithoutShareLinkInput = {
|
|||
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
|
||||
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedCreateWithoutShareLinkInput = {
|
||||
|
|
@ -784,6 +816,7 @@ export type DeckUncheckedCreateWithoutShareLinkInput = {
|
|||
groupId?: string | null
|
||||
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckCreateOrConnectWithoutShareLinkInput = {
|
||||
|
|
@ -812,6 +845,7 @@ export type DeckUpdateWithoutShareLinkInput = {
|
|||
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
|
||||
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateWithoutShareLinkInput = {
|
||||
|
|
@ -824,6 +858,7 @@ export type DeckUncheckedUpdateWithoutShareLinkInput = {
|
|||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckCreateWithoutGroupInput = {
|
||||
|
|
@ -836,6 +871,7 @@ export type DeckCreateWithoutGroupInput = {
|
|||
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedCreateWithoutGroupInput = {
|
||||
|
|
@ -848,6 +884,7 @@ export type DeckUncheckedCreateWithoutGroupInput = {
|
|||
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckCreateOrConnectWithoutGroupInput = {
|
||||
|
|
@ -875,6 +912,74 @@ export type DeckUpdateManyWithWhereWithoutGroupInput = {
|
|||
data: Prisma.XOR<Prisma.DeckUpdateManyMutationInput, Prisma.DeckUncheckedUpdateManyWithoutGroupInput>
|
||||
}
|
||||
|
||||
export type DeckCreateWithoutSpacedRepetitionMembershipsInput = {
|
||||
id?: string
|
||||
name: string
|
||||
description?: string | null
|
||||
sortOrder?: number
|
||||
createdAt?: Date | string
|
||||
class: Prisma.ClassCreateNestedOneWithoutDecksInput
|
||||
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
|
||||
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedCreateWithoutSpacedRepetitionMembershipsInput = {
|
||||
id?: string
|
||||
classId: string
|
||||
name: string
|
||||
description?: string | null
|
||||
sortOrder?: number
|
||||
createdAt?: Date | string
|
||||
groupId?: string | null
|
||||
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
|
||||
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
|
||||
}
|
||||
|
||||
export type DeckCreateOrConnectWithoutSpacedRepetitionMembershipsInput = {
|
||||
where: Prisma.DeckWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.DeckCreateWithoutSpacedRepetitionMembershipsInput, Prisma.DeckUncheckedCreateWithoutSpacedRepetitionMembershipsInput>
|
||||
}
|
||||
|
||||
export type DeckUpsertWithoutSpacedRepetitionMembershipsInput = {
|
||||
update: Prisma.XOR<Prisma.DeckUpdateWithoutSpacedRepetitionMembershipsInput, Prisma.DeckUncheckedUpdateWithoutSpacedRepetitionMembershipsInput>
|
||||
create: Prisma.XOR<Prisma.DeckCreateWithoutSpacedRepetitionMembershipsInput, Prisma.DeckUncheckedCreateWithoutSpacedRepetitionMembershipsInput>
|
||||
where?: Prisma.DeckWhereInput
|
||||
}
|
||||
|
||||
export type DeckUpdateToOneWithWhereWithoutSpacedRepetitionMembershipsInput = {
|
||||
where?: Prisma.DeckWhereInput
|
||||
data: Prisma.XOR<Prisma.DeckUpdateWithoutSpacedRepetitionMembershipsInput, Prisma.DeckUncheckedUpdateWithoutSpacedRepetitionMembershipsInput>
|
||||
}
|
||||
|
||||
export type DeckUpdateWithoutSpacedRepetitionMembershipsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
class?: Prisma.ClassUpdateOneRequiredWithoutDecksNestedInput
|
||||
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
|
||||
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateWithoutSpacedRepetitionMembershipsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
classId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckCreateManyClassInput = {
|
||||
id?: string
|
||||
name: string
|
||||
|
|
@ -894,6 +999,7 @@ export type DeckUpdateWithoutClassInput = {
|
|||
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateWithoutClassInput = {
|
||||
|
|
@ -906,6 +1012,7 @@ export type DeckUncheckedUpdateWithoutClassInput = {
|
|||
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateManyWithoutClassInput = {
|
||||
|
|
@ -936,6 +1043,7 @@ export type DeckUpdateWithoutGroupInput = {
|
|||
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateWithoutGroupInput = {
|
||||
|
|
@ -948,6 +1056,7 @@ export type DeckUncheckedUpdateWithoutGroupInput = {
|
|||
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
|
||||
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
|
||||
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
|
||||
spacedRepetitionMemberships?: Prisma.SpacedRepetitionSetDeckUncheckedUpdateManyWithoutDeckNestedInput
|
||||
}
|
||||
|
||||
export type DeckUncheckedUpdateManyWithoutGroupInput = {
|
||||
|
|
@ -967,11 +1076,13 @@ export type DeckUncheckedUpdateManyWithoutGroupInput = {
|
|||
export type DeckCountOutputType = {
|
||||
cards: number
|
||||
progress: number
|
||||
spacedRepetitionMemberships: number
|
||||
}
|
||||
|
||||
export type DeckCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
cards?: boolean | DeckCountOutputTypeCountCardsArgs
|
||||
progress?: boolean | DeckCountOutputTypeCountProgressArgs
|
||||
spacedRepetitionMemberships?: boolean | DeckCountOutputTypeCountSpacedRepetitionMembershipsArgs
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -998,6 +1109,13 @@ export type DeckCountOutputTypeCountProgressArgs<ExtArgs extends runtime.Types.E
|
|||
where?: Prisma.StudyProgressWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* DeckCountOutputType without action
|
||||
*/
|
||||
export type DeckCountOutputTypeCountSpacedRepetitionMembershipsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.SpacedRepetitionSetDeckWhereInput
|
||||
}
|
||||
|
||||
|
||||
export type DeckSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
|
|
@ -1012,6 +1130,7 @@ export type DeckSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||
cards?: boolean | Prisma.Deck$cardsArgs<ExtArgs>
|
||||
shareLink?: boolean | Prisma.Deck$shareLinkArgs<ExtArgs>
|
||||
progress?: boolean | Prisma.Deck$progressArgs<ExtArgs>
|
||||
spacedRepetitionMemberships?: boolean | Prisma.Deck$spacedRepetitionMembershipsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.DeckCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["deck"]>
|
||||
|
||||
|
|
@ -1056,6 +1175,7 @@ export type DeckInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||
cards?: boolean | Prisma.Deck$cardsArgs<ExtArgs>
|
||||
shareLink?: boolean | Prisma.Deck$shareLinkArgs<ExtArgs>
|
||||
progress?: boolean | Prisma.Deck$progressArgs<ExtArgs>
|
||||
spacedRepetitionMemberships?: boolean | Prisma.Deck$spacedRepetitionMembershipsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.DeckCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type DeckIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
|
|
@ -1075,6 +1195,7 @@ export type $DeckPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||
cards: Prisma.$FlashcardPayload<ExtArgs>[]
|
||||
shareLink: Prisma.$ShareLinkPayload<ExtArgs> | null
|
||||
progress: Prisma.$StudyProgressPayload<ExtArgs>[]
|
||||
spacedRepetitionMemberships: Prisma.$SpacedRepetitionSetDeckPayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
|
|
@ -1483,6 +1604,7 @@ export interface Prisma__DeckClient<T, Null = never, ExtArgs extends runtime.Typ
|
|||
cards<T extends Prisma.Deck$cardsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$cardsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$FlashcardPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
shareLink<T extends Prisma.Deck$shareLinkArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$shareLinkArgs<ExtArgs>>): Prisma.Prisma__ShareLinkClient<runtime.Types.Result.GetResult<Prisma.$ShareLinkPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
progress<T extends Prisma.Deck$progressArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$progressArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$StudyProgressPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
spacedRepetitionMemberships<T extends Prisma.Deck$spacedRepetitionMembershipsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$spacedRepetitionMembershipsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SpacedRepetitionSetDeckPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
|
|
@ -2003,6 +2125,30 @@ export type Deck$progressArgs<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||
distinct?: Prisma.StudyProgressScalarFieldEnum | Prisma.StudyProgressScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck.spacedRepetitionMemberships
|
||||
*/
|
||||
export type Deck$spacedRepetitionMembershipsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the SpacedRepetitionSetDeck
|
||||
*/
|
||||
select?: Prisma.SpacedRepetitionSetDeckSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the SpacedRepetitionSetDeck
|
||||
*/
|
||||
omit?: Prisma.SpacedRepetitionSetDeckOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.SpacedRepetitionSetDeckInclude<ExtArgs> | null
|
||||
where?: Prisma.SpacedRepetitionSetDeckWhereInput
|
||||
orderBy?: Prisma.SpacedRepetitionSetDeckOrderByWithRelationInput | Prisma.SpacedRepetitionSetDeckOrderByWithRelationInput[]
|
||||
cursor?: Prisma.SpacedRepetitionSetDeckWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.SpacedRepetitionSetDeckScalarFieldEnum | Prisma.SpacedRepetitionSetDeckScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck without action
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ export type FlashcardWhereInput = {
|
|||
back?: Prisma.StringFilter<"Flashcard"> | string
|
||||
sortOrder?: Prisma.IntFilter<"Flashcard"> | number
|
||||
deck?: Prisma.XOR<Prisma.DeckScalarRelationFilter, Prisma.DeckWhereInput>
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateListRelationFilter
|
||||
}
|
||||
|
||||
export type FlashcardOrderByWithRelationInput = {
|
||||
|
|
@ -226,6 +227,7 @@ export type FlashcardOrderByWithRelationInput = {
|
|||
back?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
deck?: Prisma.DeckOrderByWithRelationInput
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateOrderByRelationAggregateInput
|
||||
}
|
||||
|
||||
export type FlashcardWhereUniqueInput = Prisma.AtLeast<{
|
||||
|
|
@ -238,6 +240,7 @@ export type FlashcardWhereUniqueInput = Prisma.AtLeast<{
|
|||
back?: Prisma.StringFilter<"Flashcard"> | string
|
||||
sortOrder?: Prisma.IntFilter<"Flashcard"> | number
|
||||
deck?: Prisma.XOR<Prisma.DeckScalarRelationFilter, Prisma.DeckWhereInput>
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateListRelationFilter
|
||||
}, "id">
|
||||
|
||||
export type FlashcardOrderByWithAggregationInput = {
|
||||
|
|
@ -270,6 +273,7 @@ export type FlashcardCreateInput = {
|
|||
back: string
|
||||
sortOrder?: number
|
||||
deck: Prisma.DeckCreateNestedOneWithoutCardsInput
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateCreateNestedManyWithoutFlashcardInput
|
||||
}
|
||||
|
||||
export type FlashcardUncheckedCreateInput = {
|
||||
|
|
@ -278,6 +282,7 @@ export type FlashcardUncheckedCreateInput = {
|
|||
front: string
|
||||
back: string
|
||||
sortOrder?: number
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateUncheckedCreateNestedManyWithoutFlashcardInput
|
||||
}
|
||||
|
||||
export type FlashcardUpdateInput = {
|
||||
|
|
@ -286,6 +291,7 @@ export type FlashcardUpdateInput = {
|
|||
back?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
deck?: Prisma.DeckUpdateOneRequiredWithoutCardsNestedInput
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateUpdateManyWithoutFlashcardNestedInput
|
||||
}
|
||||
|
||||
export type FlashcardUncheckedUpdateInput = {
|
||||
|
|
@ -294,6 +300,7 @@ export type FlashcardUncheckedUpdateInput = {
|
|||
front?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
back?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateUncheckedUpdateManyWithoutFlashcardNestedInput
|
||||
}
|
||||
|
||||
export type FlashcardCreateManyInput = {
|
||||
|
|
@ -361,6 +368,11 @@ export type FlashcardSumOrderByAggregateInput = {
|
|||
sortOrder?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type FlashcardScalarRelationFilter = {
|
||||
is?: Prisma.FlashcardWhereInput
|
||||
isNot?: Prisma.FlashcardWhereInput
|
||||
}
|
||||
|
||||
export type FlashcardCreateNestedManyWithoutDeckInput = {
|
||||
create?: Prisma.XOR<Prisma.FlashcardCreateWithoutDeckInput, Prisma.FlashcardUncheckedCreateWithoutDeckInput> | Prisma.FlashcardCreateWithoutDeckInput[] | Prisma.FlashcardUncheckedCreateWithoutDeckInput[]
|
||||
connectOrCreate?: Prisma.FlashcardCreateOrConnectWithoutDeckInput | Prisma.FlashcardCreateOrConnectWithoutDeckInput[]
|
||||
|
|
@ -403,11 +415,26 @@ export type FlashcardUncheckedUpdateManyWithoutDeckNestedInput = {
|
|||
deleteMany?: Prisma.FlashcardScalarWhereInput | Prisma.FlashcardScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type FlashcardCreateNestedOneWithoutSpacedRepetitionStatesInput = {
|
||||
create?: Prisma.XOR<Prisma.FlashcardCreateWithoutSpacedRepetitionStatesInput, Prisma.FlashcardUncheckedCreateWithoutSpacedRepetitionStatesInput>
|
||||
connectOrCreate?: Prisma.FlashcardCreateOrConnectWithoutSpacedRepetitionStatesInput
|
||||
connect?: Prisma.FlashcardWhereUniqueInput
|
||||
}
|
||||
|
||||
export type FlashcardUpdateOneRequiredWithoutSpacedRepetitionStatesNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.FlashcardCreateWithoutSpacedRepetitionStatesInput, Prisma.FlashcardUncheckedCreateWithoutSpacedRepetitionStatesInput>
|
||||
connectOrCreate?: Prisma.FlashcardCreateOrConnectWithoutSpacedRepetitionStatesInput
|
||||
upsert?: Prisma.FlashcardUpsertWithoutSpacedRepetitionStatesInput
|
||||
connect?: Prisma.FlashcardWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.FlashcardUpdateToOneWithWhereWithoutSpacedRepetitionStatesInput, Prisma.FlashcardUpdateWithoutSpacedRepetitionStatesInput>, Prisma.FlashcardUncheckedUpdateWithoutSpacedRepetitionStatesInput>
|
||||
}
|
||||
|
||||
export type FlashcardCreateWithoutDeckInput = {
|
||||
id?: string
|
||||
front: string
|
||||
back: string
|
||||
sortOrder?: number
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateCreateNestedManyWithoutFlashcardInput
|
||||
}
|
||||
|
||||
export type FlashcardUncheckedCreateWithoutDeckInput = {
|
||||
|
|
@ -415,6 +442,7 @@ export type FlashcardUncheckedCreateWithoutDeckInput = {
|
|||
front: string
|
||||
back: string
|
||||
sortOrder?: number
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateUncheckedCreateNestedManyWithoutFlashcardInput
|
||||
}
|
||||
|
||||
export type FlashcardCreateOrConnectWithoutDeckInput = {
|
||||
|
|
@ -453,6 +481,54 @@ export type FlashcardScalarWhereInput = {
|
|||
sortOrder?: Prisma.IntFilter<"Flashcard"> | number
|
||||
}
|
||||
|
||||
export type FlashcardCreateWithoutSpacedRepetitionStatesInput = {
|
||||
id?: string
|
||||
front: string
|
||||
back: string
|
||||
sortOrder?: number
|
||||
deck: Prisma.DeckCreateNestedOneWithoutCardsInput
|
||||
}
|
||||
|
||||
export type FlashcardUncheckedCreateWithoutSpacedRepetitionStatesInput = {
|
||||
id?: string
|
||||
deckId: string
|
||||
front: string
|
||||
back: string
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export type FlashcardCreateOrConnectWithoutSpacedRepetitionStatesInput = {
|
||||
where: Prisma.FlashcardWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.FlashcardCreateWithoutSpacedRepetitionStatesInput, Prisma.FlashcardUncheckedCreateWithoutSpacedRepetitionStatesInput>
|
||||
}
|
||||
|
||||
export type FlashcardUpsertWithoutSpacedRepetitionStatesInput = {
|
||||
update: Prisma.XOR<Prisma.FlashcardUpdateWithoutSpacedRepetitionStatesInput, Prisma.FlashcardUncheckedUpdateWithoutSpacedRepetitionStatesInput>
|
||||
create: Prisma.XOR<Prisma.FlashcardCreateWithoutSpacedRepetitionStatesInput, Prisma.FlashcardUncheckedCreateWithoutSpacedRepetitionStatesInput>
|
||||
where?: Prisma.FlashcardWhereInput
|
||||
}
|
||||
|
||||
export type FlashcardUpdateToOneWithWhereWithoutSpacedRepetitionStatesInput = {
|
||||
where?: Prisma.FlashcardWhereInput
|
||||
data: Prisma.XOR<Prisma.FlashcardUpdateWithoutSpacedRepetitionStatesInput, Prisma.FlashcardUncheckedUpdateWithoutSpacedRepetitionStatesInput>
|
||||
}
|
||||
|
||||
export type FlashcardUpdateWithoutSpacedRepetitionStatesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
front?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
back?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
deck?: Prisma.DeckUpdateOneRequiredWithoutCardsNestedInput
|
||||
}
|
||||
|
||||
export type FlashcardUncheckedUpdateWithoutSpacedRepetitionStatesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
deckId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
front?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
back?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
export type FlashcardCreateManyDeckInput = {
|
||||
id?: string
|
||||
front: string
|
||||
|
|
@ -465,6 +541,7 @@ export type FlashcardUpdateWithoutDeckInput = {
|
|||
front?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
back?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateUpdateManyWithoutFlashcardNestedInput
|
||||
}
|
||||
|
||||
export type FlashcardUncheckedUpdateWithoutDeckInput = {
|
||||
|
|
@ -472,6 +549,7 @@ export type FlashcardUncheckedUpdateWithoutDeckInput = {
|
|||
front?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
back?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
spacedRepetitionStates?: Prisma.SpacedRepetitionCardStateUncheckedUpdateManyWithoutFlashcardNestedInput
|
||||
}
|
||||
|
||||
export type FlashcardUncheckedUpdateManyWithoutDeckInput = {
|
||||
|
|
@ -482,6 +560,35 @@ export type FlashcardUncheckedUpdateManyWithoutDeckInput = {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Count Type FlashcardCountOutputType
|
||||
*/
|
||||
|
||||
export type FlashcardCountOutputType = {
|
||||
spacedRepetitionStates: number
|
||||
}
|
||||
|
||||
export type FlashcardCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
spacedRepetitionStates?: boolean | FlashcardCountOutputTypeCountSpacedRepetitionStatesArgs
|
||||
}
|
||||
|
||||
/**
|
||||
* FlashcardCountOutputType without action
|
||||
*/
|
||||
export type FlashcardCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the FlashcardCountOutputType
|
||||
*/
|
||||
select?: Prisma.FlashcardCountOutputTypeSelect<ExtArgs> | null
|
||||
}
|
||||
|
||||
/**
|
||||
* FlashcardCountOutputType without action
|
||||
*/
|
||||
export type FlashcardCountOutputTypeCountSpacedRepetitionStatesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.SpacedRepetitionCardStateWhereInput
|
||||
}
|
||||
|
||||
|
||||
export type FlashcardSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
|
|
@ -490,6 +597,8 @@ export type FlashcardSelect<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||
back?: boolean
|
||||
sortOrder?: boolean
|
||||
deck?: boolean | Prisma.DeckDefaultArgs<ExtArgs>
|
||||
spacedRepetitionStates?: boolean | Prisma.Flashcard$spacedRepetitionStatesArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.FlashcardCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["flashcard"]>
|
||||
|
||||
export type FlashcardSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
|
|
@ -521,6 +630,8 @@ export type FlashcardSelectScalar = {
|
|||
export type FlashcardOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "deckId" | "front" | "back" | "sortOrder", ExtArgs["result"]["flashcard"]>
|
||||
export type FlashcardInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
deck?: boolean | Prisma.DeckDefaultArgs<ExtArgs>
|
||||
spacedRepetitionStates?: boolean | Prisma.Flashcard$spacedRepetitionStatesArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.FlashcardCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type FlashcardIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
deck?: boolean | Prisma.DeckDefaultArgs<ExtArgs>
|
||||
|
|
@ -533,6 +644,7 @@ export type $FlashcardPayload<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||
name: "Flashcard"
|
||||
objects: {
|
||||
deck: Prisma.$DeckPayload<ExtArgs>
|
||||
spacedRepetitionStates: Prisma.$SpacedRepetitionCardStatePayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
|
|
@ -935,6 +1047,7 @@ readonly fields: FlashcardFieldRefs;
|
|||
export interface Prisma__FlashcardClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
deck<T extends Prisma.DeckDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.DeckDefaultArgs<ExtArgs>>): Prisma.Prisma__DeckClient<runtime.Types.Result.GetResult<Prisma.$DeckPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
spacedRepetitionStates<T extends Prisma.Flashcard$spacedRepetitionStatesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Flashcard$spacedRepetitionStatesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SpacedRepetitionCardStatePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
|
|
@ -1367,6 +1480,30 @@ export type FlashcardDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Int
|
|||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Flashcard.spacedRepetitionStates
|
||||
*/
|
||||
export type Flashcard$spacedRepetitionStatesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the SpacedRepetitionCardState
|
||||
*/
|
||||
select?: Prisma.SpacedRepetitionCardStateSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the SpacedRepetitionCardState
|
||||
*/
|
||||
omit?: Prisma.SpacedRepetitionCardStateOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.SpacedRepetitionCardStateInclude<ExtArgs> | null
|
||||
where?: Prisma.SpacedRepetitionCardStateWhereInput
|
||||
orderBy?: Prisma.SpacedRepetitionCardStateOrderByWithRelationInput | Prisma.SpacedRepetitionCardStateOrderByWithRelationInput[]
|
||||
cursor?: Prisma.SpacedRepetitionCardStateWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.SpacedRepetitionCardStateScalarFieldEnum | Prisma.SpacedRepetitionCardStateScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Flashcard without action
|
||||
*/
|
||||
|
|
|
|||
2004
src/generated/prisma/models/SpacedRepetitionCardState.ts
Normal file
2004
src/generated/prisma/models/SpacedRepetitionCardState.ts
Normal file
File diff suppressed because it is too large
Load diff
1776
src/generated/prisma/models/SpacedRepetitionSet.ts
Normal file
1776
src/generated/prisma/models/SpacedRepetitionSet.ts
Normal file
File diff suppressed because it is too large
Load diff
1470
src/generated/prisma/models/SpacedRepetitionSetDeck.ts
Normal file
1470
src/generated/prisma/models/SpacedRepetitionSetDeck.ts
Normal file
File diff suppressed because it is too large
Load diff
121
src/lib/spacedRepetition.test.ts
Normal file
121
src/lib/spacedRepetition.test.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
calculateQueueCounts,
|
||||
getArizonaDayBounds,
|
||||
getEffectiveDue,
|
||||
getLearnAheadCutoff,
|
||||
previewRatings,
|
||||
scheduleRating,
|
||||
type SpacedRepetitionRating,
|
||||
} from "@/lib/spacedRepetition";
|
||||
|
||||
describe("spaced repetition scheduling", () => {
|
||||
const now = new Date("2026-07-13T20:00:00.000Z");
|
||||
|
||||
it("returns a distinct valid state for every rating", () => {
|
||||
const ratings: SpacedRepetitionRating[] = ["AGAIN", "HARD", "GOOD", "EASY"];
|
||||
for (const rating of ratings) {
|
||||
const state = scheduleRating(null, rating, now);
|
||||
expect(state.reps).toBe(1);
|
||||
expect(state.lastReview?.toISOString()).toBe(now.toISOString());
|
||||
expect(state.due.getTime()).toBeGreaterThan(now.getTime());
|
||||
}
|
||||
});
|
||||
|
||||
it("orders first-review previews from Again through Easy", () => {
|
||||
const previews = previewRatings(null, now);
|
||||
const dueTimes = previews.map((preview) => new Date(preview.due).getTime());
|
||||
expect(dueTimes).toHaveLength(4);
|
||||
expect(dueTimes[0]).toBeLessThan(dueTimes[1]);
|
||||
expect(dueTimes[1]).toBeLessThanOrEqual(dueTimes[2]);
|
||||
expect(dueTimes[2]).toBeLessThan(dueTimes[3]);
|
||||
expect(dueTimes.map((due) => Math.round((due - now.getTime()) / 60000))).toEqual([
|
||||
1,
|
||||
6,
|
||||
10,
|
||||
7 * 24 * 60 + 11 * 60,
|
||||
]);
|
||||
expect(new Date(previews[3].due).toISOString()).toBe(
|
||||
"2026-07-21T07:00:00.000Z"
|
||||
);
|
||||
expect(previews[3].scheduledDays).toBe(8);
|
||||
});
|
||||
|
||||
it("aligns graduated day intervals to Arizona midnight", () => {
|
||||
const learning = scheduleRating(null, "GOOD", now);
|
||||
const graduated = scheduleRating(learning, "GOOD", learning.due);
|
||||
|
||||
expect(graduated.scheduledDays).toBeGreaterThanOrEqual(1);
|
||||
expect(graduated.due.toISOString()).toMatch(/T07:00:00\.000Z$/);
|
||||
});
|
||||
|
||||
it("uses Anki's 20-minute learn-ahead window", () => {
|
||||
expect(getLearnAheadCutoff(now).toISOString()).toBe("2026-07-13T20:20:00.000Z");
|
||||
});
|
||||
|
||||
it("uses midnight in America/Phoenix as the daily boundary", () => {
|
||||
const beforeMidnight = getArizonaDayBounds(new Date("2026-07-14T06:59:59.000Z"));
|
||||
const afterMidnight = getArizonaDayBounds(new Date("2026-07-14T07:00:00.000Z"));
|
||||
expect(beforeMidnight.start.toISOString()).toBe("2026-07-13T07:00:00.000Z");
|
||||
expect(afterMidnight.start.toISOString()).toBe("2026-07-14T07:00:00.000Z");
|
||||
});
|
||||
|
||||
it("treats legacy day-based due times as due at midnight", () => {
|
||||
expect(getEffectiveDue({
|
||||
due: new Date("2026-07-14T20:00:00.000Z"),
|
||||
scheduledDays: 1,
|
||||
}).toISOString()).toBe("2026-07-14T07:00:00.000Z");
|
||||
expect(getEffectiveDue({
|
||||
due: new Date("2026-07-14T07:10:00.000Z"),
|
||||
scheduledDays: 0,
|
||||
}).toISOString()).toBe("2026-07-14T07:10:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("daily queue counts", () => {
|
||||
it("adds all due cards while capping only new cards", () => {
|
||||
expect(calculateQueueCounts({
|
||||
totalCards: 100,
|
||||
reviewedCards: 40,
|
||||
dueCards: 55,
|
||||
introducedToday: 12,
|
||||
newCardsPerDay: 30,
|
||||
})).toEqual({ unseenCards: 60, newCardsAvailable: 18, learningAheadCards: 0, availableCards: 73 });
|
||||
});
|
||||
|
||||
it("can pause new cards without hiding due reviews", () => {
|
||||
expect(calculateQueueCounts({
|
||||
totalCards: 100,
|
||||
reviewedCards: 40,
|
||||
dueCards: 7,
|
||||
introducedToday: 0,
|
||||
newCardsPerDay: 0,
|
||||
})).toEqual({ unseenCards: 60, newCardsAvailable: 0, learningAheadCards: 0, availableCards: 7 });
|
||||
});
|
||||
|
||||
it("never offers more new cards than remain unseen", () => {
|
||||
expect(calculateQueueCounts({
|
||||
totalCards: 12,
|
||||
reviewedCards: 10,
|
||||
dueCards: 0,
|
||||
introducedToday: 0,
|
||||
newCardsPerDay: 30,
|
||||
}).newCardsAvailable).toBe(2);
|
||||
});
|
||||
|
||||
it("keeps short learning steps available when no new cards remain", () => {
|
||||
expect(calculateQueueCounts({
|
||||
totalCards: 25,
|
||||
reviewedCards: 25,
|
||||
dueCards: 0,
|
||||
learningAheadCards: 25,
|
||||
introducedToday: 25,
|
||||
newCardsPerDay: 30,
|
||||
})).toEqual({
|
||||
unseenCards: 0,
|
||||
newCardsAvailable: 0,
|
||||
learningAheadCards: 25,
|
||||
availableCards: 25,
|
||||
});
|
||||
});
|
||||
});
|
||||
162
src/lib/spacedRepetition.ts
Normal file
162
src/lib/spacedRepetition.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import {
|
||||
createEmptyCard,
|
||||
fsrs,
|
||||
Rating,
|
||||
State,
|
||||
type Card,
|
||||
type Grade,
|
||||
} from "ts-fsrs";
|
||||
|
||||
export const SPACED_REPETITION_RATINGS = [
|
||||
"AGAIN",
|
||||
"HARD",
|
||||
"GOOD",
|
||||
"EASY",
|
||||
] as const;
|
||||
|
||||
export type SpacedRepetitionRating =
|
||||
(typeof SPACED_REPETITION_RATINGS)[number];
|
||||
|
||||
export interface StoredScheduleState {
|
||||
due: Date;
|
||||
stability: number;
|
||||
difficulty: number;
|
||||
elapsedDays: number;
|
||||
scheduledDays: number;
|
||||
learningSteps: number;
|
||||
reps: number;
|
||||
lapses: number;
|
||||
state: number;
|
||||
lastReview: Date | null;
|
||||
}
|
||||
|
||||
export interface RatingPreview {
|
||||
rating: SpacedRepetitionRating;
|
||||
due: string;
|
||||
scheduledDays: number;
|
||||
}
|
||||
|
||||
export const LEARN_AHEAD_LIMIT_MINUTES = 20;
|
||||
export const INTRADAY_LEARNING_STATES: number[] = [
|
||||
State.Learning,
|
||||
State.Relearning,
|
||||
];
|
||||
|
||||
export function getLearnAheadCutoff(now: Date) {
|
||||
return new Date(now.getTime() + LEARN_AHEAD_LIMIT_MINUTES * 60 * 1000);
|
||||
}
|
||||
|
||||
export function getEffectiveDue(state: { due: Date; scheduledDays: number }) {
|
||||
return state.scheduledDays >= 1
|
||||
? getArizonaDayBounds(state.due).start
|
||||
: state.due;
|
||||
}
|
||||
|
||||
export function calculateQueueCounts(input: {
|
||||
totalCards: number;
|
||||
reviewedCards: number;
|
||||
dueCards: number;
|
||||
learningAheadCards?: number;
|
||||
introducedToday: number;
|
||||
newCardsPerDay: number;
|
||||
}) {
|
||||
const unseenCards = Math.max(0, input.totalCards - input.reviewedCards);
|
||||
const newCardsAvailable = Math.min(
|
||||
unseenCards,
|
||||
Math.max(0, input.newCardsPerDay - input.introducedToday)
|
||||
);
|
||||
return {
|
||||
unseenCards,
|
||||
newCardsAvailable,
|
||||
learningAheadCards: input.learningAheadCards ?? 0,
|
||||
availableCards:
|
||||
input.dueCards + newCardsAvailable + (input.learningAheadCards ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
const scheduler = fsrs({ request_retention: 0.9, enable_fuzz: false });
|
||||
|
||||
const ratingMap: Record<SpacedRepetitionRating, Grade> = {
|
||||
AGAIN: Rating.Again,
|
||||
HARD: Rating.Hard,
|
||||
GOOD: Rating.Good,
|
||||
EASY: Rating.Easy,
|
||||
};
|
||||
|
||||
function toCard(state: StoredScheduleState | null, now: Date): Card {
|
||||
if (!state) return createEmptyCard(now);
|
||||
|
||||
return {
|
||||
due: state.due,
|
||||
stability: state.stability,
|
||||
difficulty: state.difficulty,
|
||||
elapsed_days: state.elapsedDays,
|
||||
scheduled_days: state.scheduledDays,
|
||||
learning_steps: state.learningSteps,
|
||||
reps: state.reps,
|
||||
lapses: state.lapses,
|
||||
state: state.state as State,
|
||||
last_review: state.lastReview ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function fromCard(card: Card): StoredScheduleState {
|
||||
return {
|
||||
due: card.due,
|
||||
stability: card.stability,
|
||||
difficulty: card.difficulty,
|
||||
elapsedDays: card.elapsed_days,
|
||||
scheduledDays: card.scheduled_days,
|
||||
learningSteps: card.learning_steps,
|
||||
reps: card.reps,
|
||||
lapses: card.lapses,
|
||||
state: card.state,
|
||||
lastReview: card.last_review ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function alignDayIntervalToArizonaMidnight(card: Card, now: Date): Card {
|
||||
if (card.scheduled_days < 1) return card;
|
||||
|
||||
const { start } = getArizonaDayBounds(now);
|
||||
return {
|
||||
...card,
|
||||
due: new Date(
|
||||
start.getTime() + card.scheduled_days * 24 * 60 * 60 * 1000
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function previewRatings(
|
||||
state: StoredScheduleState | null,
|
||||
now: Date
|
||||
): RatingPreview[] {
|
||||
const preview = scheduler.repeat(toCard(state, now), now);
|
||||
return SPACED_REPETITION_RATINGS.map((rating) => {
|
||||
const card = alignDayIntervalToArizonaMidnight(
|
||||
preview[ratingMap[rating]].card,
|
||||
now
|
||||
);
|
||||
return {
|
||||
rating,
|
||||
due: card.due.toISOString(),
|
||||
scheduledDays: card.scheduled_days,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function scheduleRating(
|
||||
state: StoredScheduleState | null,
|
||||
rating: SpacedRepetitionRating,
|
||||
now: Date
|
||||
): StoredScheduleState {
|
||||
const next = scheduler.next(toCard(state, now), now, ratingMap[rating]).card;
|
||||
return fromCard(alignDayIntervalToArizonaMidnight(next, now));
|
||||
}
|
||||
|
||||
export function getArizonaDayBounds(now: Date): { start: Date; end: Date } {
|
||||
const arizonaNow = new Date(now.getTime() - 7 * 60 * 60 * 1000);
|
||||
const dateKey = arizonaNow.toISOString().slice(0, 10);
|
||||
const start = new Date(`${dateKey}T07:00:00.000Z`);
|
||||
return { start, end: new Date(start.getTime() + 24 * 60 * 60 * 1000) };
|
||||
}
|
||||
9
src/lib/spacedRepetitionApi.ts
Normal file
9
src/lib/spacedRepetitionApi.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { SpacedRepetitionServiceError } from "@/services/spacedRepetitionService";
|
||||
|
||||
export function spacedRepetitionErrorResponse(error: unknown) {
|
||||
if (error instanceof SpacedRepetitionServiceError) {
|
||||
return NextResponse.json({ error: error.message }, { status: error.status });
|
||||
}
|
||||
return NextResponse.json({ error: "Spaced repetition request failed" }, { status: 500 });
|
||||
}
|
||||
44
src/lib/validation/spacedRepetitionSchemas.ts
Normal file
44
src/lib/validation/spacedRepetitionSchemas.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { z } from "zod";
|
||||
import { SPACED_REPETITION_RATINGS } from "@/lib/spacedRepetition";
|
||||
|
||||
const nameSchema = z.string().trim().min(1).max(120);
|
||||
const descriptionSchema = z.string().trim().max(5000).nullable().optional();
|
||||
const newCardsPerDaySchema = z.number().int().min(0).max(999);
|
||||
|
||||
export const createSpacedRepetitionSetSchema = z.object({
|
||||
classId: z.string().min(1),
|
||||
name: nameSchema,
|
||||
description: descriptionSchema,
|
||||
newCardsPerDay: newCardsPerDaySchema.default(30),
|
||||
});
|
||||
|
||||
export const updateSpacedRepetitionSetSchema = z
|
||||
.object({
|
||||
name: nameSchema.optional(),
|
||||
description: descriptionSchema,
|
||||
newCardsPerDay: newCardsPerDaySchema.optional(),
|
||||
})
|
||||
.refine((value) => Object.values(value).some((item) => item !== undefined), {
|
||||
message: "At least one field is required",
|
||||
});
|
||||
|
||||
export const addSpacedRepetitionDeckSchema = z.object({
|
||||
deckId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const reorderSpacedRepetitionDecksSchema = z.object({
|
||||
deckIds: z.array(z.string().min(1)).max(1000),
|
||||
});
|
||||
|
||||
export const reviewSpacedRepetitionCardSchema = z.object({
|
||||
flashcardId: z.string().min(1),
|
||||
rating: z.enum(SPACED_REPETITION_RATINGS),
|
||||
expectedStateVersion: z.string().datetime().nullable(),
|
||||
});
|
||||
|
||||
export type CreateSpacedRepetitionSetInput = z.infer<
|
||||
typeof createSpacedRepetitionSetSchema
|
||||
>;
|
||||
export type UpdateSpacedRepetitionSetInput = z.infer<
|
||||
typeof updateSpacedRepetitionSetSchema
|
||||
>;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { prisma } from "@/lib/db";
|
||||
import { getStudyAvailabilityByClass } from "@/services/spacedRepetitionService";
|
||||
|
||||
function slugify(name: string): string {
|
||||
return name
|
||||
|
|
@ -8,13 +9,27 @@ function slugify(name: string): string {
|
|||
}
|
||||
|
||||
export async function listClasses() {
|
||||
return prisma.class.findMany({
|
||||
const [classes, studyAvailability] = await Promise.all([
|
||||
prisma.class.findMany({
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: {
|
||||
_count: {
|
||||
select: { decks: true, quizSets: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
getStudyAvailabilityByClass(),
|
||||
]);
|
||||
|
||||
return classes.map((classItem) => {
|
||||
const availability = studyAvailability[classItem.id];
|
||||
return {
|
||||
...classItem,
|
||||
spacedRepetitionDueCards: availability?.dueCards ?? 0,
|
||||
spacedRepetitionLearningCards: availability?.learningCards ?? 0,
|
||||
spacedRepetitionNewCards: availability?.newCards ?? 0,
|
||||
spacedRepetitionReadyCards: availability?.readyCards ?? 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
448
src/services/spacedRepetitionService.ts
Normal file
448
src/services/spacedRepetitionService.ts
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
calculateQueueCounts,
|
||||
getArizonaDayBounds,
|
||||
getEffectiveDue,
|
||||
getLearnAheadCutoff,
|
||||
INTRADAY_LEARNING_STATES,
|
||||
previewRatings,
|
||||
scheduleRating,
|
||||
type SpacedRepetitionRating,
|
||||
type StoredScheduleState,
|
||||
} from "@/lib/spacedRepetition";
|
||||
import type {
|
||||
CreateSpacedRepetitionSetInput,
|
||||
UpdateSpacedRepetitionSetInput,
|
||||
} from "@/lib/validation/spacedRepetitionSchemas";
|
||||
|
||||
export class SpacedRepetitionServiceError extends Error {
|
||||
constructor(message: string, public status: 400 | 404 | 409) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const cardStateSelect = {
|
||||
id: true,
|
||||
due: true,
|
||||
stability: true,
|
||||
difficulty: true,
|
||||
elapsedDays: true,
|
||||
scheduledDays: true,
|
||||
learningSteps: true,
|
||||
reps: true,
|
||||
lapses: true,
|
||||
state: true,
|
||||
lastReview: true,
|
||||
updatedAt: true,
|
||||
} as const;
|
||||
|
||||
function asStoredState(state: {
|
||||
due: Date;
|
||||
stability: number;
|
||||
difficulty: number;
|
||||
elapsedDays: number;
|
||||
scheduledDays: number;
|
||||
learningSteps: number;
|
||||
reps: number;
|
||||
lapses: number;
|
||||
state: number;
|
||||
lastReview: Date | null;
|
||||
}): StoredScheduleState {
|
||||
return state;
|
||||
}
|
||||
|
||||
async function getSetSummary(setId: string, now: Date) {
|
||||
const { start, end } = getArizonaDayBounds(now);
|
||||
const set = await prisma.spacedRepetitionSet.findUnique({
|
||||
where: { id: setId },
|
||||
include: {
|
||||
class: { select: { id: true, slug: true, name: true } },
|
||||
decks: {
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: {
|
||||
deck: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
_count: { select: { cards: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cardStates: {
|
||||
select: {
|
||||
due: true,
|
||||
firstReviewedAt: true,
|
||||
scheduledDays: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!set) throw new SpacedRepetitionServiceError("Repetition set not found", 404);
|
||||
|
||||
const totalCards = set.decks.reduce(
|
||||
(sum, membership) => sum + membership.deck._count.cards,
|
||||
0
|
||||
);
|
||||
const reviewedCards = set.cardStates.length;
|
||||
const dueCards = set.cardStates.filter(
|
||||
(state) => getEffectiveDue(state) <= now
|
||||
).length;
|
||||
const learnAheadCutoff = getLearnAheadCutoff(now);
|
||||
const learningAheadCards = set.cardStates.filter(
|
||||
(state) =>
|
||||
getEffectiveDue(state) > now &&
|
||||
getEffectiveDue(state) <= learnAheadCutoff &&
|
||||
INTRADAY_LEARNING_STATES.includes(state.state)
|
||||
).length;
|
||||
const introducedToday = set.cardStates.filter(
|
||||
(state) => state.firstReviewedAt >= start && state.firstReviewedAt < end
|
||||
).length;
|
||||
const { unseenCards, newCardsAvailable, availableCards } = calculateQueueCounts({
|
||||
totalCards,
|
||||
reviewedCards,
|
||||
dueCards,
|
||||
learningAheadCards,
|
||||
introducedToday,
|
||||
newCardsPerDay: set.newCardsPerDay,
|
||||
});
|
||||
const nextDue = set.cardStates
|
||||
.map((state) => getEffectiveDue(state))
|
||||
.filter((due) => due > now)
|
||||
.sort((a, b) => a.getTime() - b.getTime())[0];
|
||||
|
||||
return {
|
||||
id: set.id,
|
||||
classId: set.classId,
|
||||
name: set.name,
|
||||
description: set.description,
|
||||
newCardsPerDay: set.newCardsPerDay,
|
||||
sortOrder: set.sortOrder,
|
||||
createdAt: set.createdAt,
|
||||
updatedAt: set.updatedAt,
|
||||
class: set.class,
|
||||
decks: set.decks.map((membership) => ({
|
||||
...membership.deck,
|
||||
sortOrder: membership.sortOrder,
|
||||
})),
|
||||
stats: {
|
||||
totalCards,
|
||||
reviewedCards,
|
||||
unseenCards,
|
||||
dueCards,
|
||||
learningAheadCards,
|
||||
newCardsAvailable,
|
||||
availableCards,
|
||||
nextDue: nextDue?.toISOString() ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSetsByClass(classId: string, now = new Date()) {
|
||||
const ids = await prisma.spacedRepetitionSet.findMany({
|
||||
where: { classId },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
select: { id: true },
|
||||
});
|
||||
return Promise.all(ids.map(({ id }) => getSetSummary(id, now)));
|
||||
}
|
||||
|
||||
export async function getStudyAvailabilityByClass(now = new Date()) {
|
||||
const { start, end } = getArizonaDayBounds(now);
|
||||
const learnAheadCutoff = getLearnAheadCutoff(now);
|
||||
const sets = await prisma.spacedRepetitionSet.findMany({
|
||||
select: {
|
||||
classId: true,
|
||||
newCardsPerDay: true,
|
||||
decks: {
|
||||
select: {
|
||||
deck: { select: { _count: { select: { cards: true } } } },
|
||||
},
|
||||
},
|
||||
cardStates: {
|
||||
select: {
|
||||
due: true,
|
||||
firstReviewedAt: true,
|
||||
scheduledDays: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return sets.reduce<Record<string, {
|
||||
dueCards: number;
|
||||
learningCards: number;
|
||||
newCards: number;
|
||||
readyCards: number;
|
||||
}>>((availability, set) => {
|
||||
const totalCards = set.decks.reduce(
|
||||
(sum, membership) => sum + membership.deck._count.cards,
|
||||
0
|
||||
);
|
||||
const dueCards = set.cardStates.filter(
|
||||
(state) => getEffectiveDue(state) <= now
|
||||
).length;
|
||||
const learningCards = set.cardStates.filter((state) => {
|
||||
const effectiveDue = getEffectiveDue(state);
|
||||
return effectiveDue > now &&
|
||||
effectiveDue <= learnAheadCutoff &&
|
||||
INTRADAY_LEARNING_STATES.includes(state.state);
|
||||
}).length;
|
||||
const introducedToday = set.cardStates.filter(
|
||||
(state) => state.firstReviewedAt >= start && state.firstReviewedAt < end
|
||||
).length;
|
||||
const counts = calculateQueueCounts({
|
||||
totalCards,
|
||||
reviewedCards: set.cardStates.length,
|
||||
dueCards,
|
||||
learningAheadCards: learningCards,
|
||||
introducedToday,
|
||||
newCardsPerDay: set.newCardsPerDay,
|
||||
});
|
||||
const current = availability[set.classId] ?? {
|
||||
dueCards: 0,
|
||||
learningCards: 0,
|
||||
newCards: 0,
|
||||
readyCards: 0,
|
||||
};
|
||||
availability[set.classId] = {
|
||||
dueCards: current.dueCards + dueCards,
|
||||
learningCards: current.learningCards + learningCards,
|
||||
newCards: current.newCards + counts.newCardsAvailable,
|
||||
readyCards: current.readyCards + counts.availableCards,
|
||||
};
|
||||
return availability;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export async function createSet(data: CreateSpacedRepetitionSetInput) {
|
||||
const classExists = await prisma.class.findUnique({
|
||||
where: { id: data.classId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!classExists) throw new SpacedRepetitionServiceError("Class not found", 404);
|
||||
|
||||
const maximum = await prisma.spacedRepetitionSet.aggregate({
|
||||
where: { classId: data.classId },
|
||||
_max: { sortOrder: true },
|
||||
});
|
||||
return prisma.spacedRepetitionSet.create({
|
||||
data: {
|
||||
classId: data.classId,
|
||||
name: data.name,
|
||||
description: data.description || null,
|
||||
newCardsPerDay: data.newCardsPerDay,
|
||||
sortOrder: (maximum._max.sortOrder ?? -1) + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSet(id: string, now = new Date()) {
|
||||
return getSetSummary(id, now);
|
||||
}
|
||||
|
||||
export async function updateSet(id: string, data: UpdateSpacedRepetitionSetInput) {
|
||||
const exists = await prisma.spacedRepetitionSet.findUnique({ where: { id } });
|
||||
if (!exists) throw new SpacedRepetitionServiceError("Repetition set not found", 404);
|
||||
return prisma.spacedRepetitionSet.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description === undefined ? undefined : data.description || null,
|
||||
newCardsPerDay: data.newCardsPerDay,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSet(id: string) {
|
||||
const result = await prisma.spacedRepetitionSet.deleteMany({ where: { id } });
|
||||
if (!result.count) throw new SpacedRepetitionServiceError("Repetition set not found", 404);
|
||||
}
|
||||
|
||||
export async function addDeck(setId: string, deckId: string) {
|
||||
const [set, deck, existing] = await Promise.all([
|
||||
prisma.spacedRepetitionSet.findUnique({ where: { id: setId } }),
|
||||
prisma.deck.findUnique({ where: { id: deckId } }),
|
||||
prisma.spacedRepetitionSetDeck.findUnique({
|
||||
where: { setId_deckId: { setId, deckId } },
|
||||
}),
|
||||
]);
|
||||
if (!set) throw new SpacedRepetitionServiceError("Repetition set not found", 404);
|
||||
if (!deck) throw new SpacedRepetitionServiceError("Deck not found", 404);
|
||||
if (set.classId !== deck.classId) {
|
||||
throw new SpacedRepetitionServiceError("Deck belongs to another class", 400);
|
||||
}
|
||||
if (existing) throw new SpacedRepetitionServiceError("Deck is already in this set", 409);
|
||||
|
||||
const maximum = await prisma.spacedRepetitionSetDeck.aggregate({
|
||||
where: { setId },
|
||||
_max: { sortOrder: true },
|
||||
});
|
||||
return prisma.spacedRepetitionSetDeck.create({
|
||||
data: { setId, deckId, sortOrder: (maximum._max.sortOrder ?? -1) + 1 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeDeck(setId: string, deckId: string) {
|
||||
const membership = await prisma.spacedRepetitionSetDeck.findUnique({
|
||||
where: { setId_deckId: { setId, deckId } },
|
||||
});
|
||||
if (!membership) throw new SpacedRepetitionServiceError("Deck membership not found", 404);
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.spacedRepetitionCardState.deleteMany({
|
||||
where: { setId, flashcard: { deckId } },
|
||||
}),
|
||||
prisma.spacedRepetitionSetDeck.delete({
|
||||
where: { setId_deckId: { setId, deckId } },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function reorderDecks(setId: string, deckIds: string[]) {
|
||||
if (new Set(deckIds).size !== deckIds.length) {
|
||||
throw new SpacedRepetitionServiceError("Deck order contains duplicates", 400);
|
||||
}
|
||||
const memberships = await prisma.spacedRepetitionSetDeck.findMany({
|
||||
where: { setId },
|
||||
select: { deckId: true },
|
||||
});
|
||||
const existing = new Set(memberships.map((item) => item.deckId));
|
||||
if (existing.size !== deckIds.length || deckIds.some((id) => !existing.has(id))) {
|
||||
throw new SpacedRepetitionServiceError("Deck order must include every set deck", 400);
|
||||
}
|
||||
await prisma.$transaction(
|
||||
deckIds.map((deckId, sortOrder) =>
|
||||
prisma.spacedRepetitionSetDeck.update({
|
||||
where: { setId_deckId: { setId, deckId } },
|
||||
data: { sortOrder },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function findNextCard(setId: string, now: Date, allowNew: boolean) {
|
||||
const { end } = getArizonaDayBounds(now);
|
||||
const due = await prisma.spacedRepetitionCardState.findFirst({
|
||||
where: {
|
||||
setId,
|
||||
OR: [
|
||||
{ scheduledDays: 0, due: { lte: now } },
|
||||
{ scheduledDays: { gte: 1 }, due: { lt: end } },
|
||||
],
|
||||
flashcard: { deck: { spacedRepetitionMemberships: { some: { setId } } } },
|
||||
},
|
||||
orderBy: { due: "asc" },
|
||||
include: { flashcard: { include: { deck: { select: { name: true } } } } },
|
||||
});
|
||||
if (due) {
|
||||
return {
|
||||
flashcard: due.flashcard,
|
||||
updatedAt: due.updatedAt,
|
||||
state: asStoredState(due),
|
||||
};
|
||||
}
|
||||
if (allowNew) {
|
||||
const memberships = await prisma.spacedRepetitionSetDeck.findMany({
|
||||
where: { setId },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
select: { deckId: true },
|
||||
});
|
||||
for (const membership of memberships) {
|
||||
const card = await prisma.flashcard.findFirst({
|
||||
where: {
|
||||
deckId: membership.deckId,
|
||||
spacedRepetitionStates: { none: { setId } },
|
||||
},
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: { deck: { select: { name: true } } },
|
||||
});
|
||||
if (card) return { flashcard: card, updatedAt: null, state: null };
|
||||
}
|
||||
}
|
||||
|
||||
const learningAhead = await prisma.spacedRepetitionCardState.findFirst({
|
||||
where: {
|
||||
setId,
|
||||
state: { in: INTRADAY_LEARNING_STATES },
|
||||
due: { gt: now, lte: getLearnAheadCutoff(now) },
|
||||
flashcard: { deck: { spacedRepetitionMemberships: { some: { setId } } } },
|
||||
},
|
||||
orderBy: { due: "asc" },
|
||||
include: { flashcard: { include: { deck: { select: { name: true } } } } },
|
||||
});
|
||||
if (learningAhead) {
|
||||
return {
|
||||
flashcard: learningAhead.flashcard,
|
||||
updatedAt: learningAhead.updatedAt,
|
||||
state: asStoredState(learningAhead),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getStudyState(setId: string, now = new Date()) {
|
||||
const summary = await getSetSummary(setId, now);
|
||||
const next = await findNextCard(setId, now, summary.stats.newCardsAvailable > 0);
|
||||
return {
|
||||
set: summary,
|
||||
currentCard: next
|
||||
? {
|
||||
id: next.flashcard.id,
|
||||
front: next.flashcard.front,
|
||||
back: next.flashcard.back,
|
||||
sourceDeckName: next.flashcard.deck.name,
|
||||
stateVersion: next.updatedAt?.toISOString() ?? null,
|
||||
previews: previewRatings(next.state, now),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function reviewCard(data: {
|
||||
setId: string;
|
||||
flashcardId: string;
|
||||
rating: SpacedRepetitionRating;
|
||||
expectedStateVersion: string | null;
|
||||
now?: Date;
|
||||
}) {
|
||||
const now = data.now ?? new Date();
|
||||
const card = await prisma.flashcard.findFirst({
|
||||
where: {
|
||||
id: data.flashcardId,
|
||||
deck: { spacedRepetitionMemberships: { some: { setId: data.setId } } },
|
||||
},
|
||||
});
|
||||
if (!card) throw new SpacedRepetitionServiceError("Card is not in this repetition set", 404);
|
||||
|
||||
const current = await prisma.spacedRepetitionCardState.findUnique({
|
||||
where: { setId_flashcardId: { setId: data.setId, flashcardId: data.flashcardId } },
|
||||
select: { ...cardStateSelect, firstReviewedAt: true },
|
||||
});
|
||||
const currentVersion = current?.updatedAt.toISOString() ?? null;
|
||||
if (currentVersion !== data.expectedStateVersion) {
|
||||
throw new SpacedRepetitionServiceError("This card was already reviewed. Refreshing the queue is required.", 409);
|
||||
}
|
||||
|
||||
const next = scheduleRating(current ? asStoredState(current) : null, data.rating, now);
|
||||
await prisma.$transaction(async (tx) => {
|
||||
if (current) {
|
||||
const updated = await tx.spacedRepetitionCardState.updateMany({
|
||||
where: { id: current.id, updatedAt: current.updatedAt },
|
||||
data: next,
|
||||
});
|
||||
if (!updated.count) throw new SpacedRepetitionServiceError("Card state changed", 409);
|
||||
} else {
|
||||
await tx.spacedRepetitionCardState.create({
|
||||
data: { setId: data.setId, flashcardId: data.flashcardId, ...next, firstReviewedAt: now },
|
||||
});
|
||||
}
|
||||
await tx.studyActivity.create({ data: { type: "FLASHCARD", occurredAt: now } });
|
||||
});
|
||||
|
||||
return getStudyState(data.setId, now);
|
||||
}
|
||||
13
vitest.config.ts
Normal file
13
vitest.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "node",
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue