From 061d13f421c655ef50f7d3f1e04f8dfc45708f75 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 11 Jul 2026 19:29:45 -0700 Subject: [PATCH 01/11] Update AGENTS.md --- AGENTS.md | 579 +++++++++++++++++++++--------------------------------- 1 file changed, 225 insertions(+), 354 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e398d64..e33c9fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,340 +1,242 @@ # Study Desk - AI Contributor Guide -This file is the project-specific operating guide for AI agents and human contributors working in this repository. +This file is the operating guide for AI agents and human contributors working in this repository. -## Project overview +## Project summary -Study Desk is a self-hosted, single-user study application. It organizes study material by class and supports: +Study Desk is a self-hosted, single-user study application built with Next.js, React, TypeScript, Prisma, and SQLite. -- Flashcard decks with Markdown content, self-grading, shuffle persistence, resume state, and card management. -- Practice quizzes with multiple-choice and SATA questions, scoring, rationales, category breakdowns, retakes, and attempt history. -- Importing validated JSON files and generating importable JSON from user-provided study material. -- Material groups for organizing decks and quizzes. -- Password-protected private pages with public, read-only share links for decks, quizzes, and groups. -- Light and dark themes with responsive layouts for desktop and mobile use. +Core capabilities: -The application is intentionally a small single-user container rather than a multi-tenant SaaS product. There is no `User` model. Authentication state is held in an encrypted `iron-session` cookie, and content ownership is implicit in the one local installation. +- Flashcard decks with Markdown content, self-grading, shuffle persistence, resume state, and card management +- Practice quizzes with multiple-choice and SATA questions, partial-credit scoring, rationales, category breakdowns, retakes, and attempt history +- JSON import, repair, validation, and generation workflows +- Material groups for organizing decks and quizzes +- Password-protected private pages +- Public, read-only share links for decks, quizzes, and groups +- Responsive light and dark themes -When documentation conflicts with implementation, prefer the current source code and `prisma/schema.prisma` over the older planning document or the stock README. +This is intentionally a single-user container, not a multi-tenant SaaS application. There is no `User` model. Content ownership is implicit in the local installation. -## Mandatory project rules +When documentation conflicts with implementation, prefer: -### Next.js version and documentation +1. Current source code +2. `prisma/schema.prisma` +3. Applied migrations +4. This file +5. Older planning documents and the stock README -This repository uses Next.js `16.2.9`, React `19.2.4`, and the App Router. This is not a generic Next.js project. Before changing Next.js routing, layouts, dynamic parameters, server/client boundaries, middleware, or framework APIs: +## Mandatory working rules -1. Read the relevant guide under `node_modules/next/dist/docs/`. -2. Follow the version installed in this repository, not a remembered API from an older Next.js version. -3. Pay attention to deprecation notices in build output. The current project still uses `src/middleware.ts`; Next.js currently warns that the `middleware` convention is moving toward `proxy`. Do not perform that migration as unrelated cleanup. - -Relevant local references include: - -- `node_modules/next/dist/docs/01-app/01-getting-started/05-server-and-client-components.md` -- `node_modules/next/dist/docs/01-app/01-getting-started/04-linking-and-navigating.md` -- `node_modules/next/dist/docs/01-app/03-api-reference/01-directives/use-client.md` -- `node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/route.md` -- `node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.md` - -### General contribution behavior - -- Ask a clarifying question before proceeding when missing information would materially change the implementation or create a risky assumption. Otherwise make a narrow, documented assumption and continue. -- Preserve existing user changes. Inspect `git status` before editing and do not use destructive commands such as `git reset --hard` or `git checkout --` unless explicitly requested. -- Use `apply_patch` for source and documentation edits. -- Keep changes scoped to the requested behavior. Do not rewrite unrelated components, migrate frameworks, or clean up the whole repository during a feature fix. +- Inspect `git status --short` before editing. +- Preserve unrelated user changes. +- Keep changes scoped to the requested behavior. +- Do not use destructive commands such as `git reset --hard` or `git checkout --` unless explicitly requested. +- Prefer narrow, patch-based edits over rewriting entire files. - Do not commit secrets, `.env` files, database contents, generated credentials, or private study material. -- Prefer TypeScript types over `any`, even though some existing files have `any` and the current lint baseline is not clean. - Do not manually edit generated Prisma files under `src/generated/prisma/`. -- Do not manually edit an already-applied migration. Change `prisma/schema.prisma`, then create a new migration. -- Add dependencies only when the existing stack cannot reasonably solve the problem. If dependencies change, update `package-lock.json` and verify the build. -- Keep route handlers, services, and UI components separated by responsibility as described below. +- Do not modify an already-applied migration. Change `prisma/schema.prisma`, then create a new migration. +- Prefer TypeScript types over `any`. +- Add dependencies only when the existing stack cannot reasonably solve the problem. +- When dependencies change, update `package-lock.json` and verify the build. +- Separate UI, route handling, services, and low-level utilities by responsibility. +- Ask a clarifying question only when missing information would materially change the implementation or create a risky assumption. -## Stack and repository configuration +## Stack and source of truth -| Area | Current implementation | -| --- | --- | -| Language | TypeScript, strict mode enabled | -| UI | React 19 with Next.js 16 App Router | -| Styling | Tailwind CSS v4 through `src/app/globals.css` and `@tailwindcss/postcss` | -| Fonts | `Manrope` for interface text and `Newsreader` for editorial headings via `next/font/google` | -| Persistence | SQLite with Prisma 7 and `@prisma/adapter-better-sqlite3` | -| Authentication | `iron-session` cookie, Argon2 password hashing, in-memory login rate limiter | -| Validation | Zod schemas in `src/lib/validation/` | -| Markdown | `react-markdown` with `remark-gfm` | -| Drag and drop | `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` | -| JSON repair | `jsonrepair` through `src/lib/jsonRepair.ts` | -| Runtime | Node 22-slim in the Docker image; local Node/npm should be compatible with the lockfile | -| Deployment | Next standalone output in a single Docker container | +Confirm exact versions in `package.json`. The project currently uses: -Important configuration files: +- Next.js 16 App Router +- React 19 +- TypeScript in strict mode +- Tailwind CSS v4 +- Prisma 7 with SQLite and `@prisma/adapter-better-sqlite3` +- `iron-session` for encrypted session cookies +- Argon2 password hashing +- Zod validation +- `react-markdown` with `remark-gfm` +- `@dnd-kit` for sortable UI +- Node 22 in Docker -- `package.json` - scripts and dependency versions. -- `next.config.ts` - standalone output and `better-sqlite3` as a server external package. -- `tsconfig.json` - strict TypeScript, bundler module resolution, and `@/*` mapped to `src/*`. -- `eslint.config.mjs` - Next core-web-vitals and Next TypeScript rules. -- `postcss.config.mjs` - Tailwind v4 PostCSS integration. -- `prisma.config.ts` - Prisma schema and SQLite URL resolution. -- `Dockerfile`, `docker-compose.yml`, `docker-compose.override.yml`, `docker-entrypoint.sh` - container build and runtime. +Important files: -## Commands and local setup +- `package.json` - scripts and dependency versions +- `prisma/schema.prisma` - database source of truth +- `src/lib/db.ts` - shared Prisma client +- `src/lib/auth.ts` - session helpers +- `src/services/` - business logic and database access +- `src/app/api/` - route handlers +- `src/app/globals.css` - design tokens and global styling +- `src/config/studyModes.ts` - shared study-mode navigation +- `SKILL.md` - project workflow for adding API routes +- `study-app-implementation-plan.md` - historical architectural context only +- `README.md` - currently not authoritative -### Install and run locally +For framework-sensitive changes involving routing, layouts, dynamic params, middleware, server/client boundaries, or framework APIs, follow nearby repository patterns and consult version-matched Next.js documentation. Do not perform unrelated framework migrations. -From the repository root: +## Local development + +For a clean checkout: ```bash -npm install +npm ci npx prisma generate npm run dev ``` -Then open `http://localhost:3000`. +On Windows PowerShell, use `npm.cmd` and `npx.cmd` if script execution policy blocks the default shims. -On Windows PowerShell, this environment may block the `npm.ps1` and `npx.ps1` shims because of execution policy. Use `npm.cmd` and `npx.cmd` when necessary: - -```powershell -npm.cmd install -npx.cmd prisma generate -npm.cmd run dev -``` - -The default local database is `dev.db` when `DATABASE_URL` is `file:./dev.db`. There is no seed script; classes and study material are created through the UI or API. - -### Environment variables - -Copy `.env.example` to `.env` for local work. The current variables are: - -| Variable | Purpose | -| --- | --- | -| `DATABASE_URL` | Prisma SQLite URL. Local default: `file:./dev.db`. | -| `SESSION_SECRET` | Secret used to encrypt the `iron-session` cookie. Use a random value of at least 32 characters outside local throwaway development. | -| `ADMIN_PASSWORD_HASH` | Present in `.env.example`, but the current login implementation does not read it. See authentication below. | -| `SECURE_COOKIES` | If set to `true` in production, the session cookie is marked `secure`. | - -The `.env` file is ignored by Git. Never paste real secrets into source files, documentation, logs, or commits. - -### Password initialization behavior - -The current implementation initializes the password on the first login when the `Setting` row `admin_password_hash` does not exist: - -1. The first password submitted at `/login` is Argon2-hashed and stored in the `Setting` table. -2. Later logins verify against that stored hash. -3. `ADMIN_PASSWORD_HASH` is currently not wired into this flow despite appearing in `.env.example` and the planning document. - -Do not change this behavior casually. If environment-driven password provisioning is added, update the login route, setup-status route, deployment documentation, and security handling together. - -### Available scripts +Useful scripts: ```bash -npm run dev # Next development server -npm run build # Production build; also runs TypeScript checking -npm run start # Start the built app locally -npm run lint # ESLint +npm run dev +npm run build +npm run start +npm run lint ``` -There is currently no test runner configured in `package.json`. Use the production build plus focused manual/browser verification for UI changes. +There is currently no configured test runner. Use focused manual verification plus `npm run build`. Run lint as well, but distinguish pre-existing failures from failures introduced by the current change. -### Database workflow +## Environment and authentication -After changing `prisma/schema.prisma`: +Common environment variables: + +- `DATABASE_URL` - SQLite URL +- `SESSION_SECRET` - `iron-session` secret, at least 32 characters outside throwaway local development +- `SECURE_COOKIES` - marks the session cookie secure when set to `true` +- `ADMIN_PASSWORD_HASH` - present in `.env.example`, but currently not used by the login flow + +Current password behavior: + +1. On first login, the submitted password is Argon2-hashed. +2. The hash is stored in the `Setting` row `admin_password_hash`. +3. Later logins verify against that stored hash. + +Do not change password provisioning casually. Any change must update login, setup-status behavior, deployment documentation, and security handling together. + +Authentication state is stored in the `study-app-session` cookie. + +Public paths include: + +- `/login` +- `/api/auth/*` +- `/shared/*` +- Static assets and Next internals + +Everything else requires authentication unless explicitly designed and reviewed as public. + +Do not log passwords, secrets, full share tokens, or private study content. + +## Database and migration safety + +The local `dev.db` may contain real study data. + +- Do not delete, reset, replace, or recreate it during normal work. +- Back up the database before destructive migration work. +- Use the shared Prisma client from `@/lib/db`. +- Do not create additional `PrismaClient` instances in request handlers or components. +- Use transactions when partial writes would leave inconsistent state. +- Return 404 responses for missing records instead of exposing raw Prisma errors. + +After changing the Prisma schema: ```bash npx prisma generate npx prisma migrate dev --name describe_the_change ``` -Use `npx prisma migrate deploy` for an existing deployment. The Docker entrypoint runs `prisma migrate deploy` before starting the server. +Use `npx prisma migrate deploy` for deployed environments. -Be careful with `dev.db`: it may contain the developer's actual study data. Do not delete or reset it as part of normal feature work. Inspect migration status and make a backup before any destructive database operation. - -## Docker details - -The production image is a three-stage Node 22-slim build: - -1. Install dependencies. -2. Generate Prisma and run `next build`. -3. Run the Next standalone server with SQLite data mounted at `/app/data`. - -The runtime image sets `DATABASE_URL=file:/app/data/study.db` and `PORT=3726`. `docker-entrypoint.sh` applies migrations and then starts `server.js`. - -Known compose detail: `docker-compose.yml` currently publishes `3000:3000`, while the production Dockerfile sets the runtime port to `3726`. Verify or correct that mapping before relying on the production compose file. `docker-compose.override.yml` is a development override that runs `npm run dev` and uses port 3000. - -## Current project structure - -The important source tree is: - -```text -prisma/ - schema.prisma # SQLite/Prisma source of truth - migrations/ # Applied database migrations - -src/ - app/ - layout.tsx # Root metadata, fonts, theme bootstrap - globals.css # Tailwind v4, design tokens, global styles - icon.svg - login/page.tsx # Public login/setup screen - (protected)/ - layout.tsx # Session check and authenticated navbar shell - page.tsx # Authenticated class library - [classSlug]/ - layout.tsx # Class lookup, header, and study-mode tabs - page.tsx # Redirects to flashcards - flashcards/page.tsx # Grouped deck library and drag/drop ordering - flashcards/[deckId]/page.tsx - # Deck study/manage screen - quizzes/page.tsx # Grouped quiz library and drag/drop ordering - quizzes/[quizId]/page.tsx - # Quiz taking/history screen - shared/[classSlug]/[type]/[token]/ - page.tsx # Public read-only share route - SharedViewer.tsx # Public deck/quiz viewer - SharedGroupViewer.tsx # Public group selector/viewer - api/ - .../route.ts # Route handlers; see API map below - - components/ - flashcards/ # Flashcard viewer, manager, and list - quizzes/ # Quiz viewer, results, history, breakdown - import/ # Import modal and Create/Import/Generate tabs - ui/ # Navbar, class header/tabs, theme, share menu - - services/ - classService.ts - deckService.ts - cardService.ts - quizService.ts - progressService.ts - shareService.ts - settingsService.ts # Persistence for LLM instruction templates - - lib/ - db.ts # Prisma singleton with better-sqlite3 adapter - auth.ts # iron-session helpers - rateLimiter.ts # In-memory login sliding-window limiter - scoring.ts # Quiz scoring - shuffle.ts # Stable order/session helpers - jsonRepair.ts # JSON parse/repair pipeline - validation/importSchemas.ts - - config/studyModes.ts # Central list of flashcards/quizzes modes - middleware.ts # Auth gate and public-path exceptions - generated/prisma/ # Generated Prisma client; do not hand edit - -public/ # Static assets -``` - -`(protected)` is a route group and is not part of the URL. The `classSlug`, `deckId`, `quizId`, and share `token` segments are dynamic route parameters. - -## Page routes and API surface - -### Page routes - -| Route | Behavior | -| --- | --- | -| `/login` | Public password setup/login page. | -| `/` | Authenticated class library. | -| `/` | Redirects to `//flashcards`. | -| `//flashcards` | Grouped flashcard deck library. | -| `//flashcards/` | Flashcard study or card-management view. | -| `//quizzes` | Grouped quiz library. | -| `//quizzes/` | Quiz-taking or attempt-history view. | -| `/shared//flashcards/` | Public shared deck. | -| `/shared//quizzes/` | Public shared quiz. | -| `/shared//groups/` | Public shared group; `?itemId=...` can select an item. | - -### API routes - -All API routes are behind `src/middleware.ts` unless they are under `/api/auth`. The API is currently consumed by client components with `fetch`. - -| Endpoint | Methods and purpose | -| --- | --- | -| `/api/auth/login` | `POST` login, first-time password setup, rate limiting, progressive lockout. | -| `/api/auth/logout` | `POST` destroy the session. | -| `/api/auth/setup-status` | `GET` whether the password setting exists. | -| `/api/classes` | `GET` list classes; `POST` create class. | -| `/api/classes/[id]` | `PATCH` rename; `DELETE` class and cascaded content. | -| `/api/decks` | `POST` validate and import a deck. | -| `/api/decks/list` | `GET` decks for `classId`. | -| `/api/decks/[id]` | `GET` deck with cards; `PATCH` rename/description; `DELETE`. | -| `/api/decks/[id]/cards` | `POST` append a card. | -| `/api/cards/[id]` | `PATCH` edit card; `DELETE` card. | -| `/api/decks/reorder` | `PATCH` persist deck order/group placement. | -| `/api/quizzes` | `POST` validate and import a quiz. | -| `/api/quizzes/list` | `GET` quizzes for `classId`. | -| `/api/quizzes/[id]` | `GET` quiz with questions/options; `PATCH` rename/description; `DELETE`. | -| `/api/quizzes/[id]/attempt` | `GET` attempt history; `POST` score and save an attempt. | -| `/api/quizzes/reorder` | `PATCH` persist quiz order/group placement. | -| `/api/material-groups` | `GET` groups by `classId` and optional `type`; `POST` create group. | -| `/api/material-groups/[id]` | `PATCH` rename/group metadata; `DELETE` group. | -| `/api/progress` | `GET`, `PATCH`, and `DELETE` resume progress for deck/quiz and mode. | -| `/api/settings/llm-instructions` | `GET`/`PATCH` saved generation instructions by `type`; `__RESET__` restores defaults. | -| `/api/share` | `GET` share state; `POST` toggle a deck, quiz, or group link. | - -## Architecture and coding conventions +## Architecture conventions ### Server and client components -- Use Server Components by default for layouts and pages that only fetch or compose data. -- Add `"use client"` only where state, event handlers, effects, browser APIs, drag/drop, or client-side fetches are needed. -- Once a file is a Client Component, its directly imported module graph is included in the client bundle. Keep server-only database/auth code out of client imports. -- Do not access `window`, `localStorage`, `navigator`, or the clipboard API from a Server Component. -- Dynamic server route params use the current Next.js 16 conventions in this repository. Follow nearby examples such as `src/app/(protected)/[classSlug]/layout.tsx` and API `RouteContext` types instead of inventing older parameter types. -- Prefer `Link` for normal internal navigation. Use `useRouter` only when navigation follows an action or requires imperative behavior. +- Use Server Components by default. +- Add `"use client"` only when state, effects, event handlers, browser APIs, drag and drop, or client-side fetching are required. +- Keep database and authentication modules out of client import graphs. +- Do not access `window`, `localStorage`, `navigator`, or clipboard APIs from Server Components. +- Prefer `Link` for ordinary internal navigation. +- Use `useRouter` when navigation follows an action or must be imperative. +- Follow nearby Next.js 16 patterns for dynamic params and route context types. -### API handlers and services +### API routes and services -The preferred API route shape is: +Preferred route-handler flow: -1. Parse and validate request input. -2. Call one function in the matching service or low-level utility. -3. Return a `NextResponse` with a useful status and JSON body. +1. Parse and validate external input. +2. Delegate business logic to focused service or utility functions. +3. Return a useful status and JSON response. -Business logic and Prisma calls belong in `src/services/` or a focused `src/lib/` utility. Reuse an existing service before creating a new one. `SKILL.md` contains the project-specific workflow for adding an API route. +Business logic and Prisma calls normally belong in `src/services/` or a focused `src/lib/` module. Reuse existing services before creating new abstractions. -A few older routes still contain direct Prisma calls or loose input handling. Follow the preferred pattern for new work without broad refactoring unless the task specifically requests it. +Some older routes still contain direct Prisma calls or loose input handling. Improve the requested path without turning a small task into a broad refactor. -### Prisma and database access +### Validation -- Import the shared client from `@/lib/db`; do not instantiate a new `PrismaClient` in a request or component. -- Prisma uses the generated client at `src/generated/prisma`, configured by `prisma/schema.prisma`. -- Keep database names and relation behavior consistent with the schema: `Deck` and `QuizSet` belong to a `Class`; cards/questions/options cascade from their parent; group deletion sets item `groupId` to null; share links and progress cascade with their content. -- Use transactions for multi-row reorder/update operations where partial writes would be harmful. -- Handle missing records as 404s at the route boundary rather than leaking Prisma exceptions. +- Reuse existing Zod schemas where available. +- Never rely only on client-side validation. +- Validate IDs, required strings, enums, arrays, and import payloads at the API boundary. +- Keep stored JSON parseable. +- Use `jsonRepair.ts` only for user- or LLM-generated import text, not to hide malformed database state. -### Validation and input handling +## UI and accessibility -- Reuse `flashcardImportSchema` and `quizImportSchema` from `src/lib/validation/importSchemas.ts` for import payloads on both client preview and server commit. -- Do not trust client-side validation alone. -- Validate IDs, enum-like values (`DECK`/`QUIZ`, `SEQUENTIAL`/`SHUFFLED`, question types), required strings, and array shapes at the API boundary. -- Keep stored JSON fields parseable and handle malformed or stale JSON defensively. `jsonRepair.ts` is for repairing user/LLM-generated import text, not for hiding malformed database state. +Use the existing design system in `src/app/globals.css`. -### UI and design system +Common tokens include: -- Use the CSS variables and Tailwind aliases defined in `src/app/globals.css`: `bg-bg-base`, `bg-bg-surface`, `bg-bg-surface-alt`, `text-text-heading`, `text-text-secondary`, `text-text-muted`, `primary`, `primary-hover`, `border`, `border-light`, `success`, and `error`. -- Preserve both light and dark theme behavior. Avoid introducing one-off colors when an existing token is appropriate. -- Use `editorial-title` for major serif headings, `font-sans`/Manrope for interface text, and the existing card/modal shadows and rounded-corner language. -- Maintain responsive behavior at mobile widths. Study pages, library cards, modals, and action rows must remain usable without horizontal overflow. -- Use visible focus states, descriptive `title`/`aria-label` text for icon-only buttons, semantic buttons/links, and keyboard-accessible controls. -- Keep Markdown rendering consistent with the existing `ReactMarkdown` + `remarkGfm` usage and `.markdown-content` styles. -- Use the existing `@dnd-kit` patterns when changing sortable cards or groups. Do not attach drag listeners to nested action buttons. +- `bg-bg-base` +- `bg-bg-surface` +- `bg-bg-surface-alt` +- `text-text-heading` +- `text-text-secondary` +- `text-text-muted` +- `primary` +- `primary-hover` +- `border` +- `border-light` +- `success` +- `error` -## Domain behavior to preserve +Preserve: + +- Light and dark themes +- Responsive mobile layouts +- Existing card, modal, shadow, and rounded-corner language +- `editorial-title` for major serif headings +- Manrope for interface text +- Existing Markdown rendering through `ReactMarkdown`, `remarkGfm`, and `.markdown-content` +- Existing `@dnd-kit` patterns for sortable content + +Accessibility requirements: + +- Use semantic buttons and links. +- Preserve visible keyboard focus states. +- Give icon-only controls an accessible name with visible text or `aria-label`. +- Keep interactive controls keyboard accessible. +- Avoid horizontal overflow on study pages, library cards, modals, and action rows. +- Do not attach drag listeners to nested action buttons. + +## Domain invariants ### Flashcards -- A deck contains ordered `Flashcard` rows with `front` and `back` strings. -- Study progress is stored in `StudyProgress` as JSON order/results and can be sequential or shuffled. -- Deleting a card can leave a stale ID in saved `orderJson`; the shuffle/session helpers filter and clamp stale IDs when resuming. -- The deck page supports study and manage views. Keep the existing viewer-header share action when adding other share entry points. +- A deck contains ordered `Flashcard` rows with `front` and `back`. +- Study progress stores order and results as JSON. +- Study mode may be sequential or shuffled. +- Saved progress may contain stale card IDs after deletion. Resume helpers must filter and clamp stale IDs safely. +- Preserve both study and manage views. +- Preserve existing sharing entry points when adding new ones. ### Quizzes -- A quiz contains ordered questions, each with ordered answer options. +- A quiz contains ordered questions with ordered answer options. - `MULTIPLE_CHOICE` questions must have exactly one correct option. -- `SATA` questions may have multiple correct options and use partial credit in `src/lib/scoring.ts`. -- Attempts are persisted as `QuizAttempt` rows with score, maximum score, answer JSON, retake flag, and completion time. -- Partial retakes score only the questions included in the submitted answer set. -- The quiz viewer persists in-progress answers and position through `/api/progress`. +- `SATA` questions may have multiple correct options. +- SATA uses partial-credit scoring in `src/lib/scoring.ts`. +- Partial retakes score only questions present in the submitted answer set. +- Attempts persist score, maximum score, submitted answers, retake state, and completion time. +- In-progress answers and position persist through `/api/progress`. ### Imports and generation @@ -368,112 +270,81 @@ Quiz import shape: } ``` -- Import UI is split into `ImportModal`, `CreateTab`, `ImportTab`, and `GenerateTab`. Keep that separation rather than creating one large modal component. -- Generation instructions are stored in the `Setting` table and are editable/resettable through `/api/settings/llm-instructions`. The app does not call an LLM itself; the Generate tab prepares instructions and accepts generated JSON from the user. -- Generated text may contain Markdown inside JSON string values. Preserve it and render it safely through the existing Markdown components. +The app does not call an LLM directly. The Generate UI prepares instructions and accepts generated JSON from the user. + +Preserve Markdown inside JSON string values and render it through existing Markdown components. + +Keep the current separation between `ImportModal`, `CreateTab`, `ImportTab`, and `GenerateTab`. ### Sharing -- `ShareLink` rows use UUID IDs as public tokens and can target a `DECK`, `QUIZ`, or `GROUP`. -- `src/components/ui/ShareMenu.tsx` is the shared UI for loading share state, toggling a link, and copying the public URL. Reuse it rather than duplicating share URL or group-inheritance logic. -- Public URLs are `/shared///`, where item types are `flashcards`, `quizzes`, and `groups`. -- A deck or quiz can appear publicly shared through its group. The API returns the group token and the UI includes `?itemId=...` when copying a link to an item in a shared group. -- Shared viewers are read-only and should not write authenticated progress. Preserve the distinction between protected study viewers and public shared viewers. - -## Data model summary - -The source of truth is `prisma/schema.prisma`: - -- `Class` - named study space with a unique slug and sort order. -- `Deck` - flashcard collection belonging to a class, optionally assigned to a `MaterialGroup`. -- `Flashcard` - ordered front/back card belonging to a deck. -- `QuizSet` - quiz collection belonging to a class, optionally assigned to a `MaterialGroup`. -- `Question` - ordered quiz prompt, type, rationale, and category. -- `AnswerOption` - ordered answer option with `isCorrect`. -- `StudyProgress` - resumable deck/quiz state. Unique by parent and mode. -- `QuizAttempt` - completed quiz score and submitted answer JSON. -- `MaterialGroup` - typed (`DECK` or `QUIZ`) class grouping for library organization. -- `ShareLink` - optional one-to-one link for a deck, quiz, or group. -- `Setting` - key/value storage for the admin password hash and editable LLM instructions. -- `AuthSecurity` - singleton lockout/rate-limit state persisted in SQLite. - -Most parent relations use cascading deletion. Treat deletion of a class, deck, quiz, question, or group as a data-affecting operation and preserve confirmation behavior in the UI. - -## Authentication and security boundaries - -`src/middleware.ts` allows these paths without a session: - -- `/login` -- `/api/auth/*` -- `/shared/*` -- Static assets and Next internals - -Everything else requires the `study-app-session` cookie. Individual API routes generally rely on middleware for the authenticated boundary, so a new public endpoint must be an explicit, reviewed decision. - -The login route has two protections: - -- An in-memory per-IP sliding-window limiter: 10 attempts per minute. -- Progressive lockout stored in `AuthSecurity`: one minute after 5 failures, five minutes after 10, thirty minutes after 15, and twenty-four hours after 20. - -Do not log passwords, session secrets, full share tokens, or private study content. Treat public share routes as intentionally public and validate that the token, type, and class slug agree before returning content. +- `ShareLink` may target a `DECK`, `QUIZ`, or `GROUP`. +- Reuse `src/components/ui/ShareMenu.tsx`. +- Public routes are `/shared///`. +- Public viewers are read-only. +- Public viewers must not write authenticated progress. +- Validate that token, type, and class slug agree before returning shared content. +- A deck or quiz may appear publicly shared through its group. Preserve inherited group-share behavior and `?itemId=...` handling. ## Verification checklist -### Before editing +Before editing: - Read `git status --short`. -- Inspect the target page/component, its API route, and its service before changing behavior. -- Read the relevant Next.js local guide if framework behavior is involved. -- Check whether the change affects the Prisma schema, migrations, auth boundary, public sharing, or generated client. +- Inspect the target component, route, service, and schema paths relevant to the requested behavior. +- Check whether the change affects authentication, public sharing, migrations, generated Prisma output, or real study data. -### During implementation +During implementation: -- Keep the route/service/component boundaries intact. +- Keep route, service, utility, and component responsibilities clear. - Add validation for new external input. -- Reuse existing design tokens, shared components, and services. -- Consider loading, error, empty, mobile, keyboard, and both theme states. -- Avoid synchronous state updates directly inside effects when a state initializer, derived value, event handler, or async callback is more appropriate. The current ESLint configuration reports these patterns. +- Reuse existing design tokens, components, and services. +- Consider loading, error, empty, mobile, keyboard, light-theme, and dark-theme states. +- Avoid unrelated cleanup. -### After editing - -Run the narrowest relevant checks, then the full build when possible: +After editing: ```bash npm run lint npm run build ``` -If lint is already failing, report baseline failures separately from failures introduced by the change. Do not silence rules or add broad ESLint disables just to make a task appear clean. +If lint already fails, report baseline failures separately. -For UI changes, manually verify at least the affected authenticated route and its corresponding public/shared route if applicable. A useful smoke-test path is: +For UI work, manually verify: -1. Log in or complete first-time setup. -2. Create or open a class. -3. Import or edit representative flashcards and quizzes. -4. Exercise the changed action, including loading/error/empty states. -5. Check study resume/restart, drag/drop if relevant, and public sharing if relevant. -6. Check light/dark themes and a narrow mobile viewport. +1. The affected authenticated route +2. The corresponding public/shared route when applicable +3. Loading, error, and empty states +4. Study resume and restart behavior when relevant +5. Drag and drop when relevant +6. Light and dark themes +7. A narrow mobile viewport +8. No horizontal overflow +9. Keyboard access for changed controls ## Known repository conditions -- `README.md` is still mostly the default create-next-app README and is not a reliable description of the current product. -- `study-app-implementation-plan.md` is a valuable architectural reference, but some details are historical and the current source code wins. -- `task.md` records completed implementation phases and is useful for feature context. -- `SKILL.md` is the project-specific workflow for adding API routes; follow it for new route work. -- The current full lint command has pre-existing errors in several files, including existing `any` usage and React effect-rule violations. Do not attribute those to an unrelated small change without checking the changed lines. -- A successful `next build` is the primary repository-wide compile/type verification currently available. -- Next build output currently warns about the deprecated `middleware` file convention. Treat that as known baseline unless the task is specifically about the migration. +- `README.md` is still mostly stock create-next-app documentation. +- `study-app-implementation-plan.md` contains useful historical context but may be stale. +- `task.md` records completed implementation phases. +- `SKILL.md` describes the expected workflow for new API routes. +- The current lint baseline includes existing errors such as `any` usage and React effect-rule violations. +- A successful `next build` is the primary repository-wide compile and type check. +- The current middleware convention produces a known deprecation warning. +- `docker-compose.yml` may publish port `3000`, while the production Docker image uses port `3726`. Verify the mapping before relying on production Compose. +- Do not silently fix known baseline issues as unrelated cleanup. ## Extending the application -For a genuinely new study mode, follow the existing extensibility pattern: +For a genuinely new study mode: -1. Add the domain model and migration in Prisma if persistence is needed. +1. Add or update the Prisma model and migration when persistence is required. 2. Add a focused service in `src/services/`. 3. Add API routes under `src/app/api/`. -4. Add the page route and client components. -5. Add the mode to `src/config/studyModes.ts` if it belongs in shared class navigation. -6. Update public sharing, progress, import, or scoring only if the new mode needs those capabilities. -7. Verify auth, mobile layout, theme support, and any public/private boundary. +4. Add page routes and client components. +5. Add the mode to `src/config/studyModes.ts` when it belongs in shared navigation. +6. Extend progress, import, scoring, or sharing only when required. +7. Verify authentication, public/private boundaries, mobile layout, and both themes. -Do not add a new mode by placing unrelated logic into `deckService.ts`, `quizService.ts`, or a giant page component. The existing separation is intentional and is the main maintainability boundary in this project. +Do not add a new mode by placing unrelated logic into `deckService.ts`, `quizService.ts`, or a large page component. \ No newline at end of file From f0548bcc6e9d70c0c0575420630d7001e21d738c Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 11 Jul 2026 19:36:17 -0700 Subject: [PATCH 02/11] Fix: share popup not closing when clicking outside menu --- src/components/ui/ShareMenu.tsx | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/ui/ShareMenu.tsx b/src/components/ui/ShareMenu.tsx index 9fa0814..1aa85b5 100644 --- a/src/components/ui/ShareMenu.tsx +++ b/src/components/ui/ShareMenu.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; interface ShareMenuProps { targetType: "DECK" | "QUIZ" | "GROUP"; @@ -16,6 +16,24 @@ export function ShareMenu({ targetType, contentId, classSlug, compact = false }: const [copied, setCopied] = useState(false); const [isGroupShared, setIsGroupShared] = useState(false); const [groupName, setGroupName] = useState(null); + const buttonRef = useRef(null); + const menuRef = useRef(null); + + useEffect(() => { + if (!open) return; + + function handlePointerDown(event: PointerEvent) { + const target = event.target as Node; + if (buttonRef.current?.contains(target) || menuRef.current?.contains(target)) { + return; + } + + setOpen(false); + } + + document.addEventListener("pointerdown", handlePointerDown); + return () => document.removeEventListener("pointerdown", handlePointerDown); + }, [open]); useEffect(() => { if (!open) return; @@ -63,6 +81,7 @@ export function ShareMenu({ targetType, contentId, classSlug, compact = false }: return (
- )} - - )} - - + + )} + + {stage === "token" && ( +
+
+ + setToken(event.target.value)} + autoComplete="one-time-code" + autoFocus + className="min-h-12 w-full rounded-xl border border-border bg-bg-surface-alt/60 px-4 font-mono text-text-heading placeholder:text-text-muted focus:border-primary" + placeholder="Paste the token from Docker logs" + /> +
+ + Validate Token + Back to sign in + + )} + + {stage === "password" && ( +
+
+ + = 8} + validText="Password meets the length requirement." + invalidText="Password must be at least 8 characters." + /> +
+
+ 0 && confirmPassword !== password} + /> + {confirmPassword && ( + + )} +
+ + + Set New Password + + Cancel + + )} + + {stage === "success" && ( +
+
+ Your password has been reset successfully. +
+ Sign In +
+ )} - + ); } + +function PasswordField({ + id, + label, + value, + onChange, + placeholder, + describedBy, + invalid = false, + autoFocus = false, +}: { + id: string; + label: string; + value: string; + onChange: (value: string) => void; + placeholder?: string; + describedBy?: string; + invalid?: boolean; + autoFocus?: boolean; +}) { + return ( +
+ + onChange(event.target.value)} + placeholder={placeholder} + autoComplete={id === "password" ? "current-password" : "new-password"} + aria-describedby={describedBy} + aria-invalid={invalid} + autoFocus={autoFocus} + className="min-h-12 w-full rounded-xl border border-border bg-bg-surface-alt/60 px-4 text-text-heading placeholder:text-text-muted focus:border-primary" + /> +
+ ); +} + +function ValidationMessage({ id, valid, validText, invalidText }: { + id: string; + valid: boolean; + validText: string; + invalidText: string; +}) { + return ( +

+ {valid ? validText : invalidText} +

+ ); +} + +function ErrorMessage({ message }: { message: string }) { + if (!message) return null; + return
{message}
; +} + +function PrimaryButton({ loading, disabled = false, onClick, children }: { + loading: boolean; + disabled?: boolean; + onClick?: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +function SecondaryButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) { + return ( + + ); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 2a3182d..c3e4bae 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -3,6 +3,12 @@ import { cookies } from "next/headers"; export interface SessionData { isAuthenticated: boolean; + sessionGeneration?: number; +} + +interface PasswordResetSessionData { + nonce?: string; + expiresAt?: string; } const sessionOptions: SessionOptions = { @@ -15,14 +21,27 @@ const sessionOptions: SessionOptions = { }, }; +const passwordResetSessionOptions: SessionOptions = { + password: sessionOptions.password, + cookieName: "study-app-password-reset", + cookieOptions: { + secure: sessionOptions.cookieOptions?.secure, + httpOnly: true, + sameSite: "lax" as const, + maxAge: 15 * 60, + path: "/", + }, +}; + export async function getSession() { const cookieStore = await cookies(); return getIronSession(cookieStore, sessionOptions); } -export async function createSession() { +export async function createSession(sessionGeneration: number) { const session = await getSession(); session.isAuthenticated = true; + session.sessionGeneration = sessionGeneration; await session.save(); } @@ -35,3 +54,23 @@ export async function isAuthenticated() { const session = await getSession(); return session.isAuthenticated === true; } + +export async function getPasswordResetSession() { + const cookieStore = await cookies(); + return getIronSession( + cookieStore, + passwordResetSessionOptions + ); +} + +export async function authorizePasswordReset(nonce: string, expiresAt: string) { + const session = await getPasswordResetSession(); + session.nonce = nonce; + session.expiresAt = expiresAt; + await session.save(); +} + +export async function destroyPasswordResetAuthorization() { + const session = await getPasswordResetSession(); + session.destroy(); +} diff --git a/src/lib/rateLimiter.ts b/src/lib/rateLimiter.ts index 0bf1b4b..9513c69 100644 --- a/src/lib/rateLimiter.ts +++ b/src/lib/rateLimiter.ts @@ -12,20 +12,25 @@ const store = new Map(); const WINDOW_MS = 60 * 1000; // 1 minute const MAX_REQUESTS = 10; -export function checkRateLimit(ip: string): { allowed: boolean; retryAfterMs: number } { +export function checkRateLimit( + key: string, + options: { windowMs?: number; maxRequests?: number } = {} +): { allowed: boolean; retryAfterMs: number } { const now = Date.now(); - const entry = store.get(ip) ?? { timestamps: [] }; + const windowMs = options.windowMs ?? WINDOW_MS; + const maxRequests = options.maxRequests ?? MAX_REQUESTS; + const entry = store.get(key) ?? { timestamps: [] }; // Remove timestamps outside the window - entry.timestamps = entry.timestamps.filter((t) => now - t < WINDOW_MS); + entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs); - if (entry.timestamps.length >= MAX_REQUESTS) { + if (entry.timestamps.length >= maxRequests) { const oldestInWindow = entry.timestamps[0]; - const retryAfterMs = WINDOW_MS - (now - oldestInWindow); + const retryAfterMs = windowMs - (now - oldestInWindow); return { allowed: false, retryAfterMs }; } entry.timestamps.push(now); - store.set(ip, entry); + store.set(key, entry); return { allowed: true, retryAfterMs: 0 }; } diff --git a/src/lib/validation/authSchemas.ts b/src/lib/validation/authSchemas.ts new file mode 100644 index 0000000..bb4c32c --- /dev/null +++ b/src/lib/validation/authSchemas.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const loginSchema = z.object({ + password: z.string().min(8, "Password must be at least 8 characters"), +}); + +export const resetTokenSchema = z.object({ + token: z.string().min(1, "Reset token is required"), +}); + +export const completePasswordResetSchema = z + .object({ + password: z.string().min(8, "Password must be at least 8 characters"), + confirmPassword: z.string(), + }) + .refine((value) => value.password === value.confirmPassword, { + message: "Passwords do not match", + path: ["confirmPassword"], + }); diff --git a/src/middleware.ts b/src/proxy.ts similarity index 64% rename from src/middleware.ts rename to src/proxy.ts index 2bbe2d6..8a22eb4 100644 --- a/src/middleware.ts +++ b/src/proxy.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getIronSession } from "iron-session"; import type { SessionData } from "@/lib/auth"; +import { prisma } from "@/lib/db"; const sessionOptions = { password: @@ -9,18 +10,15 @@ const sessionOptions = { cookieName: "study-app-session", }; -// Routes that don't require authentication const publicPaths = ["/login", "/api/auth", "/shared"]; -export async function middleware(request: NextRequest) { +export async function proxy(request: NextRequest) { const { pathname } = request.nextUrl; - // Allow public routes if (publicPaths.some((path) => pathname.startsWith(path))) { return NextResponse.next(); } - // Allow static assets and Next.js internals if ( pathname.startsWith("/_next") || pathname.startsWith("/favicon") || @@ -29,15 +27,18 @@ export async function middleware(request: NextRequest) { return NextResponse.next(); } - // Check session const response = NextResponse.next(); - const session = await getIronSession( - request, - response, - sessionOptions - ); + const session = await getIronSession(request, response, sessionOptions); + const generationSetting = await prisma.setting.findUnique({ + where: { key: "auth_session_generation" }, + }); + const currentGeneration = Number.parseInt(generationSetting?.value ?? "1", 10); - if (!session.isAuthenticated) { + if ( + !session.isAuthenticated || + session.sessionGeneration !== currentGeneration + ) { + session.destroy(); return NextResponse.redirect(new URL("/login", request.url)); } diff --git a/src/services/authService.ts b/src/services/authService.ts new file mode 100644 index 0000000..c54f9c5 --- /dev/null +++ b/src/services/authService.ts @@ -0,0 +1,227 @@ +import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; +import { prisma } from "@/lib/db"; +import { + authorizePasswordReset, + createSession, + destroyPasswordResetAuthorization, + getPasswordResetSession, +} from "@/lib/auth"; + +const PASSWORD_HASH_KEY = "admin_password_hash"; +const RESET_TOKEN_KEY = "admin_password_reset"; +const SESSION_GENERATION_KEY = "auth_session_generation"; +const RESET_TOKEN_LIFETIME_MS = 15 * 60 * 1000; + +interface ResetTokenRecord { + digest: string; + nonce: string; + expiresAt: string; +} + +export type LoginResult = + | { ok: true } + | { ok: false; status: 401 | 423; error: string }; + +function getLockoutDuration(failedAttempts: number): number { + if (failedAttempts >= 20) return 24 * 60 * 60 * 1000; + if (failedAttempts >= 15) return 30 * 60 * 1000; + if (failedAttempts >= 10) return 5 * 60 * 1000; + if (failedAttempts >= 5) return 60 * 1000; + return 0; +} + +function digestToken(token: string) { + return createHash("sha256").update(token, "utf8").digest(); +} + +function parseResetTokenRecord(value: string | undefined): ResetTokenRecord | null { + if (!value) return null; + + try { + const parsed: unknown = JSON.parse(value); + if ( + typeof parsed === "object" && + parsed !== null && + "digest" in parsed && + "nonce" in parsed && + "expiresAt" in parsed && + typeof parsed.digest === "string" && + typeof parsed.nonce === "string" && + typeof parsed.expiresAt === "string" + ) { + return parsed as ResetTokenRecord; + } + } catch { + // A malformed reset record is treated as invalid. + } + + return null; +} + +async function getSessionGeneration(): Promise { + const setting = await prisma.setting.findUnique({ + where: { key: SESSION_GENERATION_KEY }, + }); + const generation = Number.parseInt(setting?.value ?? "1", 10); + return Number.isSafeInteger(generation) && generation > 0 ? generation : 1; +} + +async function getOrCreateSecurityRecord() { + return prisma.authSecurity.upsert({ + where: { id: 1 }, + update: {}, + create: { id: 1 }, + }); +} + +export async function login(password: string): Promise { + const security = await getOrCreateSecurityRecord(); + + if (security.lockedUntil && security.lockedUntil > new Date()) { + const remainingMin = Math.ceil( + (security.lockedUntil.getTime() - Date.now()) / 60_000 + ); + return { + ok: false, + status: 423, + error: `Account locked. Try again in ${remainingMin} minute${remainingMin === 1 ? "" : "s"}.`, + }; + } + + const passwordSetting = await prisma.setting.findUnique({ + where: { key: PASSWORD_HASH_KEY }, + }); + const argon2 = await import("argon2"); + let isValid = false; + + if (!passwordSetting) { + await prisma.setting.create({ + data: { key: PASSWORD_HASH_KEY, value: await argon2.hash(password) }, + }); + isValid = true; + } else { + try { + isValid = await argon2.verify(passwordSetting.value, password); + } catch { + isValid = false; + } + } + + if (!isValid) { + const failedAttempts = security.failedAttempts + 1; + const lockoutMs = getLockoutDuration(failedAttempts); + await prisma.authSecurity.update({ + where: { id: 1 }, + data: { + failedAttempts, + lockedUntil: lockoutMs ? new Date(Date.now() + lockoutMs) : null, + lastAttemptAt: new Date(), + }, + }); + return { ok: false, status: 401, error: "Invalid password" }; + } + + await prisma.authSecurity.update({ + where: { id: 1 }, + data: { failedAttempts: 0, lockedUntil: null, lastAttemptAt: new Date() }, + }); + await createSession(await getSessionGeneration()); + return { ok: true }; +} + +export async function requestPasswordReset(): Promise { + const passwordSetting = await prisma.setting.findUnique({ + where: { key: PASSWORD_HASH_KEY }, + }); + if (!passwordSetting) return false; + + const token = randomBytes(24).toString("base64url"); + const expiresAt = new Date(Date.now() + RESET_TOKEN_LIFETIME_MS).toISOString(); + const record: ResetTokenRecord = { + digest: digestToken(token).toString("hex"), + nonce: randomUUID(), + expiresAt, + }; + + await prisma.setting.upsert({ + where: { key: RESET_TOKEN_KEY }, + update: { value: JSON.stringify(record) }, + create: { key: RESET_TOKEN_KEY, value: JSON.stringify(record) }, + }); + + console.info(`[password-reset] Token: ${token} (expires ${expiresAt})`); + return true; +} + +export async function verifyPasswordResetToken(token: string): Promise { + const setting = await prisma.setting.findUnique({ where: { key: RESET_TOKEN_KEY } }); + const record = parseResetTokenRecord(setting?.value); + if (!record || new Date(record.expiresAt).getTime() <= Date.now()) return false; + + const submittedDigest = digestToken(token); + const storedDigest = Buffer.from(record.digest, "hex"); + if ( + submittedDigest.length !== storedDigest.length || + !timingSafeEqual(submittedDigest, storedDigest) + ) { + return false; + } + + await authorizePasswordReset(record.nonce, record.expiresAt); + return true; +} + +export async function completePasswordReset(password: string): Promise { + const resetSession = await getPasswordResetSession(); + if (!resetSession.nonce || !resetSession.expiresAt) { + await destroyPasswordResetAuthorization(); + return false; + } + + const argon2 = await import("argon2"); + const passwordHash = await argon2.hash(password); + const completed = await prisma.$transaction(async (transaction) => { + const setting = await transaction.setting.findUnique({ + where: { key: RESET_TOKEN_KEY }, + }); + const record = parseResetTokenRecord(setting?.value); + if ( + !record || + resetSession.nonce !== record.nonce || + resetSession.expiresAt !== record.expiresAt || + new Date(record.expiresAt).getTime() <= Date.now() + ) { + return false; + } + + const generationSetting = await transaction.setting.findUnique({ + where: { key: SESSION_GENERATION_KEY }, + }); + const parsedGeneration = Number.parseInt(generationSetting?.value ?? "1", 10); + const currentGeneration = + Number.isSafeInteger(parsedGeneration) && parsedGeneration > 0 + ? parsedGeneration + : 1; + + await transaction.setting.upsert({ + where: { key: PASSWORD_HASH_KEY }, + update: { value: passwordHash }, + create: { key: PASSWORD_HASH_KEY, value: passwordHash }, + }); + await transaction.setting.upsert({ + where: { key: SESSION_GENERATION_KEY }, + update: { value: String(currentGeneration + 1) }, + create: { key: SESSION_GENERATION_KEY, value: String(currentGeneration + 1) }, + }); + await transaction.setting.delete({ where: { key: RESET_TOKEN_KEY } }); + await transaction.authSecurity.upsert({ + where: { id: 1 }, + update: { failedAttempts: 0, lockedUntil: null, lastAttemptAt: new Date() }, + create: { id: 1 }, + }); + return true; + }); + + await destroyPasswordResetAuthorization(); + return completed; +} From dad62c9914d6ac5ae39dc9b1d940b26921483c4e Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 12 Jul 2026 11:44:26 -0700 Subject: [PATCH 04/11] Add study activity tracking and refresh class header UI --- .gitignore | 6 + dev.db | Bin 155648 -> 0 bytes .../migration.sql | 9 + prisma/schema.prisma | 8 + src/app/(protected)/[classSlug]/layout.tsx | 4 +- src/app/(protected)/page.tsx | 27 +- src/app/api/activity/route.ts | 17 + src/components/activity/ActivityBanner.tsx | 199 +++ src/components/flashcards/FlashcardViewer.tsx | 13 +- src/components/quizzes/QuizViewer.tsx | 12 +- src/components/ui/ClassHeader.tsx | 10 +- src/generated/prisma/browser.ts | 5 + src/generated/prisma/client.ts | 5 + src/generated/prisma/internal/class.ts | 18 +- .../prisma/internal/prismaNamespace.ts | 87 +- .../prisma/internal/prismaNamespaceBrowser.ts | 10 + src/generated/prisma/models.ts | 1 + src/generated/prisma/models/StudyActivity.ts | 1091 +++++++++++++++++ src/lib/activityClient.ts | 9 + src/lib/validation/activitySchemas.ts | 7 + src/services/activityService.ts | 104 ++ 21 files changed, 1598 insertions(+), 44 deletions(-) delete mode 100644 dev.db create mode 100644 prisma/migrations/20260712113000_add_study_activity/migration.sql create mode 100644 src/app/api/activity/route.ts create mode 100644 src/components/activity/ActivityBanner.tsx create mode 100644 src/generated/prisma/models/StudyActivity.ts create mode 100644 src/lib/activityClient.ts create mode 100644 src/lib/validation/activitySchemas.ts create mode 100644 src/services/activityService.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..03f032b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,12 @@ # testing /coverage +# local SQLite study data +/dev.db +/dev.db-journal +/dev.db-shm +/dev.db-wal + # next.js /.next/ /out/ diff --git a/dev.db b/dev.db deleted file mode 100644 index 3dc8b2c0b46461bb0b6bead63e88903fe84fc62a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155648 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 bz-IyhGVAA2pNZUld9)J)WdF~jKl=Xx8;TkM diff --git a/prisma/migrations/20260712113000_add_study_activity/migration.sql b/prisma/migrations/20260712113000_add_study_activity/migration.sql new file mode 100644 index 0000000..817ba79 --- /dev/null +++ b/prisma/migrations/20260712113000_add_study_activity/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "StudyActivity" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "occurredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE INDEX "StudyActivity_occurredAt_idx" ON "StudyActivity"("occurredAt"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dc8a941..be359a1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -141,6 +141,14 @@ model Setting { value String } +model StudyActivity { + id String @id @default(uuid()) + type String // "FLASHCARD" | "QUIZ_QUESTION" + occurredAt DateTime @default(now()) + + @@index([occurredAt]) +} + model MaterialGroup { id String @id @default(uuid()) classId String diff --git a/src/app/(protected)/[classSlug]/layout.tsx b/src/app/(protected)/[classSlug]/layout.tsx index 09c5c86..85d64bd 100644 --- a/src/app/(protected)/[classSlug]/layout.tsx +++ b/src/app/(protected)/[classSlug]/layout.tsx @@ -12,8 +12,8 @@ export default async function ClassLayout(props: LayoutProps<"/[classSlug]">) { return (
- {/* Dynamic Header & Tabs */} - + {/* Dynamic class header */} + {/* Page content */} {props.children} diff --git a/src/app/(protected)/page.tsx b/src/app/(protected)/page.tsx index 88ece8c..ba8de20 100644 --- a/src/app/(protected)/page.tsx +++ b/src/app/(protected)/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import Link from "next/link"; +import { ActivityBanner } from "@/components/activity/ActivityBanner"; interface ClassItem { id: string; @@ -56,33 +57,9 @@ export default function HomePage() { } finally { setSavingEdit(false); } } - const deckTotal = classes.reduce((sum, item) => sum + item._count.decks, 0); - const quizTotal = classes.reduce((sum, item) => sum + item._count.quizSets, 0); - return (
-
-
-
-
-
-

Personal library

-

What are we learning today?

-

Pick up where you left off, sharpen a weak topic, or build something new.

-
- -
-
- {[[classes.length, "Classes"], [deckTotal, "Decks"], [quizTotal, "Quizzes"]].map(([value, label]) => ( -
-
{value}
-
{label}
-
- ))} -
-
+ setShowCreate(true)} /> {showCreate && (
diff --git a/src/app/api/activity/route.ts b/src/app/api/activity/route.ts new file mode 100644 index 0000000..570793f --- /dev/null +++ b/src/app/api/activity/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from "next/server"; +import { studyActivitySchema } from "@/lib/validation/activitySchemas"; +import * as activityService from "@/services/activityService"; + +export async function GET() { + return NextResponse.json(await activityService.getActivitySummary()); +} + +export async function POST(request: NextRequest) { + const parsed = studyActivitySchema.safeParse(await request.json().catch(() => null)); + if (!parsed.success) { + return NextResponse.json({ error: "Invalid activity type" }, { status: 400 }); + } + + await activityService.recordActivity(parsed.data.type); + return NextResponse.json({ success: true }, { status: 201 }); +} diff --git a/src/components/activity/ActivityBanner.tsx b/src/components/activity/ActivityBanner.tsx new file mode 100644 index 0000000..0bcaf0a --- /dev/null +++ b/src/components/activity/ActivityBanner.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; + +interface DailyActivity { + date: string; + flashcards: number; + questions: number; + total: number; + level: 0 | 1 | 2 | 3 | 4; +} + +interface ActivitySummary { + days: DailyActivity[]; + today: string; + activeDays: number; + flashcards: number; + questions: number; + currentStreak: number; +} + +const levelClasses = [ + "bg-white/10", + "bg-primary/45", + "bg-primary", + "bg-accent", + "bg-[#fbbf24]", +] as const; + +export function ActivityBanner({ onNewClass }: { onNewClass: () => void }) { + const [summary, setSummary] = useState(null); + const [error, setError] = useState(false); + + useEffect(() => { + fetch("/api/activity") + .then((response) => { + if (!response.ok) throw new Error("Failed to load activity"); + return response.json(); + }) + .then(setSummary) + .catch(() => setError(true)); + }, []); + + return ( +
+
+
+
+
+

Personal library

+

Study activity

+
+ +
+ + {error && ( +
+ Study activity could not be loaded. Your library is still available below. +
+ )} + {!summary && !error &&
} + {summary && ( +
+
+ +
+ + + +
+
+ +
+ )} +
+ ); +} + +function ActivityHeatmap({ days, today }: { days: DailyActivity[]; today: string }) { + const scrollRef = useRef(null); + const [activeDay, setActiveDay] = useState(null); + const weeks = useMemo( + () => Array.from({ length: 53 }, (_, index) => days.slice(index * 7, index * 7 + 7)), + [days] + ); + + useEffect(() => { + const container = scrollRef.current; + if (container) container.scrollLeft = container.scrollWidth; + }, [days]); + + return ( +
+
+

Last 53 weeks · Arizona time

+
+ {activeDay && } +
+
+
+
+
+
+
Mon
+
+
Wed
+
+
Fri
+
+
+
+ {weeks.map((week, index) => ( +
+ {getMonthLabel(week, weeks[index - 1])} +
+ ))} +
+
+ {weeks.map((week, weekIndex) => ( +
+ {week.map((day) => day.date > today ? ( + + ) : ( +
+ ))} +
+
+
+
+
+ Less + {levelClasses.map((className, index) => )} + More +
+
+ ); +} + +function DayTooltip({ day }: { day: DailyActivity }) { + const formatted = new Date(`${day.date}T12:00:00Z`).toLocaleDateString("en-US", { + timeZone: "America/Phoenix", + month: "short", + day: "numeric", + year: "numeric", + }); + return ( +
+
{formatted} · {day.total} total
+
{day.flashcards} flashcards · {day.questions} questions
+
+ ); +} + +function StreakDisplay({ streak }: { streak: number }) { + const size = 24 + Math.min(streak, 30) * 0.6; + const color = streak === 0 ? "text-white/30" : streak < 7 ? "text-accent" : streak < 30 ? "text-[#ff7a36]" : "text-[#fbbf24]"; + return ( +
+ + + +
+
{streak}
+
day streak
+
20 activities per day
+
+
+ ); +} + +function SummaryStat({ value, label }: { value: number; label: string }) { + return
{value}
{label}
; +} + +function getMonthLabel(week: DailyActivity[], previousWeek?: DailyActivity[]) { + const firstOfMonth = week.find((day) => Number(day.date.slice(8, 10)) <= 7); + if (!firstOfMonth) return ""; + const month = firstOfMonth.date.slice(5, 7); + if (previousWeek?.some((day) => day.date.slice(5, 7) === month)) return ""; + return new Date(`${firstOfMonth.date}T12:00:00Z`).toLocaleDateString("en-US", { month: "short", timeZone: "America/Phoenix" }); +} + +function getDayAriaLabel(day: DailyActivity) { + return `${day.date}: ${day.flashcards} flashcards and ${day.questions} questions, ${day.total} total activities`; +} diff --git a/src/components/flashcards/FlashcardViewer.tsx b/src/components/flashcards/FlashcardViewer.tsx index b2f9625..ecb6f15 100644 --- a/src/components/flashcards/FlashcardViewer.tsx +++ b/src/components/flashcards/FlashcardViewer.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import { recordStudyActivity } from "@/lib/activityClient"; interface Card { id: string; @@ -55,6 +56,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre ); const [swipeClass, setSwipeClass] = useState(""); const cardRef = useRef(null); + const gradingRef = useRef(false); // Touch/drag state const dragRef = useRef({ startX: 0, currentX: 0, isDragging: false }); @@ -120,7 +122,12 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre // Grade the current card const gradeCard = useCallback( (grade: CardResult) => { - if (!currentCard || !hasFlippedOnce) return; + if (!currentCard || !hasFlippedOnce || gradingRef.current) return; + gradingRef.current = true; + + if (!isShared) { + recordStudyActivity("FLASHCARD").catch(() => {}); + } setToastMessage(null); const newResults = { ...results, [currentCard.id]: grade }; @@ -130,6 +137,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre setSwipeClass(grade === "correct" ? "animate-swipe-right" : "animate-swipe-left"); setTimeout(() => { + gradingRef.current = false; setSwipeClass(""); setIsFlipped(false); setHasFlippedOnce(false); @@ -144,7 +152,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre } }, 350); }, - [currentCard, hasFlippedOnce, results, currentIndex, order, isShuffled] + [currentCard, hasFlippedOnce, results, currentIndex, order, isShuffled, isShared] ); // Keyboard shortcuts @@ -236,6 +244,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre // Restart handlers function restartFullSet() { + gradingRef.current = false; let newOrder: string[]; if (isShuffled) { newOrder = [...cards.map((c) => c.id)].sort(() => Math.random() - 0.5); diff --git a/src/components/quizzes/QuizViewer.tsx b/src/components/quizzes/QuizViewer.tsx index e14108f..ac2bd97 100644 --- a/src/components/quizzes/QuizViewer.tsx +++ b/src/components/quizzes/QuizViewer.tsx @@ -1,10 +1,11 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { scoreQuestion } from "@/lib/scoring"; import { QuizResults } from "./QuizResults"; +import { recordStudyActivity } from "@/lib/activityClient"; interface Option { id: string; @@ -39,6 +40,7 @@ interface QuizViewerProps { } export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: QuizViewerProps) { + const submittingQuestionRef = useRef(null); const [order, setOrder] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [answers, setAnswers] = useState>({}); @@ -194,7 +196,11 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu } function handleSubmit() { - if (isSubmitted || !currentQId) return; + if (isSubmitted || !currentQId || submittingQuestionRef.current === currentQId) return; + submittingQuestionRef.current = currentQId; + if (!isShared) { + recordStudyActivity("QUIZ_QUESTION").catch(() => {}); + } const newSubmitted = [...submittedAnswers, currentQId]; setSubmittedAnswers(newSubmitted); saveProgress(currentIndex, answers); @@ -202,6 +208,7 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu function handleNext() { if (currentIndex + 1 < order.length) { + submittingQuestionRef.current = null; const nextIdx = currentIndex + 1; setCurrentIndex(nextIdx); saveProgress(nextIdx, answers); @@ -290,6 +297,7 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu setCurrentIndex(0); setAnswers({}); setSubmittedAnswers([]); + submittingQuestionRef.current = null; setResultsData(null); // Re-shuffle options for the new attempt diff --git a/src/components/ui/ClassHeader.tsx b/src/components/ui/ClassHeader.tsx index 2fe8f63..b07ed29 100644 --- a/src/components/ui/ClassHeader.tsx +++ b/src/components/ui/ClassHeader.tsx @@ -2,14 +2,12 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { ClassTabs } from "./ClassTabs"; interface ClassHeaderProps { - classSlug: string; className: string; } -export function ClassHeader({ classSlug, className }: ClassHeaderProps) { +export function ClassHeader({ className }: ClassHeaderProps) { const pathname = usePathname(); // If we are deeper than /[classSlug]/[tab], hide this header to save space @@ -23,8 +21,7 @@ export function ClassHeader({ classSlug, className }: ClassHeaderProps) { } return ( - <> -
+

Choose a mode, then settle in for a focused study session.

- - - ); } diff --git a/src/generated/prisma/browser.ts b/src/generated/prisma/browser.ts index bbd13c9..cffdc48 100644 --- a/src/generated/prisma/browser.ts +++ b/src/generated/prisma/browser.ts @@ -72,6 +72,11 @@ export type AuthSecurity = Prisma.AuthSecurityModel * */ export type Setting = Prisma.SettingModel +/** + * Model StudyActivity + * + */ +export type StudyActivity = Prisma.StudyActivityModel /** * Model MaterialGroup * diff --git a/src/generated/prisma/client.ts b/src/generated/prisma/client.ts index c502699..27a7cbd 100644 --- a/src/generated/prisma/client.ts +++ b/src/generated/prisma/client.ts @@ -96,6 +96,11 @@ export type AuthSecurity = Prisma.AuthSecurityModel * */ export type Setting = Prisma.SettingModel +/** + * Model StudyActivity + * + */ +export type StudyActivity = Prisma.StudyActivityModel /** * Model MaterialGroup * diff --git a/src/generated/prisma/internal/class.ts b/src/generated/prisma/internal/class.ts index 43381b9..de318cc 100644 --- a/src/generated/prisma/internal/class.ts +++ b/src/generated/prisma/internal/class.ts @@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = { "clientVersion": "7.8.0", "engineVersion": "3c6e192761c0362d496ed980de936e2f3cebcd3a", "activeProvider": "sqlite", - "inlineSchema": "datasource db {\n provider = \"sqlite\"\n}\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\nmodel Class {\n id String @id @default(uuid())\n slug String @unique\n name String\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n decks Deck[]\n quizSets QuizSet[]\n materialGroups MaterialGroup[]\n}\n\nmodel Deck {\n id String @id @default(uuid())\n classId String\n name String\n description String?\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n class Class @relation(fields: [classId], references: [id], onDelete: Cascade)\n groupId String?\n group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)\n cards Flashcard[]\n shareLink ShareLink?\n progress StudyProgress[]\n}\n\nmodel Flashcard {\n id String @id @default(uuid())\n deckId String\n front String\n back String\n sortOrder Int @default(0)\n\n deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)\n}\n\nmodel QuizSet {\n id String @id @default(uuid())\n classId String\n name String\n description String?\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n class Class @relation(fields: [classId], references: [id], onDelete: Cascade)\n groupId String?\n group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)\n questions Question[]\n attempts QuizAttempt[]\n shareLink ShareLink?\n progress StudyProgress[]\n}\n\nmodel Question {\n id String @id @default(uuid())\n quizSetId String\n type String // \"MULTIPLE_CHOICE\" | \"SATA\"\n prompt String\n rationale String\n category String\n sortOrder Int @default(0)\n\n quizSet QuizSet @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n options AnswerOption[]\n}\n\nmodel AnswerOption {\n id String @id @default(uuid())\n questionId String\n text String\n isCorrect Boolean @default(false)\n sortOrder Int @default(0)\n\n question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)\n}\n\nmodel StudyProgress {\n id String @id @default(uuid())\n contentType String // \"DECK\" | \"QUIZ\"\n deckId String?\n quizSetId String?\n mode String // \"SEQUENTIAL\" | \"SHUFFLED\"\n currentIndex Int @default(0)\n orderJson String // JSON array of card/question ids\n answersJson String? // in-progress quiz answers\n cardResultsJson String? // per-card grades for decks\n updatedAt DateTime @updatedAt\n\n deck Deck? @relation(fields: [deckId], references: [id], onDelete: Cascade)\n quizSet QuizSet? @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n\n @@unique([deckId, mode])\n @@unique([quizSetId, mode])\n}\n\nmodel QuizAttempt {\n id String @id @default(uuid())\n quizSetId String\n score Float\n maxScore Int\n answersJson String\n isPartialRetake Boolean @default(false)\n completedAt DateTime @default(now())\n\n quizSet QuizSet @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n}\n\nmodel ShareLink {\n id String @id @default(uuid())\n targetType String // \"DECK\" | \"QUIZ\" | \"GROUP\"\n deckId String? @unique\n quizSetId String? @unique\n groupId String? @unique\n createdAt DateTime @default(now())\n\n deck Deck? @relation(fields: [deckId], references: [id], onDelete: Cascade)\n quizSet QuizSet? @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: Cascade)\n}\n\nmodel AuthSecurity {\n id Int @id @default(1)\n failedAttempts Int @default(0)\n lockedUntil DateTime?\n lastAttemptAt DateTime?\n}\n\nmodel Setting {\n key String @id\n value String\n}\n\nmodel MaterialGroup {\n id String @id @default(uuid())\n classId String\n name String\n type String // \"DECK\" | \"QUIZ\"\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n class Class @relation(fields: [classId], references: [id], onDelete: Cascade)\n decks Deck[]\n quizSets QuizSet[]\n shareLink ShareLink?\n}\n", + "inlineSchema": "datasource db {\n provider = \"sqlite\"\n}\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\nmodel Class {\n id String @id @default(uuid())\n slug String @unique\n name String\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n decks Deck[]\n quizSets QuizSet[]\n materialGroups MaterialGroup[]\n}\n\nmodel Deck {\n id String @id @default(uuid())\n classId String\n name String\n description String?\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n class Class @relation(fields: [classId], references: [id], onDelete: Cascade)\n groupId String?\n group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)\n cards Flashcard[]\n shareLink ShareLink?\n progress StudyProgress[]\n}\n\nmodel Flashcard {\n id String @id @default(uuid())\n deckId String\n front String\n back String\n sortOrder Int @default(0)\n\n deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)\n}\n\nmodel QuizSet {\n id String @id @default(uuid())\n classId String\n name String\n description String?\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n class Class @relation(fields: [classId], references: [id], onDelete: Cascade)\n groupId String?\n group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)\n questions Question[]\n attempts QuizAttempt[]\n shareLink ShareLink?\n progress StudyProgress[]\n}\n\nmodel Question {\n id String @id @default(uuid())\n quizSetId String\n type String // \"MULTIPLE_CHOICE\" | \"SATA\"\n prompt String\n rationale String\n category String\n sortOrder Int @default(0)\n\n quizSet QuizSet @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n options AnswerOption[]\n}\n\nmodel AnswerOption {\n id String @id @default(uuid())\n questionId String\n text String\n isCorrect Boolean @default(false)\n sortOrder Int @default(0)\n\n question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)\n}\n\nmodel StudyProgress {\n id String @id @default(uuid())\n contentType String // \"DECK\" | \"QUIZ\"\n deckId String?\n quizSetId String?\n mode String // \"SEQUENTIAL\" | \"SHUFFLED\"\n currentIndex Int @default(0)\n orderJson String // JSON array of card/question ids\n answersJson String? // in-progress quiz answers\n cardResultsJson String? // per-card grades for decks\n updatedAt DateTime @updatedAt\n\n deck Deck? @relation(fields: [deckId], references: [id], onDelete: Cascade)\n quizSet QuizSet? @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n\n @@unique([deckId, mode])\n @@unique([quizSetId, mode])\n}\n\nmodel QuizAttempt {\n id String @id @default(uuid())\n quizSetId String\n score Float\n maxScore Int\n answersJson String\n isPartialRetake Boolean @default(false)\n completedAt DateTime @default(now())\n\n quizSet QuizSet @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n}\n\nmodel ShareLink {\n id String @id @default(uuid())\n targetType String // \"DECK\" | \"QUIZ\" | \"GROUP\"\n deckId String? @unique\n quizSetId String? @unique\n groupId String? @unique\n createdAt DateTime @default(now())\n\n deck Deck? @relation(fields: [deckId], references: [id], onDelete: Cascade)\n quizSet QuizSet? @relation(fields: [quizSetId], references: [id], onDelete: Cascade)\n group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: Cascade)\n}\n\nmodel AuthSecurity {\n id Int @id @default(1)\n failedAttempts Int @default(0)\n lockedUntil DateTime?\n lastAttemptAt DateTime?\n}\n\nmodel Setting {\n key String @id\n value String\n}\n\nmodel StudyActivity {\n id String @id @default(uuid())\n type String // \"FLASHCARD\" | \"QUIZ_QUESTION\"\n occurredAt DateTime @default(now())\n\n @@index([occurredAt])\n}\n\nmodel MaterialGroup {\n id String @id @default(uuid())\n classId String\n name String\n type String // \"DECK\" | \"QUIZ\"\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n\n class Class @relation(fields: [classId], references: [id], onDelete: Cascade)\n decks Deck[]\n quizSets QuizSet[]\n shareLink ShareLink?\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -32,10 +32,10 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"Class\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"slug\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"decks\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"ClassToDeck\"},{\"name\":\"quizSets\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"ClassToQuizSet\"},{\"name\":\"materialGroups\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"ClassToMaterialGroup\"}],\"dbName\":null},\"Deck\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"classId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"class\",\"kind\":\"object\",\"type\":\"Class\",\"relationName\":\"ClassToDeck\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"DeckToMaterialGroup\"},{\"name\":\"cards\",\"kind\":\"object\",\"type\":\"Flashcard\",\"relationName\":\"DeckToFlashcard\"},{\"name\":\"shareLink\",\"kind\":\"object\",\"type\":\"ShareLink\",\"relationName\":\"DeckToShareLink\"},{\"name\":\"progress\",\"kind\":\"object\",\"type\":\"StudyProgress\",\"relationName\":\"DeckToStudyProgress\"}],\"dbName\":null},\"Flashcard\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deckId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"front\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"back\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"deck\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToFlashcard\"}],\"dbName\":null},\"QuizSet\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"classId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"class\",\"kind\":\"object\",\"type\":\"Class\",\"relationName\":\"ClassToQuizSet\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"MaterialGroupToQuizSet\"},{\"name\":\"questions\",\"kind\":\"object\",\"type\":\"Question\",\"relationName\":\"QuestionToQuizSet\"},{\"name\":\"attempts\",\"kind\":\"object\",\"type\":\"QuizAttempt\",\"relationName\":\"QuizAttemptToQuizSet\"},{\"name\":\"shareLink\",\"kind\":\"object\",\"type\":\"ShareLink\",\"relationName\":\"QuizSetToShareLink\"},{\"name\":\"progress\",\"kind\":\"object\",\"type\":\"StudyProgress\",\"relationName\":\"QuizSetToStudyProgress\"}],\"dbName\":null},\"Question\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"prompt\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rationale\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"category\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuestionToQuizSet\"},{\"name\":\"options\",\"kind\":\"object\",\"type\":\"AnswerOption\",\"relationName\":\"AnswerOptionToQuestion\"}],\"dbName\":null},\"AnswerOption\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"questionId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"text\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isCorrect\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"question\",\"kind\":\"object\",\"type\":\"Question\",\"relationName\":\"AnswerOptionToQuestion\"}],\"dbName\":null},\"StudyProgress\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"contentType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deckId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"mode\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"currentIndex\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"orderJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"answersJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cardResultsJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deck\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToStudyProgress\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuizSetToStudyProgress\"}],\"dbName\":null},\"QuizAttempt\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"score\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"maxScore\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"answersJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isPartialRetake\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"completedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuizAttemptToQuizSet\"}],\"dbName\":null},\"ShareLink\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"targetType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deckId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deck\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToShareLink\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuizSetToShareLink\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"MaterialGroupToShareLink\"}],\"dbName\":null},\"AuthSecurity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"failedAttempts\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"lockedUntil\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"lastAttemptAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Setting\":{\"fields\":[{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"MaterialGroup\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"classId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"class\",\"kind\":\"object\",\"type\":\"Class\",\"relationName\":\"ClassToMaterialGroup\"},{\"name\":\"decks\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToMaterialGroup\"},{\"name\":\"quizSets\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"MaterialGroupToQuizSet\"},{\"name\":\"shareLink\",\"kind\":\"object\",\"type\":\"ShareLink\",\"relationName\":\"MaterialGroupToShareLink\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"Class\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"slug\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"decks\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"ClassToDeck\"},{\"name\":\"quizSets\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"ClassToQuizSet\"},{\"name\":\"materialGroups\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"ClassToMaterialGroup\"}],\"dbName\":null},\"Deck\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"classId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"class\",\"kind\":\"object\",\"type\":\"Class\",\"relationName\":\"ClassToDeck\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"DeckToMaterialGroup\"},{\"name\":\"cards\",\"kind\":\"object\",\"type\":\"Flashcard\",\"relationName\":\"DeckToFlashcard\"},{\"name\":\"shareLink\",\"kind\":\"object\",\"type\":\"ShareLink\",\"relationName\":\"DeckToShareLink\"},{\"name\":\"progress\",\"kind\":\"object\",\"type\":\"StudyProgress\",\"relationName\":\"DeckToStudyProgress\"}],\"dbName\":null},\"Flashcard\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deckId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"front\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"back\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"deck\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToFlashcard\"}],\"dbName\":null},\"QuizSet\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"classId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"class\",\"kind\":\"object\",\"type\":\"Class\",\"relationName\":\"ClassToQuizSet\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"MaterialGroupToQuizSet\"},{\"name\":\"questions\",\"kind\":\"object\",\"type\":\"Question\",\"relationName\":\"QuestionToQuizSet\"},{\"name\":\"attempts\",\"kind\":\"object\",\"type\":\"QuizAttempt\",\"relationName\":\"QuizAttemptToQuizSet\"},{\"name\":\"shareLink\",\"kind\":\"object\",\"type\":\"ShareLink\",\"relationName\":\"QuizSetToShareLink\"},{\"name\":\"progress\",\"kind\":\"object\",\"type\":\"StudyProgress\",\"relationName\":\"QuizSetToStudyProgress\"}],\"dbName\":null},\"Question\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"prompt\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rationale\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"category\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuestionToQuizSet\"},{\"name\":\"options\",\"kind\":\"object\",\"type\":\"AnswerOption\",\"relationName\":\"AnswerOptionToQuestion\"}],\"dbName\":null},\"AnswerOption\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"questionId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"text\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isCorrect\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"question\",\"kind\":\"object\",\"type\":\"Question\",\"relationName\":\"AnswerOptionToQuestion\"}],\"dbName\":null},\"StudyProgress\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"contentType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deckId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"mode\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"currentIndex\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"orderJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"answersJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cardResultsJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deck\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToStudyProgress\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuizSetToStudyProgress\"}],\"dbName\":null},\"QuizAttempt\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"score\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"maxScore\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"answersJson\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isPartialRetake\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"completedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuizAttemptToQuizSet\"}],\"dbName\":null},\"ShareLink\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"targetType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deckId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quizSetId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deck\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToShareLink\"},{\"name\":\"quizSet\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"QuizSetToShareLink\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"MaterialGroup\",\"relationName\":\"MaterialGroupToShareLink\"}],\"dbName\":null},\"AuthSecurity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"failedAttempts\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"lockedUntil\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"lastAttemptAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Setting\":{\"fields\":[{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"StudyActivity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"occurredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"MaterialGroup\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"classId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"class\",\"kind\":\"object\",\"type\":\"Class\",\"relationName\":\"ClassToMaterialGroup\"},{\"name\":\"decks\",\"kind\":\"object\",\"type\":\"Deck\",\"relationName\":\"DeckToMaterialGroup\"},{\"name\":\"quizSets\",\"kind\":\"object\",\"type\":\"QuizSet\",\"relationName\":\"MaterialGroupToQuizSet\"},{\"name\":\"shareLink\",\"kind\":\"object\",\"type\":\"ShareLink\",\"relationName\":\"MaterialGroupToShareLink\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") config.parameterizationSchema = { - strings: JSON.parse("[\"where\",\"orderBy\",\"cursor\",\"class\",\"decks\",\"group\",\"quizSet\",\"question\",\"options\",\"_count\",\"questions\",\"attempts\",\"deck\",\"shareLink\",\"progress\",\"quizSets\",\"cards\",\"materialGroups\",\"Class.findUnique\",\"Class.findUniqueOrThrow\",\"Class.findFirst\",\"Class.findFirstOrThrow\",\"Class.findMany\",\"data\",\"Class.createOne\",\"Class.createMany\",\"Class.createManyAndReturn\",\"Class.updateOne\",\"Class.updateMany\",\"Class.updateManyAndReturn\",\"create\",\"update\",\"Class.upsertOne\",\"Class.deleteOne\",\"Class.deleteMany\",\"having\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"Class.groupBy\",\"Class.aggregate\",\"Deck.findUnique\",\"Deck.findUniqueOrThrow\",\"Deck.findFirst\",\"Deck.findFirstOrThrow\",\"Deck.findMany\",\"Deck.createOne\",\"Deck.createMany\",\"Deck.createManyAndReturn\",\"Deck.updateOne\",\"Deck.updateMany\",\"Deck.updateManyAndReturn\",\"Deck.upsertOne\",\"Deck.deleteOne\",\"Deck.deleteMany\",\"Deck.groupBy\",\"Deck.aggregate\",\"Flashcard.findUnique\",\"Flashcard.findUniqueOrThrow\",\"Flashcard.findFirst\",\"Flashcard.findFirstOrThrow\",\"Flashcard.findMany\",\"Flashcard.createOne\",\"Flashcard.createMany\",\"Flashcard.createManyAndReturn\",\"Flashcard.updateOne\",\"Flashcard.updateMany\",\"Flashcard.updateManyAndReturn\",\"Flashcard.upsertOne\",\"Flashcard.deleteOne\",\"Flashcard.deleteMany\",\"Flashcard.groupBy\",\"Flashcard.aggregate\",\"QuizSet.findUnique\",\"QuizSet.findUniqueOrThrow\",\"QuizSet.findFirst\",\"QuizSet.findFirstOrThrow\",\"QuizSet.findMany\",\"QuizSet.createOne\",\"QuizSet.createMany\",\"QuizSet.createManyAndReturn\",\"QuizSet.updateOne\",\"QuizSet.updateMany\",\"QuizSet.updateManyAndReturn\",\"QuizSet.upsertOne\",\"QuizSet.deleteOne\",\"QuizSet.deleteMany\",\"QuizSet.groupBy\",\"QuizSet.aggregate\",\"Question.findUnique\",\"Question.findUniqueOrThrow\",\"Question.findFirst\",\"Question.findFirstOrThrow\",\"Question.findMany\",\"Question.createOne\",\"Question.createMany\",\"Question.createManyAndReturn\",\"Question.updateOne\",\"Question.updateMany\",\"Question.updateManyAndReturn\",\"Question.upsertOne\",\"Question.deleteOne\",\"Question.deleteMany\",\"Question.groupBy\",\"Question.aggregate\",\"AnswerOption.findUnique\",\"AnswerOption.findUniqueOrThrow\",\"AnswerOption.findFirst\",\"AnswerOption.findFirstOrThrow\",\"AnswerOption.findMany\",\"AnswerOption.createOne\",\"AnswerOption.createMany\",\"AnswerOption.createManyAndReturn\",\"AnswerOption.updateOne\",\"AnswerOption.updateMany\",\"AnswerOption.updateManyAndReturn\",\"AnswerOption.upsertOne\",\"AnswerOption.deleteOne\",\"AnswerOption.deleteMany\",\"AnswerOption.groupBy\",\"AnswerOption.aggregate\",\"StudyProgress.findUnique\",\"StudyProgress.findUniqueOrThrow\",\"StudyProgress.findFirst\",\"StudyProgress.findFirstOrThrow\",\"StudyProgress.findMany\",\"StudyProgress.createOne\",\"StudyProgress.createMany\",\"StudyProgress.createManyAndReturn\",\"StudyProgress.updateOne\",\"StudyProgress.updateMany\",\"StudyProgress.updateManyAndReturn\",\"StudyProgress.upsertOne\",\"StudyProgress.deleteOne\",\"StudyProgress.deleteMany\",\"StudyProgress.groupBy\",\"StudyProgress.aggregate\",\"QuizAttempt.findUnique\",\"QuizAttempt.findUniqueOrThrow\",\"QuizAttempt.findFirst\",\"QuizAttempt.findFirstOrThrow\",\"QuizAttempt.findMany\",\"QuizAttempt.createOne\",\"QuizAttempt.createMany\",\"QuizAttempt.createManyAndReturn\",\"QuizAttempt.updateOne\",\"QuizAttempt.updateMany\",\"QuizAttempt.updateManyAndReturn\",\"QuizAttempt.upsertOne\",\"QuizAttempt.deleteOne\",\"QuizAttempt.deleteMany\",\"QuizAttempt.groupBy\",\"QuizAttempt.aggregate\",\"ShareLink.findUnique\",\"ShareLink.findUniqueOrThrow\",\"ShareLink.findFirst\",\"ShareLink.findFirstOrThrow\",\"ShareLink.findMany\",\"ShareLink.createOne\",\"ShareLink.createMany\",\"ShareLink.createManyAndReturn\",\"ShareLink.updateOne\",\"ShareLink.updateMany\",\"ShareLink.updateManyAndReturn\",\"ShareLink.upsertOne\",\"ShareLink.deleteOne\",\"ShareLink.deleteMany\",\"ShareLink.groupBy\",\"ShareLink.aggregate\",\"AuthSecurity.findUnique\",\"AuthSecurity.findUniqueOrThrow\",\"AuthSecurity.findFirst\",\"AuthSecurity.findFirstOrThrow\",\"AuthSecurity.findMany\",\"AuthSecurity.createOne\",\"AuthSecurity.createMany\",\"AuthSecurity.createManyAndReturn\",\"AuthSecurity.updateOne\",\"AuthSecurity.updateMany\",\"AuthSecurity.updateManyAndReturn\",\"AuthSecurity.upsertOne\",\"AuthSecurity.deleteOne\",\"AuthSecurity.deleteMany\",\"AuthSecurity.groupBy\",\"AuthSecurity.aggregate\",\"Setting.findUnique\",\"Setting.findUniqueOrThrow\",\"Setting.findFirst\",\"Setting.findFirstOrThrow\",\"Setting.findMany\",\"Setting.createOne\",\"Setting.createMany\",\"Setting.createManyAndReturn\",\"Setting.updateOne\",\"Setting.updateMany\",\"Setting.updateManyAndReturn\",\"Setting.upsertOne\",\"Setting.deleteOne\",\"Setting.deleteMany\",\"Setting.groupBy\",\"Setting.aggregate\",\"MaterialGroup.findUnique\",\"MaterialGroup.findUniqueOrThrow\",\"MaterialGroup.findFirst\",\"MaterialGroup.findFirstOrThrow\",\"MaterialGroup.findMany\",\"MaterialGroup.createOne\",\"MaterialGroup.createMany\",\"MaterialGroup.createManyAndReturn\",\"MaterialGroup.updateOne\",\"MaterialGroup.updateMany\",\"MaterialGroup.updateManyAndReturn\",\"MaterialGroup.upsertOne\",\"MaterialGroup.deleteOne\",\"MaterialGroup.deleteMany\",\"MaterialGroup.groupBy\",\"MaterialGroup.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"classId\",\"name\",\"type\",\"sortOrder\",\"createdAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"key\",\"value\",\"failedAttempts\",\"lockedUntil\",\"lastAttemptAt\",\"targetType\",\"deckId\",\"quizSetId\",\"groupId\",\"score\",\"maxScore\",\"answersJson\",\"isPartialRetake\",\"completedAt\",\"contentType\",\"mode\",\"currentIndex\",\"orderJson\",\"cardResultsJson\",\"updatedAt\",\"questionId\",\"text\",\"isCorrect\",\"prompt\",\"rationale\",\"category\",\"description\",\"front\",\"back\",\"slug\",\"every\",\"some\",\"none\",\"deckId_mode\",\"quizSetId_mode\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"disconnect\",\"delete\",\"connect\",\"createMany\",\"set\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), - graph: "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_QEBAOgCACH-AQIA7wIAIf8BAQDoAgAhgAIBAJMDACGBAkAA9gIAIQYGAADNBAAgDAAAzAQAIPQBAADCBAAg9QEAAMIEACD5AQAAwgQAIIACAADCBAAgEQYAAPgCACAMAAD3AgAg2gEAAJIDADDbAQAAIQAQ3AEAAJIDADDdAQEAAAAB9AEBAJMDACH1AQEAkwMAIfkBAQCTAwAh_AEBAOgCACH9AQEA6AIAIf4BAgDvAgAh_wEBAOgCACGAAgEAkwMAIYECQAD2AgAhjwIAAJADACCQAgAAkQMAIAMAAAAhACABAAAiADACAAAjACABAAAAAwAgAQAAAAoAIAEAAAAPACABAAAAGAAgAQAAACEAIAEAAAAcACABAAAAAwAgAQAAAAoAIAkMAACPAwAg2gEAAI4DADDbAQAALQAQ3AEAAI4DADDdAQEA6AIAIeEBAgDvAgAh9AEBAOgCACGJAgEA6AIAIYoCAQDoAgAhAQwAAMwEACAJDAAAjwMAINoBAACOAwAw2wEAAC0AENwBAACOAwAw3QEBAAAAAeEBAgDvAgAh9AEBAOgCACGJAgEA6AIAIYoCAQDoAgAhAwAAAC0AIAEAAC4AMAIAAC8AIAEAAAAcACADAAAAIQAgAQAAIgAwAgAAIwAgAQAAAC0AIAEAAAAhACADAAAACgAgAQAACwAwAgAADAAgBAMAAKoFACAEAACnBQAgDQAAqwUAIA8AAKgFACANAwAAjAMAIAQAAIgDACANAACNAwAgDwAAiQMAINoBAACLAwAw2wEAAAcAENwBAACLAwAw3QEBAAAAAd4BAQDoAgAh3wEBAOgCACHgAQEA6AIAIeEBAgDvAgAh4gFAAPYCACEDAAAABwAgAQAANgAwAgAANwAgAQAAAAMAIAEAAAAKACABAAAABwAgAQAAAAEAIAsEAACIAwAgDwAAiQMAIBEAAIoDACDaAQAAhwMAMNsBAAA9ABDcAQAAhwMAMN0BAQDoAgAh3wEBAOgCACHhAQIA7wIAIeIBQAD2AgAhiwIBAOgCACEDBAAApwUAIA8AAKgFACARAACpBQAgAwAAAD0AIAEAAD4AMAIAAAEAIAMAAAA9ACABAAA-ADACAAABACADAAAAPQAgAQAAPgAwAgAAAQAgCAQAAKQFACAPAAClBQAgEQAApgUAIN0BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAGLAgEAAAABARcAAEIAIAXdAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAABiwIBAAAAAQEXAABEADABFwAARAAwCAQAAIMFACAPAACEBQAgEQAAhQUAIN0BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiwIBAKcDACECAAAAAQAgFwAARwAgBd0BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiwIBAKcDACECAAAAPQAgFwAASQAgAgAAAD0AIBcAAEkAIAMAAAABACAeAABCACAfAABHACABAAAAAQAgAQAAAD0AIAUJAAD-BAAgJAAA_wQAICUAAIIFACAmAACBBQAgJwAAgAUAIAjaAQAAhgMAMNsBAABQABDcAQAAhgMAMN0BAQDcAgAh3wEBANwCACHhAQIA3QIAIeIBQADeAgAhiwIBANwCACEDAAAAPQAgAQAATwAwIwAAUAAgAwAAAD0AIAEAAD4AMAIAAAEAIAEAAAAFACABAAAABQAgAwAAAAMAIAEAAAQAMAIAAAUAIAMAAAADACABAAAEADACAAAFACADAAAAAwAgAQAABAAwAgAABQAgDAMAALcEACAFAAD9BAAgDQAAuQQAIA4AALoEACAQAAC4BAAg3QEBAAAAAd4BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAH2AQEAAAABiAIBAAAAAQEXAABYACAH3QEBAAAAAd4BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAH2AQEAAAABiAIBAAAAAQEXAABaADABFwAAWgAwAQAAAAcAIAwDAACWBAAgBQAA_AQAIA0AAJgEACAOAACZBAAgEAAAlwQAIN0BAQCnAwAh3gEBAKcDACHfAQEApwMAIeEBAgCoAwAh4gFAAKkDACH2AQEAtQMAIYgCAQC1AwAhAgAAAAUAIBcAAF4AIAfdAQEApwMAId4BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAh9gEBALUDACGIAgEAtQMAIQIAAAADACAXAABgACACAAAAAwAgFwAAYAAgAQAAAAcAIAMAAAAFACAeAABYACAfAABeACABAAAABQAgAQAAAAMAIAcJAAD3BAAgJAAA-AQAICUAAPsEACAmAAD6BAAgJwAA-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-QEBAAAAAfwBAQAAAAH9AQEAAAAB_gECAAAAAf8BAQAAAAGAAgEAAAABgQJAAAAAAQEXAADKAQAgCt0BAQAAAAH0AQEAAAAB9QEBAAAAAfkBAQAAAAH8AQEAAAAB_QEBAAAAAf4BAgAAAAH_AQEAAAABgAIBAAAAAYECQAAAAAEBFwAAzAEAMAEXAADMAQAwAQAAAAMAIAEAAAAKACAMBgAAogQAIAwAANMDACDdAQEApwMAIfQBAQC1AwAh9QEBALUDACH5AQEAtQMAIfwBAQCnAwAh_QEBAKcDACH-AQIAqAMAIf8BAQCnAwAhgAIBALUDACGBAkAAqQMAIQIAAAAjACAXAADRAQAgCt0BAQCnAwAh9AEBALUDACH1AQEAtQMAIfkBAQC1AwAh_AEBAKcDACH9AQEApwMAIf4BAgCoAwAh_wEBAKcDACGAAgEAtQMAIYECQACpAwAhAgAAACEAIBcAANMBACACAAAAIQAgFwAA0wEAIAEAAAADACABAAAACgAgAwAAACMAIB4AAMoBACAfAADRAQAgAQAAACMAIAEAAAAhACAJCQAA1gQAICQAANcEACAlAADaBAAgJgAA2QQAICcAANgEACD0AQAAwgQAIPUBAADCBAAg-QEAAMIEACCAAgAAwgQAIA3aAQAAgAMAMNsBAADcAQAQ3AEAAIADADDdAQEA3AIAIfQBAQDyAgAh9QEBAPICACH5AQEA8gIAIfwBAQDcAgAh_QEBANwCACH-AQIA3QIAIf8BAQDcAgAhgAIBAPICACGBAkAA3gIAIQMAAAAhACABAADbAQAwIwAA3AEAIAMAAAAhACABAAAiADACAAAjACABAAAAGgAgAQAAABoAIAMAAAAYACABAAAZADACAAAaACADAAAAGAAgAQAAGQAwAgAAGgAgAwAAABgAIAEAABkAMAIAABoAIAgGAADVBAAg3QEBAAAAAfUBAQAAAAH3AQgAAAAB-AECAAAAAfkBAQAAAAH6ASAAAAAB-wFAAAAAAQEXAADkAQAgB90BAQAAAAH1AQEAAAAB9wEIAAAAAfgBAgAAAAH5AQEAAAAB-gEgAAAAAfsBQAAAAAEBFwAA5gEAMAEXAADmAQAwCAYAANQEACDdAQEApwMAIfUBAQCnAwAh9wEIAOcDACH4AQIAqAMAIfkBAQCnAwAh-gEgAOgDACH7AUAAqQMAIQIAAAAaACAXAADpAQAgB90BAQCnAwAh9QEBAKcDACH3AQgA5wMAIfgBAgCoAwAh-QEBAKcDACH6ASAA6AMAIfsBQACpAwAhAgAAABgAIBcAAOsBACACAAAAGAAgFwAA6wEAIAMAAAAaACAeAADkAQAgHwAA6QEAIAEAAAAaACABAAAAGAAgBQkAAM8EACAkAADQBAAgJQAA0wQAICYAANIEACAnAADRBAAgCtoBAAD6AgAw2wEAAPIBABDcAQAA-gIAMN0BAQDcAgAh9QEBANwCACH3AQgA-wIAIfgBAgDdAgAh-QEBANwCACH6ASAA_AIAIfsBQADeAgAhAwAAABgAIAEAAPEBADAjAADyAQAgAwAAABgAIAEAABkAMAIAABoAIAwFAAD5AgAgBgAA-AIAIAwAAPcCACDaAQAA9QIAMNsBAAAcABDcAQAA9QIAMN0BAQAAAAHiAUAA9gIAIfMBAQDoAgAh9AEBAAAAAfUBAQAAAAH2AQEAAAABAQAAAPUBACABAAAA9QEAIAYFAADOBAAgBgAAzQQAIAwAAMwEACD0AQAAwgQAIPUBAADCBAAg9gEAAMIEACADAAAAHAAgAQAA-AEAMAIAAPUBACADAAAAHAAgAQAA-AEAMAIAAPUBACADAAAAHAAgAQAA-AEAMAIAAPUBACAJBQAA3AMAIAYAALcDACAMAAC2AwAg3QEBAAAAAeIBQAAAAAHzAQEAAAAB9AEBAAAAAfUBAQAAAAH2AQEAAAABARcAAPwBACAG3QEBAAAAAeIBQAAAAAHzAQEAAAAB9AEBAAAAAfUBAQAAAAH2AQEAAAABARcAAP4BADABFwAA_gEAMAEAAAADACABAAAACgAgAQAAAAcAIAkFAADbAwAgBgAAtAMAIAwAALMDACDdAQEApwMAIeIBQACpAwAh8wEBAKcDACH0AQEAtQMAIfUBAQC1AwAh9gEBALUDACECAAAA9QEAIBcAAIQCACAG3QEBAKcDACHiAUAAqQMAIfMBAQCnAwAh9AEBALUDACH1AQEAtQMAIfYBAQC1AwAhAgAAABwAIBcAAIYCACACAAAAHAAgFwAAhgIAIAEAAAADACABAAAACgAgAQAAAAcAIAMAAAD1AQAgHgAA_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-gIAMN0BAQDcAgAh9QEBANwCACH3AQgA-wIAIfgBAgDdAgAh-QEBANwCACH6ASAA_AIAIfsBQADeAgAhDQkAAOACACAkAADjAgAgJQAA4wIAICYAAOMCACAnAADjAgAg4wEIAAAAAeQBCAAAAATlAQgAAAAE5gEIAAAAAecBCAAAAAHoAQgAAAAB6QEIAAAAAeoBCAD_AgAhBQkAAOACACAmAAD-AgAgJwAA_gIAIOMBIAAAAAHqASAA_QIAIQUJAADgAgAgJgAA_gIAICcAAP4CACDjASAAAAAB6gEgAP0CACEC4wEgAAAAAeoBIAD-AgAhDQkAAOACACAkAADjAgAgJQAA4wIAICYAAOMCACAnAADjAgAg4wEIAAAAAeQBCAAAAATlAQgAAAAE5gEIAAAAAecBCAAAAAHoAQgAAAAB6QEIAAAAAeoBCAD_AgAhDdoBAACAAwAw2wEAANwBABDcAQAAgAMAMN0BAQDcAgAh9AEBAPICACH1AQEA8gIAIfkBAQDyAgAh_AEBANwCACH9AQEA3AIAIf4BAgDdAgAh_wEBANwCACGAAgEA8gIAIYECQADeAgAhCNoBAACBAwAw2wEAAMIBABDcAQAAgQMAMN0BAQDcAgAh4QECAN0CACGCAgEA3AIAIYMCAQDcAgAhhAIgAPwCACEK2gEAAIIDADDbAQAArAEAENwBAACCAwAw3QEBANwCACHgAQEA3AIAIeEBAgDdAgAh9QEBANwCACGFAgEA3AIAIYYCAQDcAgAhhwIBANwCACEK2gEAAIMDADDbAQAAlgEAENwBAACDAwAw3QEBANwCACHeAQEA3AIAId8BAQDcAgAh4QECAN0CACHiAUAA3gIAIfYBAQDyAgAhiAIBAPICACEI2gEAAIQDADDbAQAAfgAQ3AEAAIQDADDdAQEA3AIAIeEBAgDdAgAh9AEBANwCACGJAgEA3AIAIYoCAQDcAgAhCtoBAACFAwAw2wEAAGgAENwBAACFAwAw3QEBANwCACHeAQEA3AIAId8BAQDcAgAh4QECAN0CACHiAUAA3gIAIfYBAQDyAgAhiAIBAPICACEI2gEAAIYDADDbAQAAUAAQ3AEAAIYDADDdAQEA3AIAId8BAQDcAgAh4QECAN0CACHiAUAA3gIAIYsCAQDcAgAhCwQAAIgDACAPAACJAwAgEQAAigMAINoBAACHAwAw2wEAAD0AENwBAACHAwAw3QEBAOgCACHfAQEA6AIAIeEBAgDvAgAh4gFAAPYCACGLAgEA6AIAIQOMAgAAAwAgjQIAAAMAII4CAAADACADjAIAAAoAII0CAAAKACCOAgAACgAgA4wCAAAHACCNAgAABwAgjgIAAAcAIA0DAACMAwAgBAAAiAMAIA0AAI0DACAPAACJAwAg2gEAAIsDADDbAQAABwAQ3AEAAIsDADDdAQEA6AIAId4BAQDoAgAh3wEBAOgCACHgAQEA6AIAIeEBAgDvAgAh4gFAAPYCACENBAAAiAMAIA8AAIkDACARAACKAwAg2gEAAIcDADDbAQAAPQAQ3AEAAIcDADDdAQEA6AIAId8BAQDoAgAh4QECAO8CACHiAUAA9gIAIYsCAQDoAgAhkQIAAD0AIJICAAA9ACAOBQAA-QIAIAYAAPgCACAMAAD3AgAg2gEAAPUCADDbAQAAHAAQ3AEAAPUCADDdAQEA6AIAIeIBQAD2AgAh8wEBAOgCACH0AQEAkwMAIfUBAQCTAwAh9gEBAJMDACGRAgAAHAAgkgIAABwAIAkMAACPAwAg2gEAAI4DADDbAQAALQAQ3AEAAI4DADDdAQEA6AIAIeEBAgDvAgAh9AEBAOgCACGJAgEA6AIAIYoCAQDoAgAhEQMAAIwDACAFAAD5AgAgDQAAjQMAIA4AAJ8DACAQAAChAwAg2gEAAKADADDbAQAAAwAQ3AEAAKADADDdAQEA6AIAId4BAQDoAgAh3wEBAOgCACHhAQIA7wIAIeIBQAD2AgAh9gEBAJMDACGIAgEAkwMAIZECAAADACCSAgAAAwAgAvQBAQAAAAH9AQEAAAABAvUBAQAAAAH9AQEAAAABDwYAAPgCACAMAAD3AgAg2gEAAJIDADDbAQAAIQAQ3AEAAJIDADDdAQEA6AIAIfQBAQCTAwAh9QEBAJMDACH5AQEAkwMAIfwBAQDoAgAh_QEBAOgCACH-AQIA7wIAIf8BAQDoAgAhgAIBAJMDACGBAkAA9gIAIQvjAQEAAAAB5AEBAAAABeUBAQAAAAXmAQEAAAAB5wEBAAAAAegBAQAAAAHpAQEAAAAB6gEBAPQCACHrAQEAAAAB7AEBAAAAAe0BAQAAAAELBgAAlwMAINoBAACUAwAw2wEAABgAENwBAACUAwAw3QEBAOgCACH1AQEA6AIAIfcBCACVAwAh-AECAO8CACH5AQEA6AIAIfoBIACWAwAh-wFAAPYCACEI4wEIAAAAAeQBCAAAAATlAQgAAAAE5gEIAAAAAecBCAAAAAHoAQgAAAAB6QEIAAAAAeoBCADjAgAhAuMBIAAAAAHqASAA_gIAIRIDAACMAwAgBQAA-QIAIAoAAJ0DACALAACeAwAgDQAAjQMAIA4AAJ8DACDaAQAAnAMAMNsBAAAKABDcAQAAnAMAMN0BAQDoAgAh3gEBAOgCACHfAQEA6AIAIeEBAgDvAgAh4gFAAPYCACH2AQEAkwMAIYgCAQCTAwAhkQIAAAoAIJICAAAKACAJBwAAmQMAINoBAACYAwAw2wEAABMAENwBAACYAwAw3QEBAOgCACHhAQIA7wIAIYICAQDoAgAhgwIBAOgCACGEAiAAlgMAIQ4GAACXAwAgCAAAmwMAINoBAACaAwAw2wEAAA8AENwBAACaAwAw3QEBAOgCACHgAQEA6AIAIeEBAgDvAgAh9QEBAOgCACGFAgEA6AIAIYYCAQDoAgAhhwIBAOgCACGRAgAADwAgkgIAAA8AIAwGAACXAwAgCAAAmwMAINoBAACaAwAw2wEAAA8AENwBAACaAwAw3QEBAOgCACHgAQEA6AIAIeEBAgDvAgAh9QEBAOgCACGFAgEA6AIAIYYCAQDoAgAhhwIBAOgCACEDjAIAABMAII0CAAATACCOAgAAEwAgEAMAAIwDACAFAAD5AgAgCgAAnQMAIAsAAJ4DACANAACNAwAgDgAAnwMAINoBAACcAwAw2wEAAAoAENwBAACcAwAw3QEBAOgCACHeAQEA6AIAId8BAQDoAgAh4QECAO8CACHiAUAA9gIAIfYBAQCTAwAhiAIBAJMDACEDjAIAAA8AII0CAAAPACCOAgAADwAgA4wCAAAYACCNAgAAGAAgjgIAABgAIAOMAgAAIQAgjQIAACEAII4CAAAhACAPAwAAjAMAIAUAAPkCACANAACNAwAgDgAAnwMAIBAAAKEDACDaAQAAoAMAMNsBAAADABDcAQAAoAMAMN0BAQDoAgAh3gEBAOgCACHfAQEA6AIAIeEBAgDvAgAh4gFAAPYCACH2AQEAkwMAIYgCAQCTAwAhA4wCAAAtACCNAgAALQAgjgIAAC0AIAAAAAAAAZkCAQAAAAEFmQICAAAAAZwCAgAAAAGdAgIAAAABngICAAAAAZ8CAgAAAAEBmQJAAAAAAQUeAADTBQAgHwAAgQYAIJMCAADUBQAglAIAAIAGACCXAgAAAQAgCx4AAIsEADAfAACQBAAwkwIAAIwEADCUAgAAjQQAMJUCAACPBAAwlgIAAI8EADCXAgAAjwQAMJgCAACOBAAgmQIAAI8EADCaAgAAkQQAMJsCAACSBAAwCx4AALgDADAfAAC9AwAwkwIAALkDADCUAgAAugMAMJUCAAC8AwAwlgIAALwDADCXAgAAvAMAMJgCAAC7AwAgmQIAALwDADCaAgAAvgMAMJsCAAC_AwAwBx4AAK4DACAfAACxAwAgkwIAAK8DACCUAgAAsAMAIJUCAAAcACCWAgAAHAAglwIAAPUBACAHBgAAtwMAIAwAALYDACDdAQEAAAAB4gFAAAAAAfMBAQAAAAH0AQEAAAAB9QEBAAAAAQIAAAD1AQAgHgAArgMAIAMAAAAcACAeAACuAwAgHwAAsgMAIAkAAAAcACAGAAC0AwAgDAAAswMAIBcAALIDACDdAQEApwMAIeIBQACpAwAh8wEBAKcDACH0AQEAtQMAIfUBAQC1AwAhBwYAALQDACAMAACzAwAg3QEBAKcDACHiAUAAqQMAIfMBAQCnAwAh9AEBALUDACH1AQEAtQMAIQceAAD4BQAgHwAA_gUAIJMCAAD5BQAglAIAAP0FACCVAgAAAwAglgIAAAMAIJcCAAAFACAHHgAA9gUAIB8AAPsFACCTAgAA9wUAIJQCAAD6BQAglQIAAAoAIJYCAAAKACCXAgAADAAgAZkCAQAAAAEDHgAA-AUAIJMCAAD5BQAglwIAAAUAIAMeAAD2BQAgkwIAAPcFACCXAgAADAAgCwMAAIYEACAKAACHBAAgCwAAiAQAIA0AAIkEACAOAACKBAAg3QEBAAAAAd4BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAGIAgEAAAABAgAAAAwAIB4AAIUEACADAAAADAAgHgAAhQQAIB8AAMIDACABFwAA9QUAMBADAACMAwAgBQAA-QIAIAoAAJ0DACALAACeAwAgDQAAjQMAIA4AAJ8DACDaAQAAnAMAMNsBAAAKABDcAQAAnAMAMN0BAQAAAAHeAQEA6AIAId8BAQDoAgAh4QECAO8CACHiAUAA9gIAIfYBAQCTAwAhiAIBAJMDACECAAAADAAgFwAAwgMAIAIAAADAAwAgFwAAwQMAIAraAQAAvwMAMNsBAADAAwAQ3AEAAL8DADDdAQEA6AIAId4BAQDoAgAh3wEBAOgCACHhAQIA7wIAIeIBQAD2AgAh9gEBAJMDACGIAgEAkwMAIQraAQAAvwMAMNsBAADAAwAQ3AEAAL8DADDdAQEA6AIAId4BAQDoAgAh3wEBAOgCACHhAQIA7wIAIeIBQAD2AgAh9gEBAJMDACGIAgEAkwMAIQbdAQEApwMAId4BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiAIBALUDACELAwAAwwMAIAoAAMQDACALAADFAwAgDQAAxgMAIA4AAMcDACDdAQEApwMAId4BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiAIBALUDACEFHgAA4gUAIB8AAPMFACCTAgAA4wUAIJQCAADyBQAglwIAAAEAIAseAADrAwAwHwAA8AMAMJMCAADsAwAwlAIAAO0DADCVAgAA7wMAMJYCAADvAwAwlwIAAO8DADCYAgAA7gMAIJkCAADvAwAwmgIAAPEDADCbAgAA8gMAMAseAADdAwAwHwAA4gMAMJMCAADeAwAwlAIAAN8DADCVAgAA4QMAMJYCAADhAwAwlwIAAOEDADCYAgAA4AMAIJkCAADhAwAwmgIAAOMDADCbAgAA5AMAMAceAADWAwAgHwAA2QMAIJMCAADXAwAglAIAANgDACCVAgAAHAAglgIAABwAIJcCAAD1AQAgCx4AAMgDADAfAADNAwAwkwIAAMkDADCUAgAAygMAMJUCAADMAwAwlgIAAMwDADCXAgAAzAMAMJgCAADLAwAgmQIAAMwDADCaAgAAzgMAMJsCAADPAwAwCgwAANUDACDdAQEAAAAB9AEBAAAAAfkBAQAAAAH8AQEAAAAB_QEBAAAAAf4BAgAAAAH_AQEAAAABgAIBAAAAAYECQAAAAAECAAAAIwAgHgAA1AMAIAMAAAAjACAeAADUAwAgHwAA0gMAIAEXAADxBQAwEQYAAPgCACAMAAD3AgAg2gEAAJIDADDbAQAAIQAQ3AEAAJIDADDdAQEAAAAB9AEBAJMDACH1AQEAkwMAIfkBAQCTAwAh_AEBAOgCACH9AQEA6AIAIf4BAgDvAgAh_wEBAOgCACGAAgEAkwMAIYECQAD2AgAhjwIAAJADACCQAgAAkQMAIAIAAAAjACAXAADSAwAgAgAAANADACAXAADRAwAgDdoBAADPAwAw2wEAANADABDcAQAAzwMAMN0BAQDoAgAh9AEBAJMDACH1AQEAkwMAIfkBAQCTAwAh_AEBAOgCACH9AQEA6AIAIf4BAgDvAgAh_wEBAOgCACGAAgEAkwMAIYECQAD2AgAhDdoBAADPAwAw2wEAANADABDcAQAAzwMAMN0BAQDoAgAh9AEBAJMDACH1AQEAkwMAIfkBAQCTAwAh_AEBAOgCACH9AQEA6AIAIf4BAgDvAgAh_wEBAOgCACGAAgEAkwMAIYECQAD2AgAhCd0BAQCnAwAh9AEBALUDACH5AQEAtQMAIfwBAQCnAwAh_QEBAKcDACH-AQIAqAMAIf8BAQCnAwAhgAIBALUDACGBAkAAqQMAIQoMAADTAwAg3QEBAKcDACH0AQEAtQMAIfkBAQC1AwAh_AEBAKcDACH9AQEApwMAIf4BAgCoAwAh_wEBAKcDACGAAgEAtQMAIYECQACpAwAhBx4AAOwFACAfAADvBQAgkwIAAO0FACCUAgAA7gUAIJUCAAADACCWAgAAAwAglwIAAAUAIAoMAADVAwAg3QEBAAAAAfQBAQAAAAH5AQEAAAAB_AEBAAAAAf0BAQAAAAH-AQIAAAAB_wEBAAAAAYACAQAAAAGBAkAAAAABAx4AAOwFACCTAgAA7QUAIJcCAAAFACAHBQAA3AMAIAwAALYDACDdAQEAAAAB4gFAAAAAAfMBAQAAAAH0AQEAAAAB9gEBAAAAAQIAAAD1AQAgHgAA1gMAIAMAAAAcACAeAADWAwAgHwAA2gMAIAkAAAAcACAFAADbAwAgDAAAswMAIBcAANoDACDdAQEApwMAIeIBQACpAwAh8wEBAKcDACH0AQEAtQMAIfYBAQC1AwAhBwUAANsDACAMAACzAwAg3QEBAKcDACHiAUAAqQMAIfMBAQCnAwAh9AEBALUDACH2AQEAtQMAIQceAADnBQAgHwAA6gUAIJMCAADoBQAglAIAAOkFACCVAgAABwAglgIAAAcAIJcCAAA3ACADHgAA5wUAIJMCAADoBQAglwIAADcAIAbdAQEAAAAB9wEIAAAAAfgBAgAAAAH5AQEAAAAB-gEgAAAAAfsBQAAAAAECAAAAGgAgHgAA6gMAIAMAAAAaACAeAADqAwAgHwAA6QMAIAEXAADmBQAwCwYAAJcDACDaAQAAlAMAMNsBAAAYABDcAQAAlAMAMN0BAQAAAAH1AQEA6AIAIfcBCACVAwAh-AECAO8CACH5AQEA6AIAIfoBIACWAwAh-wFAAPYCACECAAAAGgAgFwAA6QMAIAIAAADlAwAgFwAA5gMAIAraAQAA5AMAMNsBAADlAwAQ3AEAAOQDADDdAQEA6AIAIfUBAQDoAgAh9wEIAJUDACH4AQIA7wIAIfkBAQDoAgAh-gEgAJYDACH7AUAA9gIAIQraAQAA5AMAMNsBAADlAwAQ3AEAAOQDADDdAQEA6AIAIfUBAQDoAgAh9wEIAJUDACH4AQIA7wIAIfkBAQDoAgAh-gEgAJYDACH7AUAA9gIAIQbdAQEApwMAIfcBCADnAwAh-AECAKgDACH5AQEApwMAIfoBIADoAwAh-wFAAKkDACEFmQIIAAAAAZwCCAAAAAGdAggAAAABngIIAAAAAZ8CCAAAAAEBmQIgAAAAAQbdAQEApwMAIfcBCADnAwAh-AECAKgDACH5AQEApwMAIfoBIADoAwAh-wFAAKkDACEG3QEBAAAAAfcBCAAAAAH4AQIAAAAB-QEBAAAAAfoBIAAAAAH7AUAAAAABBwgAAIQEACDdAQEAAAAB4AEBAAAAAeEBAgAAAAGFAgEAAAABhgIBAAAAAYcCAQAAAAECAAAAEQAgHgAAgwQAIAMAAAARACAeAACDBAAgHwAA9QMAIAEXAADlBQAwDAYAAJcDACAIAACbAwAg2gEAAJoDADDbAQAADwAQ3AEAAJoDADDdAQEAAAAB4AEBAOgCACHhAQIA7wIAIfUBAQDoAgAhhQIBAOgCACGGAgEA6AIAIYcCAQDoAgAhAgAAABEAIBcAAPUDACACAAAA8wMAIBcAAPQDACAK2gEAAPIDADDbAQAA8wMAENwBAADyAwAw3QEBAOgCACHgAQEA6AIAIeEBAgDvAgAh9QEBAOgCACGFAgEA6AIAIYYCAQDoAgAhhwIBAOgCACEK2gEAAPIDADDbAQAA8wMAENwBAADyAwAw3QEBAOgCACHgAQEA6AIAIeEBAgDvAgAh9QEBAOgCACGFAgEA6AIAIYYCAQDoAgAhhwIBAOgCACEG3QEBAKcDACHgAQEApwMAIeEBAgCoAwAhhQIBAKcDACGGAgEApwMAIYcCAQCnAwAhBwgAAPYDACDdAQEApwMAIeABAQCnAwAh4QECAKgDACGFAgEApwMAIYYCAQCnAwAhhwIBAKcDACELHgAA9wMAMB8AAPwDADCTAgAA-AMAMJQCAAD5AwAwlQIAAPsDADCWAgAA-wMAMJcCAAD7AwAwmAIAAPoDACCZAgAA-wMAMJoCAAD9AwAwmwIAAP4DADAE3QEBAAAAAeEBAgAAAAGDAgEAAAABhAIgAAAAAQIAAAAVACAeAACCBAAgAwAAABUAIB4AAIIEACAfAACBBAAgARcAAOQFADAJBwAAmQMAINoBAACYAwAw2wEAABMAENwBAACYAwAw3QEBAAAAAeEBAgDvAgAhggIBAOgCACGDAgEA6AIAIYQCIACWAwAhAgAAABUAIBcAAIEEACACAAAA_wMAIBcAAIAEACAI2gEAAP4DADDbAQAA_wMAENwBAAD-AwAw3QEBAOgCACHhAQIA7wIAIYICAQDoAgAhgwIBAOgCACGEAiAAlgMAIQjaAQAA_gMAMNsBAAD_AwAQ3AEAAP4DADDdAQEA6AIAIeEBAgDvAgAhggIBAOgCACGDAgEA6AIAIYQCIACWAwAhBN0BAQCnAwAh4QECAKgDACGDAgEApwMAIYQCIADoAwAhBN0BAQCnAwAh4QECAKgDACGDAgEApwMAIYQCIADoAwAhBN0BAQAAAAHhAQIAAAABgwIBAAAAAYQCIAAAAAEHCAAAhAQAIN0BAQAAAAHgAQEAAAAB4QECAAAAAYUCAQAAAAGGAgEAAAABhwIBAAAAAQQeAAD3AwAwkwIAAPgDADCXAgAA-wMAMJgCAAD6AwAgCwMAAIYEACAKAACHBAAgCwAAiAQAIA0AAIkEACAOAACKBAAg3QEBAAAAAd4BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAGIAgEAAAABAx4AAOIFACCTAgAA4wUAIJcCAAABACAEHgAA6wMAMJMCAADsAwAwlwIAAO8DADCYAgAA7gMAIAQeAADdAwAwkwIAAN4DADCXAgAA4QMAMJgCAADgAwAgAx4AANYDACCTAgAA1wMAIJcCAAD1AQAgBB4AAMgDADCTAgAAyQMAMJcCAADMAwAwmAIAAMsDACAKAwAAtwQAIA0AALkEACAOAAC6BAAgEAAAuAQAIN0BAQAAAAHeAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAABiAIBAAAAAQIAAAAFACAeAAC2BAAgAwAAAAUAIB4AALYEACAfAACVBAAgARcAAOEFADAPAwAAjAMAIAUAAPkCACANAACNAwAgDgAAnwMAIBAAAKEDACDaAQAAoAMAMNsBAAADABDcAQAAoAMAMN0BAQAAAAHeAQEA6AIAId8BAQDoAgAh4QECAO8CACHiAUAA9gIAIfYBAQCTAwAhiAIBAJMDACECAAAABQAgFwAAlQQAIAIAAACTBAAgFwAAlAQAIAraAQAAkgQAMNsBAACTBAAQ3AEAAJIEADDdAQEA6AIAId4BAQDoAgAh3wEBAOgCACHhAQIA7wIAIeIBQAD2AgAh9gEBAJMDACGIAgEAkwMAIQraAQAAkgQAMNsBAACTBAAQ3AEAAJIEADDdAQEA6AIAId4BAQDoAgAh3wEBAOgCACHhAQIA7wIAIeIBQAD2AgAh9gEBAJMDACGIAgEAkwMAIQbdAQEApwMAId4BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiAIBALUDACEKAwAAlgQAIA0AAJgEACAOAACZBAAgEAAAlwQAIN0BAQCnAwAh3gEBAKcDACHfAQEApwMAIeEBAgCoAwAh4gFAAKkDACGIAgEAtQMAIQUeAADVBQAgHwAA3wUAIJMCAADWBQAglAIAAN4FACCXAgAAAQAgCx4AAKoEADAfAACvBAAwkwIAAKsEADCUAgAArAQAMJUCAACuBAAwlgIAAK4EADCXAgAArgQAMJgCAACtBAAgmQIAAK4EADCaAgAAsAQAMJsCAACxBAAwBx4AAKUEACAfAACoBAAgkwIAAKYEACCUAgAApwQAIJUCAAAcACCWAgAAHAAglwIAAPUBACALHgAAmgQAMB8AAJ4EADCTAgAAmwQAMJQCAACcBAAwlQIAAMwDADCWAgAAzAMAMJcCAADMAwAwmAIAAJ0EACCZAgAAzAMAMJoCAACfBAAwmwIAAM8DADAKBgAApAQAIN0BAQAAAAH1AQEAAAAB-QEBAAAAAfwBAQAAAAH9AQEAAAAB_gECAAAAAf8BAQAAAAGAAgEAAAABgQJAAAAAAQIAAAAjACAeAACjBAAgAwAAACMAIB4AAKMEACAfAAChBAAgARcAAN0FADACAAAAIwAgFwAAoQQAIAIAAADQAwAgFwAAoAQAIAndAQEApwMAIfUBAQC1AwAh-QEBALUDACH8AQEApwMAIf0BAQCnAwAh_gECAKgDACH_AQEApwMAIYACAQC1AwAhgQJAAKkDACEKBgAAogQAIN0BAQCnAwAh9QEBALUDACH5AQEAtQMAIfwBAQCnAwAh_QEBAKcDACH-AQIAqAMAIf8BAQCnAwAhgAIBALUDACGBAkAAqQMAIQceAADYBQAgHwAA2wUAIJMCAADZBQAglAIAANoFACCVAgAACgAglgIAAAoAIJcCAAAMACAKBgAApAQAIN0BAQAAAAH1AQEAAAAB-QEBAAAAAfwBAQAAAAH9AQEAAAAB_gECAAAAAf8BAQAAAAGAAgEAAAABgQJAAAAAAQMeAADYBQAgkwIAANkFACCXAgAADAAgBwUAANwDACAGAAC3AwAg3QEBAAAAAeIBQAAAAAHzAQEAAAAB9QEBAAAAAfYBAQAAAAECAAAA9QEAIB4AAKUEACADAAAAHAAgHgAApQQAIB8AAKkEACAJAAAAHAAgBQAA2wMAIAYAALQDACAXAACpBAAg3QEBAKcDACHiAUAAqQMAIfMBAQCnAwAh9QEBALUDACH2AQEAtQMAIQcFAADbAwAgBgAAtAMAIN0BAQCnAwAh4gFAAKkDACHzAQEApwMAIfUBAQC1AwAh9gEBALUDACEE3QEBAAAAAeEBAgAAAAGJAgEAAAABigIBAAAAAQIAAAAvACAeAAC1BAAgAwAAAC8AIB4AALUEACAfAAC0BAAgARcAANcFADAJDAAAjwMAINoBAACOAwAw2wEAAC0AENwBAACOAwAw3QEBAAAAAeEBAgDvAgAh9AEBAOgCACGJAgEA6AIAIYoCAQDoAgAhAgAAAC8AIBcAALQEACACAAAAsgQAIBcAALMEACAI2gEAALEEADDbAQAAsgQAENwBAACxBAAw3QEBAOgCACHhAQIA7wIAIfQBAQDoAgAhiQIBAOgCACGKAgEA6AIAIQjaAQAAsQQAMNsBAACyBAAQ3AEAALEEADDdAQEA6AIAIeEBAgDvAgAh9AEBAOgCACGJAgEA6AIAIYoCAQDoAgAhBN0BAQCnAwAh4QECAKgDACGJAgEApwMAIYoCAQCnAwAhBN0BAQCnAwAh4QECAKgDACGJAgEApwMAIYoCAQCnAwAhBN0BAQAAAAHhAQIAAAABiQIBAAAAAYoCAQAAAAEKAwAAtwQAIA0AALkEACAOAAC6BAAgEAAAuAQAIN0BAQAAAAHeAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAABiAIBAAAAAQMeAADVBQAgkwIAANYFACCXAgAAAQAgBB4AAKoEADCTAgAAqwQAMJcCAACuBAAwmAIAAK0EACADHgAApQQAIJMCAACmBAAglwIAAPUBACAEHgAAmgQAMJMCAACbBAAwlwIAAMwDADCYAgAAnQQAIAMeAADTBQAgkwIAANQFACCXAgAAAQAgBB4AAIsEADCTAgAAjAQAMJcCAACPBAAwmAIAAI4EACAEHgAAuAMAMJMCAAC5AwAwlwIAALwDADCYAgAAuwMAIAMeAACuAwAgkwIAAK8DACCXAgAA9QEAIAAAAAAAAAAAAAGZAkAAAAABAAAABwMAAKoFACAFAADOBAAgDQAAqwUAIA4AALAFACAQAACxBQAg9gEAAMIEACCIAgAAwgQAIAgDAACqBQAgBQAAzgQAIAoAAK4FACALAACvBQAgDQAAqwUAIA4AALAFACD2AQAAwgQAIIgCAADCBAAgBAMAAKoFACAEAACnBQAgDQAAqwUAIA8AAKgFACAAAAAAAAUeAADOBQAgHwAA0QUAIJMCAADPBQAglAIAANAFACCXAgAADAAgAx4AAM4FACCTAgAAzwUAIJcCAAAMACAAAAAAAAAAAAAABR4AAMkFACAfAADMBQAgkwIAAMoFACCUAgAAywUAIJcCAAARACADHgAAyQUAIJMCAADKBQAglwIAABEAIAAAAAAABR4AAMQFACAfAADHBQAgkwIAAMUFACCUAgAAxgUAIJcCAAAMACADHgAAxAUAIJMCAADFBQAglwIAAAwAIAAAAAAABx4AAL8FACAfAADCBQAgkwIAAMAFACCUAgAAwQUAIJUCAAAHACCWAgAABwAglwIAADcAIAMeAAC_BQAgkwIAAMAFACCXAgAANwAgAAAAAAAFHgAAugUAIB8AAL0FACCTAgAAuwUAIJQCAAC8BQAglwIAAAUAIAMeAAC6BQAgkwIAALsFACCXAgAABQAgAAAAAAAHHgAAtQUAIB8AALgFACCTAgAAtgUAIJQCAAC3BQAglQIAAAcAIJYCAAAHACCXAgAANwAgAx4AALUFACCTAgAAtgUAIJcCAAA3ACAAAAAAAAseAACbBQAwHwAAnwUAMJMCAACcBQAwlAIAAJ0FADCVAgAAjwQAMJYCAACPBAAwlwIAAI8EADCYAgAAngUAIJkCAACPBAAwmgIAAKAFADCbAgAAkgQAMAseAACSBQAwHwAAlgUAMJMCAACTBQAwlAIAAJQFADCVAgAAvAMAMJYCAAC8AwAwlwIAALwDADCYAgAAlQUAIJkCAAC8AwAwmgIAAJcFADCbAgAAvwMAMAseAACGBQAwHwAAiwUAMJMCAACHBQAwlAIAAIgFADCVAgAAigUAMJYCAACKBQAwlwIAAIoFADCYAgAAiQUAIJkCAACKBQAwmgIAAIwFADCbAgAAjQUAMAgEAAC8BAAgDQAAvgQAIA8AAL0EACDdAQEAAAAB3wEBAAAAAeABAQAAAAHhAQIAAAAB4gFAAAAAAQIAAAA3ACAeAACRBQAgAwAAADcAIB4AAJEFACAfAACQBQAgARcAALQFADANAwAAjAMAIAQAAIgDACANAACNAwAgDwAAiQMAINoBAACLAwAw2wEAAAcAENwBAACLAwAw3QEBAAAAAd4BAQDoAgAh3wEBAOgCACHgAQEA6AIAIeEBAgDvAgAh4gFAAPYCACECAAAANwAgFwAAkAUAIAIAAACOBQAgFwAAjwUAIAnaAQAAjQUAMNsBAACOBQAQ3AEAAI0FADDdAQEA6AIAId4BAQDoAgAh3wEBAOgCACHgAQEA6AIAIeEBAgDvAgAh4gFAAPYCACEJ2gEAAI0FADDbAQAAjgUAENwBAACNBQAw3QEBAOgCACHeAQEA6AIAId8BAQDoAgAh4AEBAOgCACHhAQIA7wIAIeIBQAD2AgAhBd0BAQCnAwAh3wEBAKcDACHgAQEApwMAIeEBAgCoAwAh4gFAAKkDACEIBAAAqwMAIA0AAK0DACAPAACsAwAg3QEBAKcDACHfAQEApwMAIeABAQCnAwAh4QECAKgDACHiAUAAqQMAIQgEAAC8BAAgDQAAvgQAIA8AAL0EACDdAQEAAAAB3wEBAAAAAeABAQAAAAHhAQIAAAAB4gFAAAAAAQsFAADvBAAgCgAAhwQAIAsAAIgEACANAACJBAAgDgAAigQAIN0BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAH2AQEAAAABiAIBAAAAAQIAAAAMACAeAACaBQAgAwAAAAwAIB4AAJoFACAfAACZBQAgARcAALMFADACAAAADAAgFwAAmQUAIAIAAADAAwAgFwAAmAUAIAbdAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIfYBAQC1AwAhiAIBALUDACELBQAA7gQAIAoAAMQDACALAADFAwAgDQAAxgMAIA4AAMcDACDdAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIfYBAQC1AwAhiAIBALUDACELBQAA7wQAIAoAAIcEACALAACIBAAgDQAAiQQAIA4AAIoEACDdAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAAB9gEBAAAAAYgCAQAAAAEKBQAA_QQAIA0AALkEACAOAAC6BAAgEAAAuAQAIN0BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAH2AQEAAAABiAIBAAAAAQIAAAAFACAeAACjBQAgAwAAAAUAIB4AAKMFACAfAACiBQAgARcAALIFADACAAAABQAgFwAAogUAIAIAAACTBAAgFwAAoQUAIAbdAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIfYBAQC1AwAhiAIBALUDACEKBQAA_AQAIA0AAJgEACAOAACZBAAgEAAAlwQAIN0BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAh9gEBALUDACGIAgEAtQMAIQoFAAD9BAAgDQAAuQQAIA4AALoEACAQAAC4BAAg3QEBAAAAAd8BAQAAAAHhAQIAAAAB4gFAAAAAAfYBAQAAAAGIAgEAAAABBB4AAJsFADCTAgAAnAUAMJcCAACPBAAwmAIAAJ4FACAEHgAAkgUAMJMCAACTBQAwlwIAALwDADCYAgAAlQUAIAQeAACGBQAwkwIAAIcFADCXAgAAigUAMJgCAACJBQAgAAAAAwQAAKcFACAPAACoBQAgEQAAqQUAIAYFAADOBAAgBgAAzQQAIAwAAMwEACD0AQAAwgQAIPUBAADCBAAg9gEAAMIEACACBgAAzQQAIAgAAK0FACAAAAAAAAbdAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAAB9gEBAAAAAYgCAQAAAAEG3QEBAAAAAd8BAQAAAAHhAQIAAAAB4gFAAAAAAfYBAQAAAAGIAgEAAAABBd0BAQAAAAHfAQEAAAAB4AEBAAAAAeEBAgAAAAHiAUAAAAABCQMAALsEACANAAC-BAAgDwAAvQQAIN0BAQAAAAHeAQEAAAAB3wEBAAAAAeABAQAAAAHhAQIAAAAB4gFAAAAAAQIAAAA3ACAeAAC1BQAgAwAAAAcAIB4AALUFACAfAAC5BQAgCwAAAAcAIAMAAKoDACANAACtAwAgDwAArAMAIBcAALkFACDdAQEApwMAId4BAQCnAwAh3wEBAKcDACHgAQEApwMAIeEBAgCoAwAh4gFAAKkDACEJAwAAqgMAIA0AAK0DACAPAACsAwAg3QEBAKcDACHeAQEApwMAId8BAQCnAwAh4AEBAKcDACHhAQIAqAMAIeIBQACpAwAhCwMAALcEACAFAAD9BAAgDQAAuQQAIA4AALoEACDdAQEAAAAB3gEBAAAAAd8BAQAAAAHhAQIAAAAB4gFAAAAAAfYBAQAAAAGIAgEAAAABAgAAAAUAIB4AALoFACADAAAAAwAgHgAAugUAIB8AAL4FACANAAAAAwAgAwAAlgQAIAUAAPwEACANAACYBAAgDgAAmQQAIBcAAL4FACDdAQEApwMAId4BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAh9gEBALUDACGIAgEAtQMAIQsDAACWBAAgBQAA_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_AEBAAAAAf0BAQAAAAH-AQIAAAAB_wEBAAAAAYACAQAAAAGBAkAAAAABAwAAAD0AIB4AANUFACAfAADgBQAgCQAAAD0AIA8AAIQFACARAACFBQAgFwAA4AUAIN0BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiwIBAKcDACEHDwAAhAUAIBEAAIUFACDdAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIYsCAQCnAwAhBt0BAQAAAAHeAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAABiAIBAAAAAQcEAACkBQAgEQAApgUAIN0BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAGLAgEAAAABAgAAAAEAIB4AAOIFACAE3QEBAAAAAeEBAgAAAAGDAgEAAAABhAIgAAAAAQbdAQEAAAAB4AEBAAAAAeEBAgAAAAGFAgEAAAABhgIBAAAAAYcCAQAAAAEG3QEBAAAAAfcBCAAAAAH4AQIAAAAB-QEBAAAAAfoBIAAAAAH7AUAAAAABCQMAALsEACAEAAC8BAAgDwAAvQQAIN0BAQAAAAHeAQEAAAAB3wEBAAAAAeABAQAAAAHhAQIAAAAB4gFAAAAAAQIAAAA3ACAeAADnBQAgAwAAAAcAIB4AAOcFACAfAADrBQAgCwAAAAcAIAMAAKoDACAEAACrAwAgDwAArAMAIBcAAOsFACDdAQEApwMAId4BAQCnAwAh3wEBAKcDACHgAQEApwMAIeEBAgCoAwAh4gFAAKkDACEJAwAAqgMAIAQAAKsDACAPAACsAwAg3QEBAKcDACHeAQEApwMAId8BAQCnAwAh4AEBAKcDACHhAQIAqAMAIeIBQACpAwAhCwMAALcEACAFAAD9BAAgDQAAuQQAIBAAALgEACDdAQEAAAAB3gEBAAAAAd8BAQAAAAHhAQIAAAAB4gFAAAAAAfYBAQAAAAGIAgEAAAABAgAAAAUAIB4AAOwFACADAAAAAwAgHgAA7AUAIB8AAPAFACANAAAAAwAgAwAAlgQAIAUAAPwEACANAACYBAAgEAAAlwQAIBcAAPAFACDdAQEApwMAId4BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAh9gEBALUDACGIAgEAtQMAIQsDAACWBAAgBQAA_AQAIA0AAJgEACAQAACXBAAg3QEBAKcDACHeAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIfYBAQC1AwAhiAIBALUDACEJ3QEBAAAAAfQBAQAAAAH5AQEAAAAB_AEBAAAAAf0BAQAAAAH-AQIAAAAB_wEBAAAAAYACAQAAAAGBAkAAAAABAwAAAD0AIB4AAOIFACAfAAD0BQAgCQAAAD0AIAQAAIMFACARAACFBQAgFwAA9AUAIN0BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiwIBAKcDACEHBAAAgwUAIBEAAIUFACDdAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIYsCAQCnAwAhBt0BAQAAAAHeAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAABiAIBAAAAAQwDAACGBAAgBQAA7wQAIAoAAIcEACALAACIBAAgDgAAigQAIN0BAQAAAAHeAQEAAAAB3wEBAAAAAeEBAgAAAAHiAUAAAAAB9gEBAAAAAYgCAQAAAAECAAAADAAgHgAA9gUAIAsDAAC3BAAgBQAA_QQAIA4AALoEACAQAAC4BAAg3QEBAAAAAd4BAQAAAAHfAQEAAAAB4QECAAAAAeIBQAAAAAH2AQEAAAABiAIBAAAAAQIAAAAFACAeAAD4BQAgAwAAAAoAIB4AAPYFACAfAAD8BQAgDgAAAAoAIAMAAMMDACAFAADuBAAgCgAAxAMAIAsAAMUDACAOAADHAwAgFwAA_AUAIN0BAQCnAwAh3gEBAKcDACHfAQEApwMAIeEBAgCoAwAh4gFAAKkDACH2AQEAtQMAIYgCAQC1AwAhDAMAAMMDACAFAADuBAAgCgAAxAMAIAsAAMUDACAOAADHAwAg3QEBAKcDACHeAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIfYBAQC1AwAhiAIBALUDACEDAAAAAwAgHgAA-AUAIB8AAP8FACANAAAAAwAgAwAAlgQAIAUAAPwEACAOAACZBAAgEAAAlwQAIBcAAP8FACDdAQEApwMAId4BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAh9gEBALUDACGIAgEAtQMAIQsDAACWBAAgBQAA_AQAIA4AAJkEACAQAACXBAAg3QEBAKcDACHeAQEApwMAId8BAQCnAwAh4QECAKgDACHiAUAAqQMAIfYBAQC1AwAhiAIBALUDACEDAAAAPQAgHgAA0wUAIB8AAIIGACAJAAAAPQAgBAAAgwUAIA8AAIQFACAXAACCBgAg3QEBAKcDACHfAQEApwMAIeEBAgCoAwAh4gFAAKkDACGLAgEApwMAIQcEAACDBQAgDwAAhAUAIN0BAQCnAwAh3wEBAKcDACHhAQIAqAMAIeIBQACpAwAhiwIBAKcDACEEBAYCCQAPDzUEETgDBgMAAQUIAwkADg0xCQ4yChAwDQUDAAEECQIJAAwNKgkPDQQHAwABBQ4DCQALChIFCxsIDR0JDiQKAwYABAgWBgkABwEHAAUBCBcAAQYABAMFIAMGHwQMHgICBiYEDCUCAwonAAsoAA4pAAIEKwAPLAABDAACAg40ABAzAAMEOQAPOgAROwAAAAAFCQAUJAAVJQAWJgAXJwAYAAAAAAAFCQAUJAAVJQAWJgAXJwAYAgMAAQVdAwIDAAEFYwMFCQAdJAAeJQAfJgAgJwAhAAAAAAAFCQAdJAAeJQAfJgAgJwAhAQwAAgEMAAIFCQAmJAAnJQAoJgApJwAqAAAAAAAFCQAmJAAnJQAoJgApJwAqAgMAAQWLAQMCAwABBZEBAwUJAC8kADAlADEmADInADMAAAAAAAUJAC8kADAlADEmADInADMBBgAEAQYABAUJADgkADklADomADsnADwAAAAAAAUJADgkADklADomADsnADwBBwAFAQcABQUJAEEkAEIlAEMmAEQnAEUAAAAAAAUJAEEkAEIlAEMmAEQnAEUCBtABBAzPAQICBtcBBAzWAQIFCQBKJABLJQBMJgBNJwBOAAAAAAAFCQBKJABLJQBMJgBNJwBOAQYABAEGAAQFCQBTJABUJQBVJgBWJwBXAAAAAAAFCQBTJABUJQBVJgBWJwBXAwWDAgMGggIEDIECAgMFiwIDBooCBAyJAgIDCQBcJgBdJwBeAAAAAwkAXCYAXScAXgAAAAUJAGQkAGUlAGYmAGcnAGgAAAAAAAUJAGQkAGUlAGYmAGcnAGgAAAADCQBuJgBvJwBwAAAAAwkAbiYAbycAcAEDAAEBAwABBQkAdSQAdiUAdyYAeCcAeQAAAAAABQkAdSQAdiUAdyYAeCcAeRICARM8ARQ_ARVAARZBARhDARlFEBpGERtIARxKEB1LEiBMASFNASJOEChREylSGSpTAitUAixVAi1WAi5XAi9ZAjBbEDFcGjJfAjNhEDRiGzVkAjZlAjdmEDhpHDlqIjprDTtsDTxtDT1uDT5vDT9xDUBzEEF0I0J2DUN4EER5JEV6DUZ7DUd8EEh_JUmAAStKgQEES4IBBEyDAQRNhAEEToUBBE-HAQRQiQEQUYoBLFKNAQRTjwEQVJABLVWSAQRWkwEEV5QBEFiXAS5ZmAE0WpkBBVuaAQVcmwEFXZwBBV6dAQVfnwEFYKEBEGGiATVipAEFY6YBEGSnATZlqAEFZqkBBWeqARBorQE3aa4BPWqvAQZrsAEGbLEBBm2yAQZuswEGb7UBBnC3ARBxuAE-croBBnO8ARB0vQE_db4BBna_AQZ3wAEQeMMBQHnEAUZ6xQEKe8YBCnzHAQp9yAEKfskBCn_LAQqAAc0BEIEBzgFHggHSAQqDAdQBEIQB1QFIhQHYAQqGAdkBCocB2gEQiAHdAUmJAd4BT4oB3wEIiwHgAQiMAeEBCI0B4gEIjgHjAQiPAeUBCJAB5wEQkQHoAVCSAeoBCJMB7AEQlAHtAVGVAe4BCJYB7wEIlwHwARCYAfMBUpkB9AFYmgH2AQmbAfcBCZwB-QEJnQH6AQmeAfsBCZ8B_QEJoAH_ARChAYACWaIBhQIJowGHAhCkAYgCWqUBjAIJpgGNAgmnAY4CEKgBkQJbqQGSAl-qAZQCYKsBlQJgrAGYAmCtAZkCYK4BmgJgrwGcAmCwAZ4CELEBnwJhsgGhAmCzAaMCELQBpAJitQGlAmC2AaYCYLcBpwIQuAGqAmO5AasCaboBrQJquwGuAmq8AbECar0BsgJqvgGzAmq_AbUCasABtwIQwQG4AmvCAboCasMBvAIQxAG9AmzFAb4CasYBvwJqxwHAAhDIAcMCbckBxAJxygHFAgPLAcYCA8wBxwIDzQHIAgPOAckCA88BywID0AHNAhDRAc4CctIB0AID0wHSAhDUAdMCc9UB1AID1gHVAgPXAdYCENgB2QJ02QHaAno" + strings: JSON.parse("[\"where\",\"orderBy\",\"cursor\",\"class\",\"decks\",\"group\",\"quizSet\",\"question\",\"options\",\"_count\",\"questions\",\"attempts\",\"deck\",\"shareLink\",\"progress\",\"quizSets\",\"cards\",\"materialGroups\",\"Class.findUnique\",\"Class.findUniqueOrThrow\",\"Class.findFirst\",\"Class.findFirstOrThrow\",\"Class.findMany\",\"data\",\"Class.createOne\",\"Class.createMany\",\"Class.createManyAndReturn\",\"Class.updateOne\",\"Class.updateMany\",\"Class.updateManyAndReturn\",\"create\",\"update\",\"Class.upsertOne\",\"Class.deleteOne\",\"Class.deleteMany\",\"having\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"Class.groupBy\",\"Class.aggregate\",\"Deck.findUnique\",\"Deck.findUniqueOrThrow\",\"Deck.findFirst\",\"Deck.findFirstOrThrow\",\"Deck.findMany\",\"Deck.createOne\",\"Deck.createMany\",\"Deck.createManyAndReturn\",\"Deck.updateOne\",\"Deck.updateMany\",\"Deck.updateManyAndReturn\",\"Deck.upsertOne\",\"Deck.deleteOne\",\"Deck.deleteMany\",\"Deck.groupBy\",\"Deck.aggregate\",\"Flashcard.findUnique\",\"Flashcard.findUniqueOrThrow\",\"Flashcard.findFirst\",\"Flashcard.findFirstOrThrow\",\"Flashcard.findMany\",\"Flashcard.createOne\",\"Flashcard.createMany\",\"Flashcard.createManyAndReturn\",\"Flashcard.updateOne\",\"Flashcard.updateMany\",\"Flashcard.updateManyAndReturn\",\"Flashcard.upsertOne\",\"Flashcard.deleteOne\",\"Flashcard.deleteMany\",\"Flashcard.groupBy\",\"Flashcard.aggregate\",\"QuizSet.findUnique\",\"QuizSet.findUniqueOrThrow\",\"QuizSet.findFirst\",\"QuizSet.findFirstOrThrow\",\"QuizSet.findMany\",\"QuizSet.createOne\",\"QuizSet.createMany\",\"QuizSet.createManyAndReturn\",\"QuizSet.updateOne\",\"QuizSet.updateMany\",\"QuizSet.updateManyAndReturn\",\"QuizSet.upsertOne\",\"QuizSet.deleteOne\",\"QuizSet.deleteMany\",\"QuizSet.groupBy\",\"QuizSet.aggregate\",\"Question.findUnique\",\"Question.findUniqueOrThrow\",\"Question.findFirst\",\"Question.findFirstOrThrow\",\"Question.findMany\",\"Question.createOne\",\"Question.createMany\",\"Question.createManyAndReturn\",\"Question.updateOne\",\"Question.updateMany\",\"Question.updateManyAndReturn\",\"Question.upsertOne\",\"Question.deleteOne\",\"Question.deleteMany\",\"Question.groupBy\",\"Question.aggregate\",\"AnswerOption.findUnique\",\"AnswerOption.findUniqueOrThrow\",\"AnswerOption.findFirst\",\"AnswerOption.findFirstOrThrow\",\"AnswerOption.findMany\",\"AnswerOption.createOne\",\"AnswerOption.createMany\",\"AnswerOption.createManyAndReturn\",\"AnswerOption.updateOne\",\"AnswerOption.updateMany\",\"AnswerOption.updateManyAndReturn\",\"AnswerOption.upsertOne\",\"AnswerOption.deleteOne\",\"AnswerOption.deleteMany\",\"AnswerOption.groupBy\",\"AnswerOption.aggregate\",\"StudyProgress.findUnique\",\"StudyProgress.findUniqueOrThrow\",\"StudyProgress.findFirst\",\"StudyProgress.findFirstOrThrow\",\"StudyProgress.findMany\",\"StudyProgress.createOne\",\"StudyProgress.createMany\",\"StudyProgress.createManyAndReturn\",\"StudyProgress.updateOne\",\"StudyProgress.updateMany\",\"StudyProgress.updateManyAndReturn\",\"StudyProgress.upsertOne\",\"StudyProgress.deleteOne\",\"StudyProgress.deleteMany\",\"StudyProgress.groupBy\",\"StudyProgress.aggregate\",\"QuizAttempt.findUnique\",\"QuizAttempt.findUniqueOrThrow\",\"QuizAttempt.findFirst\",\"QuizAttempt.findFirstOrThrow\",\"QuizAttempt.findMany\",\"QuizAttempt.createOne\",\"QuizAttempt.createMany\",\"QuizAttempt.createManyAndReturn\",\"QuizAttempt.updateOne\",\"QuizAttempt.updateMany\",\"QuizAttempt.updateManyAndReturn\",\"QuizAttempt.upsertOne\",\"QuizAttempt.deleteOne\",\"QuizAttempt.deleteMany\",\"QuizAttempt.groupBy\",\"QuizAttempt.aggregate\",\"ShareLink.findUnique\",\"ShareLink.findUniqueOrThrow\",\"ShareLink.findFirst\",\"ShareLink.findFirstOrThrow\",\"ShareLink.findMany\",\"ShareLink.createOne\",\"ShareLink.createMany\",\"ShareLink.createManyAndReturn\",\"ShareLink.updateOne\",\"ShareLink.updateMany\",\"ShareLink.updateManyAndReturn\",\"ShareLink.upsertOne\",\"ShareLink.deleteOne\",\"ShareLink.deleteMany\",\"ShareLink.groupBy\",\"ShareLink.aggregate\",\"AuthSecurity.findUnique\",\"AuthSecurity.findUniqueOrThrow\",\"AuthSecurity.findFirst\",\"AuthSecurity.findFirstOrThrow\",\"AuthSecurity.findMany\",\"AuthSecurity.createOne\",\"AuthSecurity.createMany\",\"AuthSecurity.createManyAndReturn\",\"AuthSecurity.updateOne\",\"AuthSecurity.updateMany\",\"AuthSecurity.updateManyAndReturn\",\"AuthSecurity.upsertOne\",\"AuthSecurity.deleteOne\",\"AuthSecurity.deleteMany\",\"AuthSecurity.groupBy\",\"AuthSecurity.aggregate\",\"Setting.findUnique\",\"Setting.findUniqueOrThrow\",\"Setting.findFirst\",\"Setting.findFirstOrThrow\",\"Setting.findMany\",\"Setting.createOne\",\"Setting.createMany\",\"Setting.createManyAndReturn\",\"Setting.updateOne\",\"Setting.updateMany\",\"Setting.updateManyAndReturn\",\"Setting.upsertOne\",\"Setting.deleteOne\",\"Setting.deleteMany\",\"Setting.groupBy\",\"Setting.aggregate\",\"StudyActivity.findUnique\",\"StudyActivity.findUniqueOrThrow\",\"StudyActivity.findFirst\",\"StudyActivity.findFirstOrThrow\",\"StudyActivity.findMany\",\"StudyActivity.createOne\",\"StudyActivity.createMany\",\"StudyActivity.createManyAndReturn\",\"StudyActivity.updateOne\",\"StudyActivity.updateMany\",\"StudyActivity.updateManyAndReturn\",\"StudyActivity.upsertOne\",\"StudyActivity.deleteOne\",\"StudyActivity.deleteMany\",\"StudyActivity.groupBy\",\"StudyActivity.aggregate\",\"MaterialGroup.findUnique\",\"MaterialGroup.findUniqueOrThrow\",\"MaterialGroup.findFirst\",\"MaterialGroup.findFirstOrThrow\",\"MaterialGroup.findMany\",\"MaterialGroup.createOne\",\"MaterialGroup.createMany\",\"MaterialGroup.createManyAndReturn\",\"MaterialGroup.updateOne\",\"MaterialGroup.updateMany\",\"MaterialGroup.updateManyAndReturn\",\"MaterialGroup.upsertOne\",\"MaterialGroup.deleteOne\",\"MaterialGroup.deleteMany\",\"MaterialGroup.groupBy\",\"MaterialGroup.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"classId\",\"name\",\"type\",\"sortOrder\",\"createdAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"occurredAt\",\"key\",\"value\",\"failedAttempts\",\"lockedUntil\",\"lastAttemptAt\",\"targetType\",\"deckId\",\"quizSetId\",\"groupId\",\"score\",\"maxScore\",\"answersJson\",\"isPartialRetake\",\"completedAt\",\"contentType\",\"mode\",\"currentIndex\",\"orderJson\",\"cardResultsJson\",\"updatedAt\",\"questionId\",\"text\",\"isCorrect\",\"prompt\",\"rationale\",\"category\",\"description\",\"front\",\"back\",\"slug\",\"every\",\"some\",\"none\",\"deckId_mode\",\"quizSetId_mode\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"disconnect\",\"delete\",\"connect\",\"createMany\",\"set\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), + graph: "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-QQAICQAAPoEACAlAAD9BAAgJgAA_AQAICcAAPsEACAI6gEAAJwDADDrAQAAwgEAEOwBAACcAwAw7QEBAPUCACHxAQIA9gIAIZMCAQD1AgAhlAIBAPUCACGVAiAAlwMAIQMAAAATACABAADBAQAwIwAAwgEAIAMAAAATACABAAAUADACAAAVACABAAAAIwAgAQAAACMAIAMAAAAhACABAAAiADACAAAjACADAAAAIQAgAQAAIgAwAgAAIwAgAwAAACEAIAEAACIAMAIAACMAIAwGAAC_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_AQEAAAABgAIBAIEDACEBAAAArAIAIAEAAACsAgAgBeoBAACEAwAw6wEAAK8CABDsAQAAhAMAMP8BAQCBAwAhgAIBAIEDACEAAwAAAK8CACABAACwAgAwAgAArAIAIAMAAACvAgAgAQAAsAIAMAIAAKwCACADAAAArwIAIAEAALACADACAACsAgAgAv8BAQAAAAGAAgEAAAABARcAALQCACAC_wEBAAAAAYACAQAAAAEBFwAAtgIAMAEXAAC2AgAwAv8BAQDCAwAhgAIBAMIDACECAAAArAIAIBcAALkCACAC_wEBAMIDACGAAgEAwgMAIQIAAACvAgAgFwAAuwIAIAIAAACvAgAgFwAAuwIAIAMAAACsAgAgHgAAtAIAIB8AALkCACABAAAArAIAIAEAAACvAgAgAwkAAN0EACAmAADfBAAgJwAA3gQAIAXqAQAAgwMAMOsBAADCAgAQ7AEAAIMDADD_AQEA9QIAIYACAQD1AgAhAwAAAK8CACABAADBAgAwIwAAwgIAIAMAAACvAgAgAQAAsAIAMAIAAKwCACAG6gEAAIADADDrAQAAyAIAEOwBAACAAwAw7QEBAAAAAfABAQCBAwAh_gFAAIIDACEBAAAAxQIAIAEAAADFAgAgBuoBAACAAwAw6wEAAMgCABDsAQAAgAMAMO0BAQCBAwAh8AEBAIEDACH-AUAAggMAIQADAAAAyAIAIAEAAMkCADACAADFAgAgAwAAAMgCACABAADJAgAwAgAAxQIAIAMAAADIAgAgAQAAyQIAMAIAAMUCACAD7QEBAAAAAfABAQAAAAH-AUAAAAABARcAAM0CACAD7QEBAAAAAfABAQAAAAH-AUAAAAABARcAAM8CADABFwAAzwIAMAPtAQEAwgMAIfABAQDCAwAh_gFAAMQDACECAAAAxQIAIBcAANICACAD7QEBAMIDACHwAQEAwgMAIf4BQADEAwAhAgAAAMgCACAXAADUAgAgAgAAAMgCACAXAADUAgAgAwAAAMUCACAeAADNAgAgHwAA0gIAIAEAAADFAgAgAQAAAMgCACADCQAA2gQAICYAANwEACAnAADbBAAgBuoBAAD_AgAw6wEAANsCABDsAQAA_wIAMO0BAQD1AgAh8AEBAPUCACH-AUAA9wIAIQMAAADIAgAgAQAA2gIAMCMAANsCACADAAAAyAIAIAEAAMkCADACAADFAgAgAQAAADcAIAEAAAA3ACADAAAABwAgAQAANgAwAgAANwAgAwAAAAcAIAEAADYAMAIAADcAIAMAAAAHACABAAA2ADACAAA3ACAKAwAA1gQAIAQAANcEACANAADZBAAgDwAA2AQAIO0BAQAAAAHuAQEAAAAB7wEBAAAAAfABAQAAAAHxAQIAAAAB8gFAAAAAAQEXAADjAgAgBu0BAQAAAAHuAQEAAAAB7wEBAAAAAfABAQAAAAHxAQIAAAAB8gFAAAAAAQEXAADlAgAwARcAAOUCADAKAwAAxQMAIAQAAMYDACANAADIAwAgDwAAxwMAIO0BAQDCAwAh7gEBAMIDACHvAQEAwgMAIfABAQDCAwAh8QECAMMDACHyAUAAxAMAIQIAAAA3ACAXAADoAgAgBu0BAQDCAwAh7gEBAMIDACHvAQEAwgMAIfABAQDCAwAh8QECAMMDACHyAUAAxAMAIQIAAAAHACAXAADqAgAgAgAAAAcAIBcAAOoCACADAAAANwAgHgAA4wIAIB8AAOgCACABAAAANwAgAQAAAAcAIAUJAAC9AwAgJAAAvgMAICUAAMEDACAmAADAAwAgJwAAvwMAIAnqAQAA9AIAMOsBAADxAgAQ7AEAAPQCADDtAQEA9QIAIe4BAQD1AgAh7wEBAPUCACHwAQEA9QIAIfEBAgD2AgAh8gFAAPcCACEDAAAABwAgAQAA8AIAMCMAAPECACADAAAABwAgAQAANgAwAgAANwAgCeoBAAD0AgAw6wEAAPECABDsAQAA9AIAMO0BAQD1AgAh7gEBAPUCACHvAQEA9QIAIfABAQD1AgAh8QECAPYCACHyAUAA9wIAIQ4JAAD5AgAgJgAA_gIAICcAAP4CACDzAQEAAAAB9AEBAAAABPUBAQAAAAT2AQEAAAAB9wEBAAAAAfgBAQAAAAH5AQEAAAAB-gEBAP0CACH7AQEAAAAB_AEBAAAAAf0BAQAAAAENCQAA-QIAICQAAPwCACAlAAD5AgAgJgAA-QIAICcAAPkCACDzAQIAAAAB9AECAAAABPUBAgAAAAT2AQIAAAAB9wECAAAAAfgBAgAAAAH5AQIAAAAB-gECAPsCACELCQAA-QIAICYAAPoCACAnAAD6AgAg8wFAAAAAAfQBQAAAAAT1AUAAAAAE9gFAAAAAAfcBQAAAAAH4AUAAAAAB-QFAAAAAAfoBQAD4AgAhCwkAAPkCACAmAAD6AgAgJwAA-gIAIPMBQAAAAAH0AUAAAAAE9QFAAAAABPYBQAAAAAH3AUAAAAAB-AFAAAAAAfkBQAAAAAH6AUAA-AIAIQjzAQIAAAAB9AECAAAABPUBAgAAAAT2AQIAAAAB9wECAAAAAfgBAgAAAAH5AQIAAAAB-gECAPkCACEI8wFAAAAAAfQBQAAAAAT1AUAAAAAE9gFAAAAAAfcBQAAAAAH4AUAAAAAB-QFAAAAAAfoBQAD6AgAhDQkAAPkCACAkAAD8AgAgJQAA-QIAICYAAPkCACAnAAD5AgAg8wECAAAAAfQBAgAAAAT1AQIAAAAE9gECAAAAAfcBAgAAAAH4AQIAAAAB-QECAAAAAfoBAgD7AgAhCPMBCAAAAAH0AQgAAAAE9QEIAAAABPYBCAAAAAH3AQgAAAAB-AEIAAAAAfkBCAAAAAH6AQgA_AIAIQ4JAAD5AgAgJgAA_gIAICcAAP4CACDzAQEAAAAB9AEBAAAABPUBAQAAAAT2AQEAAAAB9wEBAAAAAfgBAQAAAAH5AQEAAAAB-gEBAP0CACH7AQEAAAAB_AEBAAAAAf0BAQAAAAEL8wEBAAAAAfQBAQAAAAT1AQEAAAAE9gEBAAAAAfcBAQAAAAH4AQEAAAAB-QEBAAAAAfoBAQD-AgAh-wEBAAAAAfwBAQAAAAH9AQEAAAABBuoBAAD_AgAw6wEAANsCABDsAQAA_wIAMO0BAQD1AgAh8AEBAPUCACH-AUAA9wIAIQbqAQAAgAMAMOsBAADIAgAQ7AEAAIADADDtAQEAgQMAIfABAQCBAwAh_gFAAIIDACEL8wEBAAAAAfQBAQAAAAT1AQEAAAAE9gEBAAAAAfcBAQAAAAH4AQEAAAAB-QEBAAAAAfoBAQD-AgAh-wEBAAAAAfwBAQAAAAH9AQEAAAABCPMBQAAAAAH0AUAAAAAE9QFAAAAABPYBQAAAAAH3AUAAAAAB-AFAAAAAAfkBQAAAAAH6AUAA-gIAIQXqAQAAgwMAMOsBAADCAgAQ7AEAAIMDADD_AQEA9QIAIYACAQD1AgAhBeoBAACEAwAw6wEAAK8CABDsAQAAhAMAMP8BAQCBAwAhgAIBAIEDACEH6gEAAIUDADDrAQAAqQIAEOwBAACFAwAw7QECAPYCACGBAgIA9gIAIYICQACGAwAhgwJAAIYDACELCQAAiAMAICYAAIkDACAnAACJAwAg8wFAAAAAAfQBQAAAAAX1AUAAAAAF9gFAAAAAAfcBQAAAAAH4AUAAAAAB-QFAAAAAAfoBQACHAwAhCwkAAIgDACAmAACJAwAgJwAAiQMAIPMBQAAAAAH0AUAAAAAF9QFAAAAABfYBQAAAAAH3AUAAAAAB-AFAAAAAAfkBQAAAAAH6AUAAhwMAIQjzAQIAAAAB9AECAAAABfUBAgAAAAX2AQIAAAAB9wECAAAAAfgBAgAAAAH5AQIAAAAB-gECAIgDACEI8wFAAAAAAfQBQAAAAAX1AUAAAAAF9gFAAAAAAfcBQAAAAAH4AUAAAAAB-QFAAAAAAfoBQACJAwAhB-oBAACKAwAw6wEAAJYCABDsAQAAigMAMO0BAgCLAwAhgQICAIsDACGCAkAAjAMAIYMCQACMAwAhCPMBAgAAAAH0AQIAAAAE9QECAAAABPYBAgAAAAH3AQIAAAAB-AECAAAAAfkBAgAAAAH6AQIA-QIAIQjzAUAAAAAB9AFAAAAABfUBQAAAAAX2AUAAAAAB9wFAAAAAAfgBQAAAAAH5AUAAAAAB-gFAAIkDACEJ6gEAAI0DADDrAQAAkAIAEOwBAACNAwAw7QEBAPUCACHyAUAA9wIAIYQCAQD1AgAhhQIBAI4DACGGAgEAjgMAIYcCAQCOAwAhDgkAAIgDACAmAACQAwAgJwAAkAMAIPMBAQAAAAH0AQEAAAAF9QEBAAAABfYBAQAAAAH3AQEAAAAB-AEBAAAAAfkBAQAAAAH6AQEAjwMAIfsBAQAAAAH8AQEAAAAB_QEBAAAAAQ4JAACIAwAgJgAAkAMAICcAAJADACDzAQEAAAAB9AEBAAAABfUBAQAAAAX2AQEAAAAB9wEBAAAAAfgBAQAAAAH5AQEAAAAB-gEBAI8DACH7AQEAAAAB_AEBAAAAAf0BAQAAAAEL8wEBAAAAAfQBAQAAAAX1AQEAAAAF9gEBAAAAAfcBAQAAAAH4AQEAAAAB-QEBAAAAAfoBAQCQAwAh-wEBAAAAAfwBAQAAAAH9AQEAAAABDAUAAJQDACAGAACTAwAgDAAAkgMAIOoBAACRAwAw6wEAABwAEOwBAACRAwAw7QEBAIEDACHyAUAAggMAIYQCAQCBAwAhhQIBAK4DACGGAgEArgMAIYcCAQCuAwAhEQMAAKcDACAFAACUAwAgDQAAqAMAIA4AALoDACAQAAC8AwAg6gEAALsDADDrAQAAAwAQ7AEAALsDADDtAQEAgQMAIe4BAQCBAwAh7wEBAIEDACHxAQIAiwMAIfIBQACCAwAhhwIBAK4DACGZAgEArgMAIaICAAADACCjAgAAAwAgEgMAAKcDACAFAACUAwAgCgAAuAMAIAsAALkDACANAACoAwAgDgAAugMAIOoBAAC3AwAw6wEAAAoAEOwBAAC3AwAw7QEBAIEDACHuAQEAgQMAIe8BAQCBAwAh8QECAIsDACHyAUAAggMAIYcCAQCuAwAhmQIBAK4DACGiAgAACgAgowIAAAoAIA8DAACnAwAgBAAAowMAIA0AAKgDACAPAACkAwAg6gEAAKYDADDrAQAABwAQ7AEAAKYDADDtAQEAgQMAIe4BAQCBAwAh7wEBAIEDACHwAQEAgQMAIfEBAgCLAwAh8gFAAIIDACGiAgAABwAgowIAAAcAIArqAQAAlQMAMOsBAADyAQAQ7AEAAJUDADDtAQEA9QIAIYYCAQD1AgAhiAIIAJYDACGJAgIA9gIAIYoCAQD1AgAhiwIgAJcDACGMAkAA9wIAIQ0JAAD5AgAgJAAA_AIAICUAAPwCACAmAAD8AgAgJwAA_AIAIPMBCAAAAAH0AQgAAAAE9QEIAAAABPYBCAAAAAH3AQgAAAAB-AEIAAAAAfkBCAAAAAH6AQgAmgMAIQUJAAD5AgAgJgAAmQMAICcAAJkDACDzASAAAAAB-gEgAJgDACEFCQAA-QIAICYAAJkDACAnAACZAwAg8wEgAAAAAfoBIACYAwAhAvMBIAAAAAH6ASAAmQMAIQ0JAAD5AgAgJAAA_AIAICUAAPwCACAmAAD8AgAgJwAA_AIAIPMBCAAAAAH0AQgAAAAE9QEIAAAABPYBCAAAAAH3AQgAAAAB-AEIAAAAAfkBCAAAAAH6AQgAmgMAIQ3qAQAAmwMAMOsBAADcAQAQ7AEAAJsDADDtAQEA9QIAIYUCAQCOAwAhhgIBAI4DACGKAgEAjgMAIY0CAQD1AgAhjgIBAPUCACGPAgIA9gIAIZACAQD1AgAhkQIBAI4DACGSAkAA9wIAIQjqAQAAnAMAMOsBAADCAQAQ7AEAAJwDADDtAQEA9QIAIfEBAgD2AgAhkwIBAPUCACGUAgEA9QIAIZUCIACXAwAhCuoBAACdAwAw6wEAAKwBABDsAQAAnQMAMO0BAQD1AgAh8AEBAPUCACHxAQIA9gIAIYYCAQD1AgAhlgIBAPUCACGXAgEA9QIAIZgCAQD1AgAhCuoBAACeAwAw6wEAAJYBABDsAQAAngMAMO0BAQD1AgAh7gEBAPUCACHvAQEA9QIAIfEBAgD2AgAh8gFAAPcCACGHAgEAjgMAIZkCAQCOAwAhCOoBAACfAwAw6wEAAH4AEOwBAACfAwAw7QEBAPUCACHxAQIA9gIAIYUCAQD1AgAhmgIBAPUCACGbAgEA9QIAIQrqAQAAoAMAMOsBAABoABDsAQAAoAMAMO0BAQD1AgAh7gEBAPUCACHvAQEA9QIAIfEBAgD2AgAh8gFAAPcCACGHAgEAjgMAIZkCAQCOAwAhCOoBAAChAwAw6wEAAFAAEOwBAAChAwAw7QEBAPUCACHvAQEA9QIAIfEBAgD2AgAh8gFAAPcCACGcAgEA9QIAIQsEAACjAwAgDwAApAMAIBEAAKUDACDqAQAAogMAMOsBAAA9ABDsAQAAogMAMO0BAQCBAwAh7wEBAIEDACHxAQIAiwMAIfIBQACCAwAhnAIBAIEDACEDnQIAAAMAIJ4CAAADACCfAgAAAwAgA50CAAAKACCeAgAACgAgnwIAAAoAIAOdAgAABwAgngIAAAcAIJ8CAAAHACANAwAApwMAIAQAAKMDACANAACoAwAgDwAApAMAIOoBAACmAwAw6wEAAAcAEOwBAACmAwAw7QEBAIEDACHuAQEAgQMAIe8BAQCBAwAh8AEBAIEDACHxAQIAiwMAIfIBQACCAwAhDQQAAKMDACAPAACkAwAgEQAApQMAIOoBAACiAwAw6wEAAD0AEOwBAACiAwAw7QEBAIEDACHvAQEAgQMAIfEBAgCLAwAh8gFAAIIDACGcAgEAgQMAIaICAAA9ACCjAgAAPQAgDgUAAJQDACAGAACTAwAgDAAAkgMAIOoBAACRAwAw6wEAABwAEOwBAACRAwAw7QEBAIEDACHyAUAAggMAIYQCAQCBAwAhhQIBAK4DACGGAgEArgMAIYcCAQCuAwAhogIAABwAIKMCAAAcACAJDAAAqgMAIOoBAACpAwAw6wEAAC0AEOwBAACpAwAw7QEBAIEDACHxAQIAiwMAIYUCAQCBAwAhmgIBAIEDACGbAgEAgQMAIREDAACnAwAgBQAAlAMAIA0AAKgDACAOAAC6AwAgEAAAvAMAIOoBAAC7AwAw6wEAAAMAEOwBAAC7AwAw7QEBAIEDACHuAQEAgQMAIe8BAQCBAwAh8QECAIsDACHyAUAAggMAIYcCAQCuAwAhmQIBAK4DACGiAgAAAwAgowIAAAMAIAKFAgEAAAABjgIBAAAAAQKGAgEAAAABjgIBAAAAAQ8GAACTAwAgDAAAkgMAIOoBAACtAwAw6wEAACEAEOwBAACtAwAw7QEBAIEDACGFAgEArgMAIYYCAQCuAwAhigIBAK4DACGNAgEAgQMAIY4CAQCBAwAhjwICAIsDACGQAgEAgQMAIZECAQCuAwAhkgJAAIIDACEL8wEBAAAAAfQBAQAAAAX1AQEAAAAF9gEBAAAAAfcBAQAAAAH4AQEAAAAB-QEBAAAAAfoBAQCQAwAh-wEBAAAAAfwBAQAAAAH9AQEAAAABCwYAALIDACDqAQAArwMAMOsBAAAYABDsAQAArwMAMO0BAQCBAwAhhgIBAIEDACGIAggAsAMAIYkCAgCLAwAhigIBAIEDACGLAiAAsQMAIYwCQACCAwAhCPMBCAAAAAH0AQgAAAAE9QEIAAAABPYBCAAAAAH3AQgAAAAB-AEIAAAAAfkBCAAAAAH6AQgA_AIAIQLzASAAAAAB-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-AMAMB8AAP0DADCkAgAA-QMAMKUCAAD6AwAwpgIAAPwDADCnAgAA_AMAMKgCAAD8AwAwqQIAAPsDACCqAgAA_AMAMKsCAAD-AwAwrAIAAP8DADAHHgAA8QMAIB8AAPQDACCkAgAA8gMAIKUCAADzAwAgpgIAABwAIKcCAAAcACCoAgAA9QEAIAseAADjAwAwHwAA6AMAMKQCAADkAwAwpQIAAOUDADCmAgAA5wMAMKcCAADnAwAwqAIAAOcDADCpAgAA5gMAIKoCAADnAwAwqwIAAOkDADCsAgAA6gMAMAoMAADwAwAg7QEBAAAAAYUCAQAAAAGKAgEAAAABjQIBAAAAAY4CAQAAAAGPAgIAAAABkAIBAAAAAZECAQAAAAGSAkAAAAABAgAAACMAIB4AAO8DACADAAAAIwAgHgAA7wMAIB8AAO0DACABFwAAjwYAMBEGAACTAwAgDAAAkgMAIOoBAACtAwAw6wEAACEAEOwBAACtAwAw7QEBAAAAAYUCAQCuAwAhhgIBAK4DACGKAgEArgMAIY0CAQCBAwAhjgIBAIEDACGPAgIAiwMAIZACAQCBAwAhkQIBAK4DACGSAkAAggMAIaACAACrAwAgoQIAAKwDACACAAAAIwAgFwAA7QMAIAIAAADrAwAgFwAA7AMAIA3qAQAA6gMAMOsBAADrAwAQ7AEAAOoDADDtAQEAgQMAIYUCAQCuAwAhhgIBAK4DACGKAgEArgMAIY0CAQCBAwAhjgIBAIEDACGPAgIAiwMAIZACAQCBAwAhkQIBAK4DACGSAkAAggMAIQ3qAQAA6gMAMOsBAADrAwAQ7AEAAOoDADDtAQEAgQMAIYUCAQCuAwAhhgIBAK4DACGKAgEArgMAIY0CAQCBAwAhjgIBAIEDACGPAgIAiwMAIZACAQCBAwAhkQIBAK4DACGSAkAAggMAIQntAQEAwgMAIYUCAQDQAwAhigIBANADACGNAgEAwgMAIY4CAQDCAwAhjwICAMMDACGQAgEAwgMAIZECAQDQAwAhkgJAAMQDACEKDAAA7gMAIO0BAQDCAwAhhQIBANADACGKAgEA0AMAIY0CAQDCAwAhjgIBAMIDACGPAgIAwwMAIZACAQDCAwAhkQIBANADACGSAkAAxAMAIQceAACKBgAgHwAAjQYAIKQCAACLBgAgpQIAAIwGACCmAgAAAwAgpwIAAAMAIKgCAAAFACAKDAAA8AMAIO0BAQAAAAGFAgEAAAABigIBAAAAAY0CAQAAAAGOAgEAAAABjwICAAAAAZACAQAAAAGRAgEAAAABkgJAAAAAAQMeAACKBgAgpAIAAIsGACCoAgAABQAgBwUAAPcDACAMAADRAwAg7QEBAAAAAfIBQAAAAAGEAgEAAAABhQIBAAAAAYcCAQAAAAECAAAA9QEAIB4AAPEDACADAAAAHAAgHgAA8QMAIB8AAPUDACAJAAAAHAAgBQAA9gMAIAwAAM4DACAXAAD1AwAg7QEBAMIDACHyAUAAxAMAIYQCAQDCAwAhhQIBANADACGHAgEA0AMAIQcFAAD2AwAgDAAAzgMAIO0BAQDCAwAh8gFAAMQDACGEAgEAwgMAIYUCAQDQAwAhhwIBANADACEHHgAAhQYAIB8AAIgGACCkAgAAhgYAIKUCAACHBgAgpgIAAAcAIKcCAAAHACCoAgAANwAgAx4AAIUGACCkAgAAhgYAIKgCAAA3ACAG7QEBAAAAAYgCCAAAAAGJAgIAAAABigIBAAAAAYsCIAAAAAGMAkAAAAABAgAAABoAIB4AAIUEACADAAAAGgAgHgAAhQQAIB8AAIQEACABFwAAhAYAMAsGAACyAwAg6gEAAK8DADDrAQAAGAAQ7AEAAK8DADDtAQEAAAABhgIBAIEDACGIAggAsAMAIYkCAgCLAwAhigIBAIEDACGLAiAAsQMAIYwCQACCAwAhAgAAABoAIBcAAIQEACACAAAAgAQAIBcAAIEEACAK6gEAAP8DADDrAQAAgAQAEOwBAAD_AwAw7QEBAIEDACGGAgEAgQMAIYgCCACwAwAhiQICAIsDACGKAgEAgQMAIYsCIACxAwAhjAJAAIIDACEK6gEAAP8DADDrAQAAgAQAEOwBAAD_AwAw7QEBAIEDACGGAgEAgQMAIYgCCACwAwAhiQICAIsDACGKAgEAgQMAIYsCIACxAwAhjAJAAIIDACEG7QEBAMIDACGIAggAggQAIYkCAgDDAwAhigIBAMIDACGLAiAAgwQAIYwCQADEAwAhBaoCCAAAAAGtAggAAAABrgIIAAAAAa8CCAAAAAGwAggAAAABAaoCIAAAAAEG7QEBAMIDACGIAggAggQAIYkCAgDDAwAhigIBAMIDACGLAiAAgwQAIYwCQADEAwAhBu0BAQAAAAGIAggAAAABiQICAAAAAYoCAQAAAAGLAiAAAAABjAJAAAAAAQcIAACfBAAg7QEBAAAAAfABAQAAAAHxAQIAAAABlgIBAAAAAZcCAQAAAAGYAgEAAAABAgAAABEAIB4AAJ4EACADAAAAEQAgHgAAngQAIB8AAJAEACABFwAAgwYAMAwGAACyAwAgCAAAtgMAIOoBAAC1AwAw6wEAAA8AEOwBAAC1AwAw7QEBAAAAAfABAQCBAwAh8QECAIsDACGGAgEAgQMAIZYCAQCBAwAhlwIBAIEDACGYAgEAgQMAIQIAAAARACAXAACQBAAgAgAAAI4EACAXAACPBAAgCuoBAACNBAAw6wEAAI4EABDsAQAAjQQAMO0BAQCBAwAh8AEBAIEDACHxAQIAiwMAIYYCAQCBAwAhlgIBAIEDACGXAgEAgQMAIZgCAQCBAwAhCuoBAACNBAAw6wEAAI4EABDsAQAAjQQAMO0BAQCBAwAh8AEBAIEDACHxAQIAiwMAIYYCAQCBAwAhlgIBAIEDACGXAgEAgQMAIZgCAQCBAwAhBu0BAQDCAwAh8AEBAMIDACHxAQIAwwMAIZYCAQDCAwAhlwIBAMIDACGYAgEAwgMAIQcIAACRBAAg7QEBAMIDACHwAQEAwgMAIfEBAgDDAwAhlgIBAMIDACGXAgEAwgMAIZgCAQDCAwAhCx4AAJIEADAfAACXBAAwpAIAAJMEADClAgAAlAQAMKYCAACWBAAwpwIAAJYEADCoAgAAlgQAMKkCAACVBAAgqgIAAJYEADCrAgAAmAQAMKwCAACZBAAwBO0BAQAAAAHxAQIAAAABlAIBAAAAAZUCIAAAAAECAAAAFQAgHgAAnQQAIAMAAAAVACAeAACdBAAgHwAAnAQAIAEXAACCBgAwCQcAALQDACDqAQAAswMAMOsBAAATABDsAQAAswMAMO0BAQAAAAHxAQIAiwMAIZMCAQCBAwAhlAIBAIEDACGVAiAAsQMAIQIAAAAVACAXAACcBAAgAgAAAJoEACAXAACbBAAgCOoBAACZBAAw6wEAAJoEABDsAQAAmQQAMO0BAQCBAwAh8QECAIsDACGTAgEAgQMAIZQCAQCBAwAhlQIgALEDACEI6gEAAJkEADDrAQAAmgQAEOwBAACZBAAw7QEBAIEDACHxAQIAiwMAIZMCAQCBAwAhlAIBAIEDACGVAiAAsQMAIQTtAQEAwgMAIfEBAgDDAwAhlAIBAMIDACGVAiAAgwQAIQTtAQEAwgMAIfEBAgDDAwAhlAIBAMIDACGVAiAAgwQAIQTtAQEAAAAB8QECAAAAAZQCAQAAAAGVAiAAAAABBwgAAJ8EACDtAQEAAAAB8AEBAAAAAfEBAgAAAAGWAgEAAAABlwIBAAAAAZgCAQAAAAEEHgAAkgQAMKQCAACTBAAwqAIAAJYEADCpAgAAlQQAIAsDAAChBAAgCgAAogQAIAsAAKMEACANAACkBAAgDgAApQQAIO0BAQAAAAHuAQEAAAAB7wEBAAAAAfEBAgAAAAHyAUAAAAABmQIBAAAAAQMeAACABgAgpAIAAIEGACCoAgAAAQAgBB4AAIYEADCkAgAAhwQAMKgCAACKBAAwqQIAAIkEACAEHgAA-AMAMKQCAAD5AwAwqAIAAPwDADCpAgAA-wMAIAMeAADxAwAgpAIAAPIDACCoAgAA9QEAIAQeAADjAwAwpAIAAOQDADCoAgAA5wMAMKkCAADmAwAgCgMAANIEACANAADUBAAgDgAA1QQAIBAAANMEACDtAQEAAAAB7gEBAAAAAe8BAQAAAAHxAQIAAAAB8gFAAAAAAZkCAQAAAAECAAAABQAgHgAA0QQAIAMAAAAFACAeAADRBAAgHwAAsAQAIAEXAAD_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_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-JQB_JgCAAScAgQESAgETPAEUPwEVQAEWQQEYQwEZRRAaRhEbSAEcShAdSxIgTAEhTQEiThAoURMpUhkqUwIrVAIsVQItVgIuVwIvWQIwWxAxXBoyXwIzYRA0Yhs1ZAI2ZQI3ZhA4aRw5aiI6aw07bA08bQ09bg0-bw0_cQ1AcxBBdCNCdg1DeBBEeSRFeg1Gew1HfBBIfyVJgAErSoEBBEuCAQRMgwEETYQBBE6FAQRPhwEEUIkBEFGKASxSjQEEU48BEFSQAS1VkgEEVpMBBFeUARBYlwEuWZgBNFqZAQVbmgEFXJsBBV2cAQVenQEFX58BBWChARBhogE1YqQBBWOmARBkpwE2ZagBBWapAQVnqgEQaK0BN2muAT1qrwEGa7ABBmyxAQZtsgEGbrMBBm-1AQZwtwEQcbgBPnK6AQZzvAEQdL0BP3W-AQZ2vwEGd8ABEHjDAUB5xAFGesUBCnvGAQp8xwEKfcgBCn7JAQp_ywEKgAHNARCBAc4BR4IB0gEKgwHUARCEAdUBSIUB2AEKhgHZAQqHAdoBEIgB3QFJiQHeAU-KAd8BCIsB4AEIjAHhAQiNAeIBCI4B4wEIjwHlAQiQAecBEJEB6AFQkgHqAQiTAewBEJQB7QFRlQHuAQiWAe8BCJcB8AEQmAHzAVKZAfQBWJoB9gEJmwH3AQmcAfkBCZ0B-gEJngH7AQmfAf0BCaAB_wEQoQGAAlmiAYUCCaMBhwIQpAGIAlqlAYwCCaYBjQIJpwGOAhCoAZECW6kBkgJfqgGUAmCrAZUCYKwBmAJgrQGZAmCuAZoCYK8BnAJgsAGeAhCxAZ8CYbIBoQJgswGjAhC0AaQCYrUBpQJgtgGmAmC3AacCELgBqgJjuQGrAmm6Aa0CarsBrgJqvAGxAmq9AbICar4BswJqvwG1AmrAAbcCEMEBuAJrwgG6AmrDAbwCEMQBvQJsxQG-AmrGAb8CascBwAIQyAHDAm3JAcQCccoBxgJyywHHAnLMAcoCcs0BywJyzgHMAnLPAc4CctAB0AIQ0QHRAnPSAdMCctMB1QIQ1AHWAnTVAdcCctYB2AJy1wHZAhDYAdwCddkB3QJ52gHeAgPbAd8CA9wB4AID3QHhAgPeAeICA98B5AID4AHmAhDhAecCeuIB6QID4wHrAhDkAewCe-UB7QID5gHuAgPnAe8CEOgB8gJ86QHzAoIB" } async function decodeBase64AsWasm(wasmBase64: string): Promise { @@ -298,6 +298,16 @@ export interface PrismaClient< */ get setting(): Prisma.SettingDelegate; + /** + * `prisma.studyActivity`: Exposes CRUD operations for the **StudyActivity** model. + * Example usage: + * ```ts + * // Fetch zero or more StudyActivities + * const studyActivities = await prisma.studyActivity.findMany() + * ``` + */ + get studyActivity(): Prisma.StudyActivityDelegate; + /** * `prisma.materialGroup`: Exposes CRUD operations for the **MaterialGroup** model. * Example usage: diff --git a/src/generated/prisma/internal/prismaNamespace.ts b/src/generated/prisma/internal/prismaNamespace.ts index e12ee22..7f2be5f 100644 --- a/src/generated/prisma/internal/prismaNamespace.ts +++ b/src/generated/prisma/internal/prismaNamespace.ts @@ -395,6 +395,7 @@ export const ModelName = { ShareLink: 'ShareLink', AuthSecurity: 'AuthSecurity', Setting: 'Setting', + StudyActivity: 'StudyActivity', MaterialGroup: 'MaterialGroup' } as const @@ -411,7 +412,7 @@ export type TypeMap + fields: Prisma.StudyActivityFieldRefs + operations: { + findUnique: { + args: Prisma.StudyActivityFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.StudyActivityFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.StudyActivityFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.StudyActivityFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.StudyActivityFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.StudyActivityCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.StudyActivityCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.StudyActivityCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.StudyActivityDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.StudyActivityUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.StudyActivityDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.StudyActivityUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.StudyActivityUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.StudyActivityUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.StudyActivityAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.StudyActivityGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.StudyActivityCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } MaterialGroup: { payload: Prisma.$MaterialGroupPayload fields: Prisma.MaterialGroupFieldRefs @@ -1470,6 +1545,15 @@ export const SettingScalarFieldEnum = { export type SettingScalarFieldEnum = (typeof SettingScalarFieldEnum)[keyof typeof SettingScalarFieldEnum] +export const StudyActivityScalarFieldEnum = { + id: 'id', + type: 'type', + occurredAt: 'occurredAt' +} as const + +export type StudyActivityScalarFieldEnum = (typeof StudyActivityScalarFieldEnum)[keyof typeof StudyActivityScalarFieldEnum] + + export const MaterialGroupScalarFieldEnum = { id: 'id', classId: 'classId', @@ -1659,6 +1743,7 @@ export type GlobalOmitConfig = { shareLink?: Prisma.ShareLinkOmit authSecurity?: Prisma.AuthSecurityOmit setting?: Prisma.SettingOmit + studyActivity?: Prisma.StudyActivityOmit materialGroup?: Prisma.MaterialGroupOmit } diff --git a/src/generated/prisma/internal/prismaNamespaceBrowser.ts b/src/generated/prisma/internal/prismaNamespaceBrowser.ts index 90ca7f0..7b45804 100644 --- a/src/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/src/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -62,6 +62,7 @@ export const ModelName = { ShareLink: 'ShareLink', AuthSecurity: 'AuthSecurity', Setting: 'Setting', + StudyActivity: 'StudyActivity', MaterialGroup: 'MaterialGroup' } as const @@ -209,6 +210,15 @@ export const SettingScalarFieldEnum = { export type SettingScalarFieldEnum = (typeof SettingScalarFieldEnum)[keyof typeof SettingScalarFieldEnum] +export const StudyActivityScalarFieldEnum = { + id: 'id', + type: 'type', + occurredAt: 'occurredAt' +} as const + +export type StudyActivityScalarFieldEnum = (typeof StudyActivityScalarFieldEnum)[keyof typeof StudyActivityScalarFieldEnum] + + export const MaterialGroupScalarFieldEnum = { id: 'id', classId: 'classId', diff --git a/src/generated/prisma/models.ts b/src/generated/prisma/models.ts index 145eb9e..b7bcb19 100644 --- a/src/generated/prisma/models.ts +++ b/src/generated/prisma/models.ts @@ -19,5 +19,6 @@ export type * from './models/QuizAttempt' export type * from './models/ShareLink' export type * from './models/AuthSecurity' export type * from './models/Setting' +export type * from './models/StudyActivity' export type * from './models/MaterialGroup' export type * from './commonInputTypes' \ No newline at end of file diff --git a/src/generated/prisma/models/StudyActivity.ts b/src/generated/prisma/models/StudyActivity.ts new file mode 100644 index 0000000..316fa78 --- /dev/null +++ b/src/generated/prisma/models/StudyActivity.ts @@ -0,0 +1,1091 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `StudyActivity` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums" +import type * as Prisma from "../internal/prismaNamespace" + +/** + * Model StudyActivity + * + */ +export type StudyActivityModel = runtime.Types.Result.DefaultSelection + +export type AggregateStudyActivity = { + _count: StudyActivityCountAggregateOutputType | null + _min: StudyActivityMinAggregateOutputType | null + _max: StudyActivityMaxAggregateOutputType | null +} + +export type StudyActivityMinAggregateOutputType = { + id: string | null + type: string | null + occurredAt: Date | null +} + +export type StudyActivityMaxAggregateOutputType = { + id: string | null + type: string | null + occurredAt: Date | null +} + +export type StudyActivityCountAggregateOutputType = { + id: number + type: number + occurredAt: number + _all: number +} + + +export type StudyActivityMinAggregateInputType = { + id?: true + type?: true + occurredAt?: true +} + +export type StudyActivityMaxAggregateInputType = { + id?: true + type?: true + occurredAt?: true +} + +export type StudyActivityCountAggregateInputType = { + id?: true + type?: true + occurredAt?: true + _all?: true +} + +export type StudyActivityAggregateArgs = { + /** + * Filter which StudyActivity to aggregate. + */ + where?: Prisma.StudyActivityWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of StudyActivities to fetch. + */ + orderBy?: Prisma.StudyActivityOrderByWithRelationInput | Prisma.StudyActivityOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.StudyActivityWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` StudyActivities from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` StudyActivities. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned StudyActivities + **/ + _count?: true | StudyActivityCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: StudyActivityMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: StudyActivityMaxAggregateInputType +} + +export type GetStudyActivityAggregateType = { + [P in keyof T & keyof AggregateStudyActivity]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type StudyActivityGroupByArgs = { + where?: Prisma.StudyActivityWhereInput + orderBy?: Prisma.StudyActivityOrderByWithAggregationInput | Prisma.StudyActivityOrderByWithAggregationInput[] + by: Prisma.StudyActivityScalarFieldEnum[] | Prisma.StudyActivityScalarFieldEnum + having?: Prisma.StudyActivityScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: StudyActivityCountAggregateInputType | true + _min?: StudyActivityMinAggregateInputType + _max?: StudyActivityMaxAggregateInputType +} + +export type StudyActivityGroupByOutputType = { + id: string + type: string + occurredAt: Date + _count: StudyActivityCountAggregateOutputType | null + _min: StudyActivityMinAggregateOutputType | null + _max: StudyActivityMaxAggregateOutputType | null +} + +export type GetStudyActivityGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof StudyActivityGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type StudyActivityWhereInput = { + AND?: Prisma.StudyActivityWhereInput | Prisma.StudyActivityWhereInput[] + OR?: Prisma.StudyActivityWhereInput[] + NOT?: Prisma.StudyActivityWhereInput | Prisma.StudyActivityWhereInput[] + id?: Prisma.StringFilter<"StudyActivity"> | string + type?: Prisma.StringFilter<"StudyActivity"> | string + occurredAt?: Prisma.DateTimeFilter<"StudyActivity"> | Date | string +} + +export type StudyActivityOrderByWithRelationInput = { + id?: Prisma.SortOrder + type?: Prisma.SortOrder + occurredAt?: Prisma.SortOrder +} + +export type StudyActivityWhereUniqueInput = Prisma.AtLeast<{ + id?: string + AND?: Prisma.StudyActivityWhereInput | Prisma.StudyActivityWhereInput[] + OR?: Prisma.StudyActivityWhereInput[] + NOT?: Prisma.StudyActivityWhereInput | Prisma.StudyActivityWhereInput[] + type?: Prisma.StringFilter<"StudyActivity"> | string + occurredAt?: Prisma.DateTimeFilter<"StudyActivity"> | Date | string +}, "id"> + +export type StudyActivityOrderByWithAggregationInput = { + id?: Prisma.SortOrder + type?: Prisma.SortOrder + occurredAt?: Prisma.SortOrder + _count?: Prisma.StudyActivityCountOrderByAggregateInput + _max?: Prisma.StudyActivityMaxOrderByAggregateInput + _min?: Prisma.StudyActivityMinOrderByAggregateInput +} + +export type StudyActivityScalarWhereWithAggregatesInput = { + AND?: Prisma.StudyActivityScalarWhereWithAggregatesInput | Prisma.StudyActivityScalarWhereWithAggregatesInput[] + OR?: Prisma.StudyActivityScalarWhereWithAggregatesInput[] + NOT?: Prisma.StudyActivityScalarWhereWithAggregatesInput | Prisma.StudyActivityScalarWhereWithAggregatesInput[] + id?: Prisma.StringWithAggregatesFilter<"StudyActivity"> | string + type?: Prisma.StringWithAggregatesFilter<"StudyActivity"> | string + occurredAt?: Prisma.DateTimeWithAggregatesFilter<"StudyActivity"> | Date | string +} + +export type StudyActivityCreateInput = { + id?: string + type: string + occurredAt?: Date | string +} + +export type StudyActivityUncheckedCreateInput = { + id?: string + type: string + occurredAt?: Date | string +} + +export type StudyActivityUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.StringFieldUpdateOperationsInput | string + occurredAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type StudyActivityUncheckedUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.StringFieldUpdateOperationsInput | string + occurredAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type StudyActivityCreateManyInput = { + id?: string + type: string + occurredAt?: Date | string +} + +export type StudyActivityUpdateManyMutationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.StringFieldUpdateOperationsInput | string + occurredAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type StudyActivityUncheckedUpdateManyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.StringFieldUpdateOperationsInput | string + occurredAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type StudyActivityCountOrderByAggregateInput = { + id?: Prisma.SortOrder + type?: Prisma.SortOrder + occurredAt?: Prisma.SortOrder +} + +export type StudyActivityMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + type?: Prisma.SortOrder + occurredAt?: Prisma.SortOrder +} + +export type StudyActivityMinOrderByAggregateInput = { + id?: Prisma.SortOrder + type?: Prisma.SortOrder + occurredAt?: Prisma.SortOrder +} + + + +export type StudyActivitySelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + type?: boolean + occurredAt?: boolean +}, ExtArgs["result"]["studyActivity"]> + +export type StudyActivitySelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + type?: boolean + occurredAt?: boolean +}, ExtArgs["result"]["studyActivity"]> + +export type StudyActivitySelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + type?: boolean + occurredAt?: boolean +}, ExtArgs["result"]["studyActivity"]> + +export type StudyActivitySelectScalar = { + id?: boolean + type?: boolean + occurredAt?: boolean +} + +export type StudyActivityOmit = runtime.Types.Extensions.GetOmit<"id" | "type" | "occurredAt", ExtArgs["result"]["studyActivity"]> + +export type $StudyActivityPayload = { + name: "StudyActivity" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: string + type: string + occurredAt: Date + }, ExtArgs["result"]["studyActivity"]> + composites: {} +} + +export type StudyActivityGetPayload = runtime.Types.Result.GetResult + +export type StudyActivityCountArgs = + Omit & { + select?: StudyActivityCountAggregateInputType | true + } + +export interface StudyActivityDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['StudyActivity'], meta: { name: 'StudyActivity' } } + /** + * Find zero or one StudyActivity that matches the filter. + * @param {StudyActivityFindUniqueArgs} args - Arguments to find a StudyActivity + * @example + * // Get one StudyActivity + * const studyActivity = await prisma.studyActivity.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one StudyActivity that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {StudyActivityFindUniqueOrThrowArgs} args - Arguments to find a StudyActivity + * @example + * // Get one StudyActivity + * const studyActivity = await prisma.studyActivity.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first StudyActivity that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {StudyActivityFindFirstArgs} args - Arguments to find a StudyActivity + * @example + * // Get one StudyActivity + * const studyActivity = await prisma.studyActivity.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first StudyActivity that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {StudyActivityFindFirstOrThrowArgs} args - Arguments to find a StudyActivity + * @example + * // Get one StudyActivity + * const studyActivity = await prisma.studyActivity.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more StudyActivities that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {StudyActivityFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all StudyActivities + * const studyActivities = await prisma.studyActivity.findMany() + * + * // Get first 10 StudyActivities + * const studyActivities = await prisma.studyActivity.findMany({ take: 10 }) + * + * // Only select the `id` + * const studyActivityWithIdOnly = await prisma.studyActivity.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a StudyActivity. + * @param {StudyActivityCreateArgs} args - Arguments to create a StudyActivity. + * @example + * // Create one StudyActivity + * const StudyActivity = await prisma.studyActivity.create({ + * data: { + * // ... data to create a StudyActivity + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many StudyActivities. + * @param {StudyActivityCreateManyArgs} args - Arguments to create many StudyActivities. + * @example + * // Create many StudyActivities + * const studyActivity = await prisma.studyActivity.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many StudyActivities and returns the data saved in the database. + * @param {StudyActivityCreateManyAndReturnArgs} args - Arguments to create many StudyActivities. + * @example + * // Create many StudyActivities + * const studyActivity = await prisma.studyActivity.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many StudyActivities and only return the `id` + * const studyActivityWithIdOnly = await prisma.studyActivity.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a StudyActivity. + * @param {StudyActivityDeleteArgs} args - Arguments to delete one StudyActivity. + * @example + * // Delete one StudyActivity + * const StudyActivity = await prisma.studyActivity.delete({ + * where: { + * // ... filter to delete one StudyActivity + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one StudyActivity. + * @param {StudyActivityUpdateArgs} args - Arguments to update one StudyActivity. + * @example + * // Update one StudyActivity + * const studyActivity = await prisma.studyActivity.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more StudyActivities. + * @param {StudyActivityDeleteManyArgs} args - Arguments to filter StudyActivities to delete. + * @example + * // Delete a few StudyActivities + * const { count } = await prisma.studyActivity.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more StudyActivities. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {StudyActivityUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many StudyActivities + * const studyActivity = await prisma.studyActivity.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more StudyActivities and returns the data updated in the database. + * @param {StudyActivityUpdateManyAndReturnArgs} args - Arguments to update many StudyActivities. + * @example + * // Update many StudyActivities + * const studyActivity = await prisma.studyActivity.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more StudyActivities and only return the `id` + * const studyActivityWithIdOnly = await prisma.studyActivity.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one StudyActivity. + * @param {StudyActivityUpsertArgs} args - Arguments to update or create a StudyActivity. + * @example + * // Update or create a StudyActivity + * const studyActivity = await prisma.studyActivity.upsert({ + * create: { + * // ... data to create a StudyActivity + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the StudyActivity we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__StudyActivityClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of StudyActivities. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {StudyActivityCountArgs} args - Arguments to filter StudyActivities to count. + * @example + * // Count the number of StudyActivities + * const count = await prisma.studyActivity.count({ + * where: { + * // ... the filter for the StudyActivities we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a StudyActivity. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {StudyActivityAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by StudyActivity. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {StudyActivityGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends StudyActivityGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: StudyActivityGroupByArgs['orderBy'] } + : { orderBy?: StudyActivityGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetStudyActivityGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the StudyActivity model + */ +readonly fields: StudyActivityFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for StudyActivity. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__StudyActivityClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the StudyActivity model + */ +export interface StudyActivityFieldRefs { + readonly id: Prisma.FieldRef<"StudyActivity", 'String'> + readonly type: Prisma.FieldRef<"StudyActivity", 'String'> + readonly occurredAt: Prisma.FieldRef<"StudyActivity", 'DateTime'> +} + + +// Custom InputTypes +/** + * StudyActivity findUnique + */ +export type StudyActivityFindUniqueArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * Filter, which StudyActivity to fetch. + */ + where: Prisma.StudyActivityWhereUniqueInput +} + +/** + * StudyActivity findUniqueOrThrow + */ +export type StudyActivityFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * Filter, which StudyActivity to fetch. + */ + where: Prisma.StudyActivityWhereUniqueInput +} + +/** + * StudyActivity findFirst + */ +export type StudyActivityFindFirstArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * Filter, which StudyActivity to fetch. + */ + where?: Prisma.StudyActivityWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of StudyActivities to fetch. + */ + orderBy?: Prisma.StudyActivityOrderByWithRelationInput | Prisma.StudyActivityOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for StudyActivities. + */ + cursor?: Prisma.StudyActivityWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` StudyActivities from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` StudyActivities. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of StudyActivities. + */ + distinct?: Prisma.StudyActivityScalarFieldEnum | Prisma.StudyActivityScalarFieldEnum[] +} + +/** + * StudyActivity findFirstOrThrow + */ +export type StudyActivityFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * Filter, which StudyActivity to fetch. + */ + where?: Prisma.StudyActivityWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of StudyActivities to fetch. + */ + orderBy?: Prisma.StudyActivityOrderByWithRelationInput | Prisma.StudyActivityOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for StudyActivities. + */ + cursor?: Prisma.StudyActivityWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` StudyActivities from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` StudyActivities. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of StudyActivities. + */ + distinct?: Prisma.StudyActivityScalarFieldEnum | Prisma.StudyActivityScalarFieldEnum[] +} + +/** + * StudyActivity findMany + */ +export type StudyActivityFindManyArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * Filter, which StudyActivities to fetch. + */ + where?: Prisma.StudyActivityWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of StudyActivities to fetch. + */ + orderBy?: Prisma.StudyActivityOrderByWithRelationInput | Prisma.StudyActivityOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing StudyActivities. + */ + cursor?: Prisma.StudyActivityWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` StudyActivities from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` StudyActivities. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of StudyActivities. + */ + distinct?: Prisma.StudyActivityScalarFieldEnum | Prisma.StudyActivityScalarFieldEnum[] +} + +/** + * StudyActivity create + */ +export type StudyActivityCreateArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * The data needed to create a StudyActivity. + */ + data: Prisma.XOR +} + +/** + * StudyActivity createMany + */ +export type StudyActivityCreateManyArgs = { + /** + * The data used to create many StudyActivities. + */ + data: Prisma.StudyActivityCreateManyInput | Prisma.StudyActivityCreateManyInput[] +} + +/** + * StudyActivity createManyAndReturn + */ +export type StudyActivityCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelectCreateManyAndReturn | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * The data used to create many StudyActivities. + */ + data: Prisma.StudyActivityCreateManyInput | Prisma.StudyActivityCreateManyInput[] +} + +/** + * StudyActivity update + */ +export type StudyActivityUpdateArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * The data needed to update a StudyActivity. + */ + data: Prisma.XOR + /** + * Choose, which StudyActivity to update. + */ + where: Prisma.StudyActivityWhereUniqueInput +} + +/** + * StudyActivity updateMany + */ +export type StudyActivityUpdateManyArgs = { + /** + * The data used to update StudyActivities. + */ + data: Prisma.XOR + /** + * Filter which StudyActivities to update + */ + where?: Prisma.StudyActivityWhereInput + /** + * Limit how many StudyActivities to update. + */ + limit?: number +} + +/** + * StudyActivity updateManyAndReturn + */ +export type StudyActivityUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelectUpdateManyAndReturn | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * The data used to update StudyActivities. + */ + data: Prisma.XOR + /** + * Filter which StudyActivities to update + */ + where?: Prisma.StudyActivityWhereInput + /** + * Limit how many StudyActivities to update. + */ + limit?: number +} + +/** + * StudyActivity upsert + */ +export type StudyActivityUpsertArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * The filter to search for the StudyActivity to update in case it exists. + */ + where: Prisma.StudyActivityWhereUniqueInput + /** + * In case the StudyActivity found by the `where` argument doesn't exist, create a new StudyActivity with this data. + */ + create: Prisma.XOR + /** + * In case the StudyActivity was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * StudyActivity delete + */ +export type StudyActivityDeleteArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null + /** + * Filter which StudyActivity to delete. + */ + where: Prisma.StudyActivityWhereUniqueInput +} + +/** + * StudyActivity deleteMany + */ +export type StudyActivityDeleteManyArgs = { + /** + * Filter which StudyActivities to delete + */ + where?: Prisma.StudyActivityWhereInput + /** + * Limit how many StudyActivities to delete. + */ + limit?: number +} + +/** + * StudyActivity without action + */ +export type StudyActivityDefaultArgs = { + /** + * Select specific fields to fetch from the StudyActivity + */ + select?: Prisma.StudyActivitySelect | null + /** + * Omit specific fields from the StudyActivity + */ + omit?: Prisma.StudyActivityOmit | null +} diff --git a/src/lib/activityClient.ts b/src/lib/activityClient.ts new file mode 100644 index 0000000..6eaaafd --- /dev/null +++ b/src/lib/activityClient.ts @@ -0,0 +1,9 @@ +import type { StudyActivityType } from "@/lib/validation/activitySchemas"; + +export function recordStudyActivity(type: StudyActivityType) { + return fetch("/api/activity", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type }), + }); +} diff --git a/src/lib/validation/activitySchemas.ts b/src/lib/validation/activitySchemas.ts new file mode 100644 index 0000000..a71c764 --- /dev/null +++ b/src/lib/validation/activitySchemas.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const studyActivitySchema = z.object({ + type: z.enum(["FLASHCARD", "QUIZ_QUESTION"]), +}); + +export type StudyActivityType = z.infer["type"]; diff --git a/src/services/activityService.ts b/src/services/activityService.ts new file mode 100644 index 0000000..dea1574 --- /dev/null +++ b/src/services/activityService.ts @@ -0,0 +1,104 @@ +import { prisma } from "@/lib/db"; +import type { StudyActivityType } from "@/lib/validation/activitySchemas"; + +const ARIZONA_OFFSET_MS = 7 * 60 * 60 * 1000; +const WEEKS_TO_DISPLAY = 53; +const DAYS_TO_DISPLAY = WEEKS_TO_DISPLAY * 7; +const STREAK_THRESHOLD = 20; + +export interface DailyActivity { + date: string; + flashcards: number; + questions: number; + total: number; + level: 0 | 1 | 2 | 3 | 4; +} + +export interface ActivitySummary { + days: DailyActivity[]; + today: string; + activeDays: number; + flashcards: number; + questions: number; + currentStreak: number; +} + +function toArizonaDateKey(date: Date): string { + return new Date(date.getTime() - ARIZONA_OFFSET_MS).toISOString().slice(0, 10); +} + +function dateKeyToUtcDate(dateKey: string): Date { + return new Date(`${dateKey}T00:00:00.000Z`); +} + +function addDays(dateKey: string, days: number): string { + const date = dateKeyToUtcDate(dateKey); + date.setUTCDate(date.getUTCDate() + days); + return date.toISOString().slice(0, 10); +} + +function getLevel(total: number): 0 | 1 | 2 | 3 | 4 { + if (total === 0) return 0; + if (total < 20) return 1; + if (total < 50) return 2; + if (total < 100) return 3; + return 4; +} + +function emptyDay(date: string): DailyActivity { + return { date, flashcards: 0, questions: 0, total: 0, level: 0 }; +} + +export async function recordActivity(type: StudyActivityType) { + return prisma.studyActivity.create({ data: { type } }); +} + +export async function getActivitySummary(now = new Date()): Promise { + const today = toArizonaDateKey(now); + const todayDate = dateKeyToUtcDate(today); + const dayOfWeek = todayDate.getUTCDay(); + const startDate = addDays(today, -(dayOfWeek + (WEEKS_TO_DISPLAY - 1) * 7)); + const endDate = addDays(startDate, DAYS_TO_DISPLAY - 1); + + const activities = await prisma.studyActivity.findMany({ + select: { type: true, occurredAt: true }, + orderBy: { occurredAt: "asc" }, + }); + const totals = new Map(); + + for (const activity of activities) { + const date = toArizonaDateKey(activity.occurredAt); + const day = totals.get(date) ?? emptyDay(date); + if (activity.type === "FLASHCARD") day.flashcards += 1; + if (activity.type === "QUIZ_QUESTION") day.questions += 1; + day.total += 1; + day.level = getLevel(day.total); + totals.set(date, day); + } + + const days = Array.from({ length: DAYS_TO_DISPLAY }, (_, index) => { + const date = addDays(startDate, index); + return totals.get(date) ?? emptyDay(date); + }); + const visibleDays = days.filter((day) => day.date <= today && day.date <= endDate); + const flashcards = visibleDays.reduce((sum, day) => sum + day.flashcards, 0); + const questions = visibleDays.reduce((sum, day) => sum + day.questions, 0); + + let streakDate = (totals.get(today)?.total ?? 0) >= STREAK_THRESHOLD + ? today + : addDays(today, -1); + let currentStreak = 0; + while ((totals.get(streakDate)?.total ?? 0) >= STREAK_THRESHOLD) { + currentStreak += 1; + streakDate = addDays(streakDate, -1); + } + + return { + days, + today, + activeDays: visibleDays.filter((day) => day.total > 0).length, + flashcards, + questions, + currentStreak, + }; +} From e3265f03e2f6cf402195c42223c45b492820b343 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 12 Jul 2026 12:01:31 -0700 Subject: [PATCH 05/11] Remove activity summary stats from banner --- src/components/activity/ActivityBanner.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/activity/ActivityBanner.tsx b/src/components/activity/ActivityBanner.tsx index 0bcaf0a..eea45cf 100644 --- a/src/components/activity/ActivityBanner.tsx +++ b/src/components/activity/ActivityBanner.tsx @@ -13,9 +13,6 @@ interface DailyActivity { interface ActivitySummary { days: DailyActivity[]; today: string; - activeDays: number; - flashcards: number; - questions: number; currentStreak: number; } @@ -65,11 +62,6 @@ export function ActivityBanner({ onNewClass }: { onNewClass: () => void }) {
-
- - - -
@@ -182,10 +174,6 @@ function StreakDisplay({ streak }: { streak: number }) { ); } -function SummaryStat({ value, label }: { value: number; label: string }) { - return
{value}
{label}
; -} - function getMonthLabel(week: DailyActivity[], previousWeek?: DailyActivity[]) { const firstOfMonth = week.find((day) => Number(day.date.slice(8, 10)) <= 7); if (!firstOfMonth) return ""; From 6d1d9183553e06a216418011fe238f522e887f28 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 13 Jul 2026 06:32:54 -0700 Subject: [PATCH 06/11] Improve responsive layout for study and quiz cards --- .../[classSlug]/flashcards/page.tsx | 44 +++++++------ .../(protected)/[classSlug]/quizzes/page.tsx | 63 ++++++++++++++----- src/components/quizzes/QuizViewer.tsx | 12 ++-- 3 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/app/(protected)/[classSlug]/flashcards/page.tsx b/src/app/(protected)/[classSlug]/flashcards/page.tsx index 21f2fdb..5bd7ab7 100644 --- a/src/app/(protected)/[classSlug]/flashcards/page.tsx +++ b/src/app/(protected)/[classSlug]/flashcards/page.tsx @@ -99,9 +99,10 @@ function SortableDeckCard({ deck, onEdit, onDelete, classSlug }: any) { )}
-
- {deck.progress?.length ? ( - <> +
+
+ {deck.progress?.length ? ( + <> Continue @@ -119,23 +120,26 @@ function SortableDeckCard({ deck, onEdit, onDelete, classSlug }: any) { > Restart - - ) : ( - - Study - - )} - - - + + ) : ( + + Study + + )} +
+
+ + + +
diff --git a/src/app/(protected)/[classSlug]/quizzes/page.tsx b/src/app/(protected)/[classSlug]/quizzes/page.tsx index d007514..7a09211 100644 --- a/src/app/(protected)/[classSlug]/quizzes/page.tsx +++ b/src/app/(protected)/[classSlug]/quizzes/page.tsx @@ -35,6 +35,12 @@ interface QuizItem { groupId: string | null; sortOrder: number; _count: { questions: number; attempts: number }; + progress: Array<{ + mode: string; + currentIndex: number; + orderJson: string; + answersJson: string | null; + }>; } interface MaterialGroup { @@ -79,21 +85,48 @@ function SortableQuizCard({ quiz, onEdit, onDelete, classSlug }: any) { )}
-
- - Start Quiz - - - - +
+
+ {quiz.progress?.length ? ( + <> + + Continue + + + + ) : ( + + Start Quiz + + )} +
+
+ + + +
diff --git a/src/components/quizzes/QuizViewer.tsx b/src/components/quizzes/QuizViewer.tsx index ac2bd97..d269ec4 100644 --- a/src/components/quizzes/QuizViewer.tsx +++ b/src/components/quizzes/QuizViewer.tsx @@ -350,11 +350,11 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
{/* Tags */} -
- +
+ {currentQ.category} - + {currentQ.type === "MULTIPLE_CHOICE" ? "Multiple Choice" : "Select All That Apply"}
@@ -382,7 +382,7 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu } else if (!isSelected && opt.isCorrect) { stateClass = "border-success/50 bg-success/5 text-text-heading"; rightIcon = ( -
+
@@ -400,7 +400,7 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
toggleOption(opt.id)} - className={`flex min-h-14 items-center gap-4 rounded-2xl border px-4 py-4 transition-all duration-200 md:px-5 ${stateClass}`} + className={`grid min-h-14 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-4 gap-y-2 rounded-2xl border px-4 py-4 transition-all duration-200 md:flex md:gap-4 md:px-5 ${stateClass}`} >
{rightIcon && ( -
+
{rightIcon}
)} From d6f3502cb1013849a9dbaa6e17439fb6a69032dd Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 13 Jul 2026 06:55:17 -0700 Subject: [PATCH 07/11] Tighten typing and input validation across study pages --- .../[classSlug]/flashcards/page.tsx | 14 ++-- .../(protected)/[classSlug]/quizzes/page.tsx | 14 ++-- src/app/api/decks/reorder/route.ts | 11 ++-- src/app/api/material-groups/[id]/route.ts | 3 +- src/app/api/material-groups/route.ts | 3 +- src/app/api/quizzes/reorder/route.ts | 11 ++-- .../[type]/[token]/SharedGroupViewer.tsx | 7 +- .../[type]/[token]/SharedViewer.tsx | 15 +++-- .../[classSlug]/[type]/[token]/page.tsx | 17 +++-- src/components/import/CreateTab.tsx | 4 +- src/components/quizzes/AttemptHistory.tsx | 7 +- src/components/quizzes/CategoryBreakdown.tsx | 14 ++-- src/components/quizzes/QuizResults.tsx | 27 +++++--- src/components/quizzes/QuizViewer.tsx | 3 +- src/lib/validation/reorderSchemas.ts | 11 ++++ src/types/study.ts | 65 +++++++++++++++++++ 16 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 src/lib/validation/reorderSchemas.ts create mode 100644 src/types/study.ts diff --git a/src/app/(protected)/[classSlug]/flashcards/page.tsx b/src/app/(protected)/[classSlug]/flashcards/page.tsx index 5bd7ab7..8a455f1 100644 --- a/src/app/(protected)/[classSlug]/flashcards/page.tsx +++ b/src/app/(protected)/[classSlug]/flashcards/page.tsx @@ -27,6 +27,7 @@ import { useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import type { ReorderItem } from "@/types/study"; interface DeckItem { id: string; @@ -65,7 +66,14 @@ function getProgressLabel(deck: DeckItem) { return `${current}/${total}, ${modeLabel} · ${correctCount} ✓`; } -function SortableDeckCard({ deck, onEdit, onDelete, classSlug }: any) { +interface SortableDeckCardProps { + deck: DeckItem; + onEdit: (deck: DeckItem) => void; + onDelete: (deck: DeckItem) => void; + classSlug: string; +} + +function SortableDeckCard({ deck, onEdit, onDelete, classSlug }: SortableDeckCardProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: deck.id }); const style = { transform: CSS.Transform.toString(transform), @@ -314,8 +322,6 @@ export default function FlashcardsPage() { if (!activeDeck) return; let targetGroupId: string | null = null; - let targetIndex = 0; - const overContainerId = over.data.current?.sortable?.containerId; if (overContainerId) { targetGroupId = overContainerId === "uncategorized" ? null : overContainerId; @@ -344,7 +350,7 @@ export default function FlashcardsPage() { } const affectedGroups = new Set([activeDeck.groupId, targetGroupId]); - const updates: any[] = []; + const updates: ReorderItem[] = []; affectedGroups.forEach(gId => { const gItems = newItems.filter(d => d.groupId === gId); diff --git a/src/app/(protected)/[classSlug]/quizzes/page.tsx b/src/app/(protected)/[classSlug]/quizzes/page.tsx index 7a09211..97be050 100644 --- a/src/app/(protected)/[classSlug]/quizzes/page.tsx +++ b/src/app/(protected)/[classSlug]/quizzes/page.tsx @@ -27,6 +27,7 @@ import { useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import type { ReorderItem } from "@/types/study"; interface QuizItem { id: string; @@ -51,7 +52,14 @@ interface MaterialGroup { const cache: Record = {}; -function SortableQuizCard({ quiz, onEdit, onDelete, classSlug }: any) { +interface SortableQuizCardProps { + quiz: QuizItem; + onEdit: (quiz: QuizItem) => void; + onDelete: (quiz: QuizItem) => void; + classSlug: string; +} + +function SortableQuizCard({ quiz, onEdit, onDelete, classSlug }: SortableQuizCardProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: quiz.id }); const style = { transform: CSS.Transform.toString(transform), @@ -312,8 +320,6 @@ export default function QuizzesPage() { if (!activeQuiz) return; let targetGroupId: string | null = null; - let targetIndex = 0; - // Check if over a group container directly const overContainerId = over.data.current?.sortable?.containerId; if (overContainerId) { @@ -345,7 +351,7 @@ export default function QuizzesPage() { // Re-calculate sortOrder for the affected groups to persist const affectedGroups = new Set([activeQuiz.groupId, targetGroupId]); - const updates: any[] = []; + const updates: ReorderItem[] = []; affectedGroups.forEach(gId => { const gItems = newItems.filter(q => q.groupId === gId); diff --git a/src/app/api/decks/reorder/route.ts b/src/app/api/decks/reorder/route.ts index 97313be..c7bcc43 100644 --- a/src/app/api/decks/reorder/route.ts +++ b/src/app/api/decks/reorder/route.ts @@ -1,19 +1,20 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; +import { reorderRequestSchema } from "@/lib/validation/reorderSchemas"; export async function PATCH(request: NextRequest) { try { - const body = await request.json(); - const { items } = body; - - if (!Array.isArray(items)) { + const parsed = reorderRequestSchema.safeParse(await request.json()); + if (!parsed.success) { return NextResponse.json({ error: "Invalid input" }, { status: 400 }); } + const { items } = parsed.data; + // items should be an array of { id, sortOrder, groupId } // Using a transaction to perform all updates await prisma.$transaction( - items.map((item: any) => + items.map((item) => prisma.deck.update({ where: { id: item.id }, data: { diff --git a/src/app/api/material-groups/[id]/route.ts b/src/app/api/material-groups/[id]/route.ts index f2e0316..bbd03cf 100644 --- a/src/app/api/material-groups/[id]/route.ts +++ b/src/app/api/material-groups/[id]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; +import type { Prisma } from "@/generated/prisma/client"; export async function PATCH( request: NextRequest, @@ -10,7 +11,7 @@ export async function PATCH( const body = await request.json(); const { name, sortOrder } = body; - const updateData: any = {}; + const updateData: Prisma.MaterialGroupUpdateInput = {}; if (name !== undefined) updateData.name = name; if (sortOrder !== undefined) updateData.sortOrder = sortOrder; diff --git a/src/app/api/material-groups/route.ts b/src/app/api/material-groups/route.ts index 974b8ee..6184603 100644 --- a/src/app/api/material-groups/route.ts +++ b/src/app/api/material-groups/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; +import type { Prisma } from "@/generated/prisma/client"; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); @@ -14,7 +15,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "Invalid type" }, { status: 400 }); } - const whereClause: any = { classId }; + const whereClause: Prisma.MaterialGroupWhereInput = { classId }; if (type) { whereClause.type = type; } diff --git a/src/app/api/quizzes/reorder/route.ts b/src/app/api/quizzes/reorder/route.ts index cc6d499..7ad7087 100644 --- a/src/app/api/quizzes/reorder/route.ts +++ b/src/app/api/quizzes/reorder/route.ts @@ -1,19 +1,20 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; +import { reorderRequestSchema } from "@/lib/validation/reorderSchemas"; export async function PATCH(request: NextRequest) { try { - const body = await request.json(); - const { items } = body; - - if (!Array.isArray(items)) { + const parsed = reorderRequestSchema.safeParse(await request.json()); + if (!parsed.success) { return NextResponse.json({ error: "Invalid input" }, { status: 400 }); } + const { items } = parsed.data; + // items should be an array of { id, sortOrder, groupId } // Using a transaction to perform all updates await prisma.$transaction( - items.map((item: any) => + items.map((item) => prisma.quizSet.update({ where: { id: item.id }, data: { diff --git a/src/app/shared/[classSlug]/[type]/[token]/SharedGroupViewer.tsx b/src/app/shared/[classSlug]/[type]/[token]/SharedGroupViewer.tsx index a49d137..5381446 100644 --- a/src/app/shared/[classSlug]/[type]/[token]/SharedGroupViewer.tsx +++ b/src/app/shared/[classSlug]/[type]/[token]/SharedGroupViewer.tsx @@ -4,8 +4,9 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; +import type { SharedGroupData } from "@/types/study"; -export function SharedGroupViewer({ data }: { data: any }) { +export function SharedGroupViewer({ data }: { data: SharedGroupData }) { const pathname = usePathname(); const router = useRouter(); @@ -18,7 +19,7 @@ export function SharedGroupViewer({ data }: { data: any }) { if (typeof window === 'undefined') return; const newProgress: Record = {}; - items.forEach((item: any) => { + items.forEach((item) => { const key = data.type === "DECK" ? `flashcard_progress_${item.id}` : `quiz_progress_${item.id}`; const saved = localStorage.getItem(key); if (saved) { @@ -71,7 +72,7 @@ export function SharedGroupViewer({ data }: { data: any }) {
- {items.map((item: any) => ( + {items.map((item) => (
q.category.toUpperCase()))).join(" · ") + ? Array.from(new Set(data.questions.map((q) => q.category.toUpperCase()))).join(" · ") : ""; return ( @@ -137,18 +138,20 @@ export function SharedViewer({ type, data, groupMode = false }: SharedViewerProp view === "study" ? ( ) : ( - + ) ) : ( d.id === itemId); + targetData = link.group.decks.find(d => d.id === itemId) ?? null; targetType = "flashcards"; } else { - targetData = link.group.quizSets.find(q => q.id === itemId); + targetData = link.group.quizSets.find(q => q.id === itemId) ?? null; targetType = "quizzes"; } if (!targetData) notFound(); @@ -69,6 +72,8 @@ export default async function SharedPage(props: SharedPageProps & { searchParams targetData = type === "flashcards" ? link.deck : link.quizSet; } + if (!targetData) notFound(); + return (
{/* Read-only Header */} @@ -105,11 +110,11 @@ export default async function SharedPage(props: SharedPageProps & { searchParams
{type === "groups" && !itemId ? ( - + ) : ( )} diff --git a/src/components/import/CreateTab.tsx b/src/components/import/CreateTab.tsx index 37fd693..6867575 100644 --- a/src/components/import/CreateTab.tsx +++ b/src/components/import/CreateTab.tsx @@ -74,8 +74,8 @@ export function CreateTab({ classId, onCreated }: CreateTabProps) { } onCreated(); - } catch (err: any) { - setError(err.message); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to create deck"); } finally { setSaving(false); } diff --git a/src/components/quizzes/AttemptHistory.tsx b/src/components/quizzes/AttemptHistory.tsx index a0b7892..6ee69d3 100644 --- a/src/components/quizzes/AttemptHistory.tsx +++ b/src/components/quizzes/AttemptHistory.tsx @@ -2,15 +2,16 @@ import { useState } from "react"; import { QuizResults } from "./QuizResults"; +import type { QuizAttempt, QuizSummary } from "@/types/study"; interface AttemptHistoryProps { - quiz: any; - attempts: any[]; + quiz: QuizSummary; + attempts: QuizAttempt[]; onRetake: (missedIds: string[]) => void; } export function AttemptHistory({ quiz, attempts, onRetake }: AttemptHistoryProps) { - const [selectedAttempt, setSelectedAttempt] = useState(null); + const [selectedAttempt, setSelectedAttempt] = useState(null); if (selectedAttempt) { return ( diff --git a/src/components/quizzes/CategoryBreakdown.tsx b/src/components/quizzes/CategoryBreakdown.tsx index 6f16b22..e8956a9 100644 --- a/src/components/quizzes/CategoryBreakdown.tsx +++ b/src/components/quizzes/CategoryBreakdown.tsx @@ -2,20 +2,20 @@ import { Fragment } from "react"; import { scoreQuestion } from "@/lib/scoring"; +import type { QuizSummary } from "@/types/study"; interface CategoryBreakdownProps { - quiz: any; - attempt: any; + quiz: QuizSummary; answeredIds: string[]; answers: Record; } -export function CategoryBreakdown({ quiz, attempt, answeredIds, answers }: CategoryBreakdownProps) { +export function CategoryBreakdown({ quiz, answeredIds, answers }: CategoryBreakdownProps) { // Aggregate stats per category const stats: Record = {}; answeredIds.forEach((qId) => { - const q = quiz.questions.find((x: any) => x.id === qId); + const q = quiz.questions.find((x) => x.id === qId); if (!q) return; const cat = q.category.trim().toLowerCase(); @@ -23,7 +23,11 @@ export function CategoryBreakdown({ quiz, attempt, answeredIds, answers }: Categ stats[cat] = { earned: 0, possible: 0 }; } - const formattedQ = { id: q.id, type: q.type, options: q.options }; + const formattedQ = { + id: q.id, + type: q.type as "MULTIPLE_CHOICE" | "SATA", + options: q.options, + }; const points = scoreQuestion(formattedQ, answers[q.id] || []); stats[cat].earned += points; diff --git a/src/components/quizzes/QuizResults.tsx b/src/components/quizzes/QuizResults.tsx index a66e7e8..95f0baa 100644 --- a/src/components/quizzes/QuizResults.tsx +++ b/src/components/quizzes/QuizResults.tsx @@ -4,14 +4,21 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { CategoryBreakdown } from "./CategoryBreakdown"; import { scoreQuestion } from "@/lib/scoring"; +import type { QuizAttempt, QuizQuestion, QuizSummary } from "@/types/study"; interface QuizResultsProps { - quiz: any; // QuizData - attempt: any; // QuizAttempt + quiz: QuizSummary; + attempt: QuizAttempt; onRetake: (missedIds: string[]) => void; onClose?: () => void; } +interface ReviewItem { + question: QuizQuestion; + score: number; + selections: string[]; +} + export function QuizResults({ quiz, attempt, onRetake, onClose }: QuizResultsProps) { let answers: Record = {}; try { @@ -19,16 +26,16 @@ export function QuizResults({ quiz, attempt, onRetake, onClose }: QuizResultsPro } catch {} // Determine non-full-credit questions for review list - const reviewItems: any[] = []; + const reviewItems: ReviewItem[] = []; const missedIds: string[] = []; const answeredIds = Object.keys(answers); - const questionsToReview = quiz.questions.filter((q: any) => answeredIds.includes(q.id)); + const questionsToReview = quiz.questions.filter((q) => answeredIds.includes(q.id)); - questionsToReview.forEach((q: any) => { + questionsToReview.forEach((q) => { const formattedQ = { id: q.id, - type: q.type, + type: q.type as "MULTIPLE_CHOICE" | "SATA", options: q.options, }; const score = scoreQuestion(formattedQ, answers[q.id] || []); @@ -92,7 +99,7 @@ export function QuizResults({ quiz, attempt, onRetake, onClose }: QuizResultsPro +
+

{deck.name}

+

{deck._count.cards} cards

+
+
+ ); +} + +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 ( +
+ +
+

{deck.name}

+

{deck._count.cards} cards

+
+ +
+ ); +} + +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 ( +
+ {children} +
+ ); +} + +export function SpacedRepetitionSets() { + const { classSlug } = useParams<{ classSlug: string }>(); + const [classId, setClassId] = useState(""); + const [sets, setSets] = useState([]); + const [decks, setDecks] = useState([]); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const [newName, setNewName] = useState(""); + const [editing, setEditing] = useState(null); + const [message, setMessage] = useState(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
Loading repetition sets…
; + + return ( +
+
+

Adaptive review

Spaced repetition

+ +
+ + {message &&
{message}
} + + {creating && ( +
+ 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" /> + + +
+ )} + + +
+ {sets.length === 0 &&

No repetition sets yet

Create one, then drag flashcard decks into it.

} + {sets.map((set) => ( +
+
+
+

{set.name}

+ {set.description &&

{set.description}

} +
+ {set.stats.totalCards} cards + {set.stats.dueCards} due + {set.stats.learningAheadCards > 0 && {set.stats.learningAheadCards} learning} + {set.stats.newCardsAvailable} new today + Limit {set.newCardsPerDay}/day +
+
+
+ {set.stats.totalCards === 0 ? : set.stats.availableCards > 0 ? ( + {set.stats.reviewedCards === 0 ? "Study" : "Continue"} · {set.stats.availableCards} + ) : Caught up} + + +
+
+ `set:${set.id}:deck:${deck.id}`)} strategy={verticalListSortingStrategy}> + + {set.decks.length === 0 ?

Drop flashcard decks here

: set.decks.map((deck) => removeDeck(set, deck)} />)} +
+
+
+ ))} + +
+

Source library

+

Flashcard decks

+

Drag a deck into any repetition set. The original deck stays here and can be reused.

+ {decks.length ?
{decks.map((deck) => )}
:
Create flashcard decks before building a repetition set.
} +
+
+
+ + {editing && ( +
+
+

Edit repetition set

+ + 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" /> + +