Implement local browser caching for shared groups and UI improvements
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s

This commit is contained in:
Elijah 2026-07-10 16:01:22 -07:00
parent 42ed0d275b
commit 7af0935657
37 changed files with 6141 additions and 411 deletions

BIN
dev.db

Binary file not shown.

1
out.css Normal file
View file

@ -0,0 +1 @@
:root { --color-primary: #fff; }

57
package-lock.json generated
View file

@ -8,6 +8,9 @@
"name": "study-app",
"version": "0.1.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@prisma/adapter-better-sqlite3": "^7.8.0",
"@prisma/client": "^7.8.0",
"argon2": "^0.44.0",
@ -287,6 +290,60 @@
"node": ">=6.9.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@electric-sql/pglite": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",

View file

@ -9,6 +9,9 @@
"lint": "eslint"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@prisma/adapter-better-sqlite3": "^7.8.0",
"@prisma/client": "^7.8.0",
"argon2": "^0.44.0",

View file

@ -14,8 +14,9 @@ model Class {
sortOrder Int @default(0)
createdAt DateTime @default(now())
decks Deck[]
quizSets QuizSet[]
decks Deck[]
quizSets QuizSet[]
materialGroups MaterialGroup[]
}
model Deck {
@ -27,6 +28,8 @@ model Deck {
createdAt DateTime @default(now())
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
groupId String?
group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
cards Flashcard[]
shareLink ShareLink?
progress StudyProgress[]
@ -51,6 +54,8 @@ model QuizSet {
createdAt DateTime @default(now())
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
groupId String?
group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
questions Question[]
attempts QuizAttempt[]
shareLink ShareLink?
@ -113,13 +118,15 @@ model QuizAttempt {
model ShareLink {
id String @id @default(uuid())
targetType String // "DECK" | "QUIZ"
targetType String // "DECK" | "QUIZ" | "GROUP"
deckId String? @unique
quizSetId String? @unique
groupId String? @unique
createdAt DateTime @default(now())
deck Deck? @relation(fields: [deckId], references: [id], onDelete: Cascade)
quizSet QuizSet? @relation(fields: [quizSetId], references: [id], onDelete: Cascade)
deck Deck? @relation(fields: [deckId], references: [id], onDelete: Cascade)
quizSet QuizSet? @relation(fields: [quizSetId], references: [id], onDelete: Cascade)
group MaterialGroup? @relation(fields: [groupId], references: [id], onDelete: Cascade)
}
model AuthSecurity {
@ -133,3 +140,17 @@ model Setting {
key String @id
value String
}
model MaterialGroup {
id String @id @default(uuid())
classId String
name String
type String // "DECK" | "QUIZ"
sortOrder Int @default(0)
createdAt DateTime @default(now())
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
decks Deck[]
quizSets QuizSet[]
shareLink ShareLink?
}

View file

@ -4,11 +4,36 @@ import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { ImportModal } from "@/components/import/ImportModal";
import { ShareMenu } from "@/components/ui/ShareMenu";
import {
DndContext,
pointerWithin,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
DragOverEvent,
DragStartEvent,
DragOverlay,
useDroppable,
useDndContext,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface DeckItem {
id: string;
name: string;
description: string | null;
groupId: string | null;
sortOrder: number;
_count: { cards: number };
progress: Array<{
mode: string;
@ -18,12 +43,126 @@ interface DeckItem {
}>;
}
const cache: Record<string, DeckItem[]> = {};
interface MaterialGroup {
id: string;
name: string;
sortOrder: number;
}
const cache: Record<string, { decks: DeckItem[]; groups: MaterialGroup[] }> = {};
function getProgressLabel(deck: DeckItem) {
if (!deck.progress?.length) return null;
const prog = deck.progress[0];
const order = JSON.parse(prog.orderJson) as string[];
const results = prog.cardResultsJson
? (JSON.parse(prog.cardResultsJson) as Record<string, string>)
: {};
const correctCount = Object.values(results).filter((r) => r === "correct").length;
const total = order.length;
const current = Math.min(prog.currentIndex + 1, total);
const modeLabel = prog.mode === "SHUFFLED" ? "shuffled" : "sequential";
return `${current}/${total}, ${modeLabel} · ${correctCount}`;
}
function SortableDeckCard({ deck, onEdit, onDelete, classSlug }: any) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: deck.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.4 : 1,
};
const progressLabel = getProgressLabel(deck);
return (
<div ref={setNodeRef} style={style} className="group flex flex-col bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300 h-full">
<div className="p-4 flex flex-col flex-1 h-full">
<div className="flex items-start gap-2">
<div {...attributes} {...listeners} className="mt-1 cursor-grab active:cursor-grabbing text-text-muted hover:text-text-heading">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8h16M4 16h16" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-text-heading mb-1">{deck.name}</h3>
{deck.description && <p className="text-sm text-text-secondary mb-3 line-clamp-2">{deck.description}</p>}
</div>
</div>
<div className="flex items-center gap-3 mt-auto mb-4 pl-7">
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium">
{deck._count.cards} {deck._count.cards === 1 ? "card" : "cards"}
</span>
{progressLabel && (
<span className="text-xs text-text-muted">{progressLabel}</span>
)}
</div>
<div className="flex items-center gap-2 pt-4 border-t border-border-light pl-7">
{deck.progress?.length ? (
<>
<Link href={`/${classSlug}/flashcards/${deck.id}`} className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200">
Continue
</Link>
<button
onClick={async () => {
if (!confirm("Start over from the beginning? This will clear your current progress for this deck.")) return;
await Promise.all([
fetch("/api/progress", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contentType: "DECK", contentId: deck.id, mode: "SEQUENTIAL" }) }).catch(() => {}),
fetch("/api/progress", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contentType: "DECK", contentId: deck.id, mode: "SHUFFLED" }) }).catch(() => {})
]);
window.location.href = `/${classSlug}/flashcards/${deck.id}`;
}}
className="p-2 rounded-lg text-text-muted border border-border-light hover:bg-bg-surface-alt hover:text-text-heading transition-all duration-200 cursor-pointer"
title="Start New"
>
<svg className="w-5 h-5 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</>
) : (
<Link href={`/${classSlug}/flashcards/${deck.id}`} className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200">
Study
</Link>
)}
<button onClick={() => onEdit(deck)} className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer" title="Edit">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button onClick={() => onDelete(deck)} className="p-2 rounded-lg text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer" title="Delete">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
</div>
);
}
function DroppableContainer({ id, children }: { id: string; children: React.ReactNode }) {
const { setNodeRef } = useDroppable({ id });
const { over } = useDndContext();
const isOverContainer = over?.id === id;
const isOverChild = over?.data?.current?.sortable?.containerId === id;
const isOver = isOverContainer || isOverChild;
return (
<div ref={setNodeRef} className={`grid grid-cols-1 md:grid-cols-2 gap-4 min-h-[60px] p-2 -mx-2 rounded-lg border-2 transition-colors duration-200 ${isOver ? "border-primary/40 bg-primary/5 shadow-inner" : "border-transparent"}`}>
{children}
</div>
);
}
export default function FlashcardsPage() {
const params = useParams();
const classSlug = params.classSlug as string;
const [decks, setDecks] = useState<DeckItem[]>(cache[classSlug] || []);
const [decks, setDecks] = useState<DeckItem[]>(cache[classSlug]?.decks || []);
const [groups, setGroups] = useState<MaterialGroup[]>(cache[classSlug]?.groups || []);
const [loading, setLoading] = useState(!cache[classSlug]);
const [animate] = useState(!cache[classSlug]);
const [showImport, setShowImport] = useState(false);
@ -31,22 +170,29 @@ export default function FlashcardsPage() {
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState("");
const [editDescription, setEditDescription] = useState("");
const [isCreatingGroup, setIsCreatingGroup] = useState(false);
const [newGroupName, setNewGroupName] = useState("");
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
const [editGroupName, setEditGroupName] = useState("");
const fetchDecks = useCallback(async (cId: string) => {
const fetchAll = useCallback(async (cId: string) => {
try {
const res = await fetch(`/api/decks/list?classId=${cId}`);
if (res.ok) {
const data = await res.json();
const arr = Array.isArray(data) ? data : [];
setDecks(arr);
cache[classSlug] = arr;
}
const [deckRes, groupRes] = await Promise.all([
fetch(`/api/decks/list?classId=${cId}`),
fetch(`/api/material-groups?classId=${cId}&type=DECK`),
]);
const ds = deckRes.ok ? await deckRes.json() : [];
const gs = groupRes.ok ? await groupRes.json() : [];
setDecks(ds);
setGroups(gs);
cache[classSlug] = { decks: ds, groups: gs };
} catch {
setDecks([]);
setGroups([]);
} finally {
setLoading(false);
}
}, []);
}, [classSlug]);
useEffect(() => {
async function init() {
@ -59,21 +205,56 @@ export default function FlashcardsPage() {
return;
}
setClassId(cls.id);
fetchDecks(cls.id);
fetchAll(cls.id);
} catch {
setLoading(false);
}
}
init();
}, [classSlug, fetchDecks]);
}, [classSlug, fetchAll]);
async function handleDelete(id: string, name: string) {
if (!confirm(`Delete "${name}" and all its cards?`)) return;
await fetch(`/api/decks/${id}`, { method: "DELETE" });
setDecks((prev) => prev.filter((d) => d.id !== id));
// Group Management
async function handleCreateGroup() {
if (!newGroupName.trim()) return;
const res = await fetch("/api/material-groups", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ classId, name: newGroupName.trim(), type: "DECK" }),
});
if (res.ok) {
const g = await res.json();
setGroups((prev) => [...prev, g]);
setIsCreatingGroup(false);
setNewGroupName("");
}
}
async function handleRename(id: string) {
async function handleRenameGroup(id: string) {
if (!editGroupName.trim()) return;
await fetch(`/api/material-groups/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: editGroupName.trim() }),
});
setGroups((prev) => prev.map((g) => (g.id === id ? { ...g, name: editGroupName.trim() } : g)));
setEditingGroupId(null);
}
async function handleDeleteGroup(id: string, name: string) {
if (!confirm(`Delete group "${name}"? Decks inside will be moved to Uncategorized.`)) return;
await fetch(`/api/material-groups/${id}`, { method: "DELETE" });
setGroups((prev) => prev.filter((g) => g.id !== id));
setDecks((prev) => prev.map((d) => (d.groupId === id ? { ...d, groupId: null } : d)));
}
// Deck Management
async function handleDeleteDeck(deck: DeckItem) {
if (!confirm(`Delete "${deck.name}" and all its cards?`)) return;
await fetch(`/api/decks/${deck.id}`, { method: "DELETE" });
setDecks((prev) => prev.filter((d) => d.id !== deck.id));
}
async function handleRenameDeck(id: string) {
if (!editName.trim()) return;
await fetch(`/api/decks/${id}`, {
method: "PATCH",
@ -86,201 +267,230 @@ export default function FlashcardsPage() {
setEditingId(null);
}
function getProgressLabel(deck: DeckItem) {
if (!deck.progress?.length) return null;
const prog = deck.progress[0];
const order = JSON.parse(prog.orderJson) as string[];
const results = prog.cardResultsJson
? (JSON.parse(prog.cardResultsJson) as Record<string, string>)
: {};
const correctCount = Object.values(results).filter((r) => r === "correct").length;
const total = order.length;
const current = Math.min(prog.currentIndex + 1, total);
const modeLabel = prog.mode === "SHUFFLED" ? "shuffled" : "sequential";
return `${current}/${total}, ${modeLabel} · ${correctCount}`;
// Drag and drop setup
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
const [activeId, setActiveId] = useState<string | null>(null);
function handleDragStart(event: DragStartEvent) {
setActiveId(event.active.id as string);
}
function handleDragOver(event: DragOverEvent) {
// optional layout updates here
}
async function handleDragEnd(event: DragEndEvent) {
setActiveId(null);
const { active, over } = event;
if (!over) return;
const activeId = active.id as string;
const overId = over.id as string;
const activeDeck = decks.find((d) => d.id === activeId);
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;
} else if (groups.find(g => g.id === overId) || overId === "uncategorized") {
targetGroupId = overId === "uncategorized" ? null : overId;
} else {
const overDeck = decks.find((d) => d.id === overId);
if (overDeck) {
targetGroupId = overDeck.groupId;
}
}
if (targetGroupId !== undefined) {
setDecks((items) => {
const oldIndex = items.findIndex((d) => d.id === activeId);
const overIndex = items.findIndex((d) => d.id === overId);
let newItems = [...items];
newItems[oldIndex].groupId = targetGroupId;
if (overIndex >= 0 && overIndex !== oldIndex) {
newItems = arrayMove(newItems, oldIndex, overIndex);
} else {
const oldItem = newItems.splice(oldIndex, 1)[0];
newItems.push(oldItem);
}
const affectedGroups = new Set([activeDeck.groupId, targetGroupId]);
const updates: any[] = [];
affectedGroups.forEach(gId => {
const gItems = newItems.filter(d => d.groupId === gId);
gItems.forEach((item, index) => {
item.sortOrder = index;
updates.push({ id: item.id, sortOrder: index, groupId: item.groupId });
});
});
fetch("/api/decks/reorder", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: updates }),
});
return newItems;
});
}
}
const groupedDecks = groups.map(g => ({
...g,
decks: decks.filter(d => d.groupId === g.id).sort((a,b) => a.sortOrder - b.sortOrder)
}));
const uncategorizedDecks = decks.filter(d => d.groupId === null).sort((a,b) => a.sortOrder - b.sortOrder);
const activeDeck = decks.find(d => d.id === activeId);
return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-text-heading">Flashcard Decks</h2>
<button
onClick={() => setShowImport(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 shadow-sm cursor-pointer"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Import Deck
</button>
<div className="flex items-center gap-3">
<button onClick={() => setIsCreatingGroup(true)} className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-bg-surface-alt text-text-heading font-medium border border-border-light hover:bg-border transition-all duration-200 shadow-sm cursor-pointer">
Add Group
</button>
<button onClick={() => setShowImport(true)} className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 shadow-sm cursor-pointer">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Import Deck
</button>
</div>
</div>
{/* Empty state */}
{!loading && decks.length === 0 && (
<div className={`text-center py-16 ${animate ? "animate-slide-up" : ""}`}>
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-bg-callout mb-4">
<svg className="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<h3 className="text-lg font-semibold text-text-heading mb-1">No flashcard decks yet</h3>
<p className="text-text-secondary mb-4">Import your first deck to start studying</p>
<button
onClick={() => setShowImport(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 cursor-pointer"
>
Import Deck
{isCreatingGroup && (
<div className="mb-6 p-4 bg-bg-surface rounded-xl border border-primary/20 shadow-sm flex items-center gap-3">
<input
autoFocus
type="text"
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
placeholder="e.g. Exam 1 Flashcards"
className="flex-1 px-3 py-2 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<button onClick={handleCreateGroup} className="px-4 py-2 bg-primary text-white font-medium rounded-md hover:bg-primary-hover cursor-pointer">
Save
</button>
<button onClick={() => { setIsCreatingGroup(false); setNewGroupName(""); }} className="px-4 py-2 bg-bg-surface-alt text-text-heading font-medium rounded-md hover:bg-border cursor-pointer">
Cancel
</button>
</div>
)}
{/* Deck grid */}
{!loading && decks.length > 0 && (
<div className={`grid grid-cols-1 md:grid-cols-2 gap-4 ${animate ? "animate-slide-up" : ""}`}>
{decks.map((deck) => {
const progressLabel = getProgressLabel(deck);
return (
<div
key={deck.id}
className="group flex flex-col bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300 h-full"
>
<div className="p-6 flex flex-col flex-1 h-full">
{/* Name and Description (editable) */}
{editingId === deck.id ? (
<div className="flex flex-col gap-3 mb-3">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="Deck Name"
autoFocus
className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
placeholder="Description (optional)"
rows={2}
className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
<div className="flex gap-2 mt-1">
<button
onClick={() => handleRename(deck.id)}
className="px-3 py-1.5 bg-primary text-white text-xs font-medium rounded-md hover:bg-primary-hover cursor-pointer"
>
Save Changes
</button>
<button
onClick={() => setEditingId(null)}
className="px-3 py-1.5 bg-bg-surface-alt text-text-heading text-xs font-medium rounded-md hover:bg-border cursor-pointer transition-colors"
>
Cancel
</button>
</div>
</div>
) : (
<>
<h3 className="text-lg font-semibold text-text-heading mb-1">
{deck.name}
</h3>
{deck.description && (
<p className="text-sm text-text-secondary mb-3 line-clamp-2">{deck.description}</p>
)}
</>
)}
{/* Stats */}
<div className="flex items-center gap-3 mt-auto mb-4">
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium">
{deck._count.cards} {deck._count.cards === 1 ? "card" : "cards"}
</span>
{progressLabel && (
<span className="text-xs text-text-muted">{progressLabel}</span>
)}
</div>
{/* Actions */}
<div className="flex items-center gap-2 pt-4 border-t border-border-light">
{deck.progress?.length ? (
<>
<Link
href={`/${classSlug}/flashcards/${deck.id}`}
className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200"
>
Continue
</Link>
<button
onClick={async () => {
if (!confirm("Start over from the beginning? This will clear your current progress for this deck.")) return;
await Promise.all([
fetch("/api/progress", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contentType: "DECK", contentId: deck.id, mode: "SEQUENTIAL" }) }).catch(() => {}),
fetch("/api/progress", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contentType: "DECK", contentId: deck.id, mode: "SHUFFLED" }) }).catch(() => {})
]);
window.location.href = `/${classSlug}/flashcards/${deck.id}`;
}}
className="p-2 rounded-lg text-text-muted border border-border-light hover:bg-bg-surface-alt hover:text-text-heading transition-all duration-200 cursor-pointer"
title="Start New"
>
<svg className="w-5 h-5 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</>
) : (
<Link
href={`/${classSlug}/flashcards/${deck.id}`}
className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200"
>
Study
</Link>
)}
<button
onClick={() => {
setEditingId(deck.id);
setEditName(deck.name);
setEditDescription(deck.description || "");
}}
className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer"
title="Edit"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDelete(deck.id, deck.name)}
className="p-2 rounded-lg text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer"
title="Delete"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
</div>
);
})}
{editingId && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
<div className="bg-bg-surface rounded-xl p-6 w-full max-w-md shadow-lg border border-border">
<h3 className="text-lg font-semibold text-text-heading mb-4">Edit Deck</h3>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="Deck Name"
autoFocus
className="w-full px-3 py-2 mb-3 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
placeholder="Description (optional)"
rows={3}
className="w-full px-3 py-2 mb-4 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
<div className="flex gap-2 justify-end">
<button onClick={() => setEditingId(null)} className="px-4 py-2 bg-bg-surface-alt text-text-heading font-medium rounded-md hover:bg-border cursor-pointer">Cancel</button>
<button onClick={() => handleRenameDeck(editingId)} className="px-4 py-2 bg-primary text-white font-medium rounded-md hover:bg-primary-hover cursor-pointer">Save Changes</button>
</div>
</div>
</div>
)}
{/* Import Modal */}
{!loading && decks.length === 0 && groups.length === 0 && (
<div className="text-center py-16">
<h3 className="text-lg font-semibold text-text-heading mb-1">No decks yet</h3>
<p className="text-text-secondary mb-4">Import your first deck or create a group</p>
</div>
)}
{!loading && (
<DndContext sensors={sensors} collisionDetection={pointerWithin} onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd}>
<div className="space-y-8">
{groupedDecks.map(group => (
<div key={group.id} className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light">
<div className="flex items-center justify-between mb-4">
{editingGroupId === group.id ? (
<div className="flex items-center gap-2">
<input autoFocus value={editGroupName} onChange={e => setEditGroupName(e.target.value)} className="px-2 py-1 rounded border border-border bg-bg-surface text-text-heading focus:ring-1 focus:ring-primary" />
<button onClick={() => handleRenameGroup(group.id)} className="text-xs px-2 py-1 bg-primary text-white rounded">Save</button>
<button onClick={() => setEditingGroupId(null)} className="text-xs px-2 py-1 bg-bg-surface text-text-heading rounded border border-border-light">Cancel</button>
</div>
) : (
<h3 className="text-lg font-semibold text-text-heading">{group.name}</h3>
)}
<div className="flex items-center gap-2">
<ShareMenu targetType="GROUP" contentId={group.id} classSlug={params.classSlug as string} />
<button onClick={() => { setEditingGroupId(group.id); setEditGroupName(group.name); }} className="text-text-muted hover:text-text-heading"><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg></button>
<button onClick={() => handleDeleteGroup(group.id, group.name)} className="text-text-muted hover:text-error"><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg></button>
</div>
</div>
<SortableContext id={group.id} items={group.decks.map(d => d.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id={group.id}>
{group.decks.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">Drop decks here</div>
) : (
group.decks.map(deck => (
<SortableDeckCard key={deck.id} deck={deck} onEdit={(d: DeckItem) => { setEditingId(d.id); setEditName(d.name); setEditDescription(d.description || ""); }} onDelete={handleDeleteDeck} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
</div>
))}
{/* Uncategorized */}
<div className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light">
<h3 className="text-lg font-semibold text-text-heading mb-4">Uncategorized</h3>
<SortableContext id="uncategorized" items={uncategorizedDecks.map(d => d.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id="uncategorized">
{uncategorizedDecks.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">No uncategorized decks</div>
) : (
uncategorizedDecks.map(deck => (
<SortableDeckCard key={deck.id} deck={deck} onEdit={(d: DeckItem) => { setEditingId(d.id); setEditName(d.name); setEditDescription(d.description || ""); }} onDelete={handleDeleteDeck} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
</div>
</div>
<DragOverlay>
{activeDeck ? (
<div className="opacity-80 scale-105 shadow-xl rotate-2">
<SortableDeckCard deck={activeDeck} onEdit={()=>{}} onDelete={()=>{}} classSlug={classSlug} />
</div>
) : null}
</DragOverlay>
</DndContext>
)}
{showImport && (
<ImportModal
classId={classId}
importType="flashcards"
onClose={() => setShowImport(false)}
onImported={() => {
setShowImport(false);
setLoading(true);
// Re-fetch
window.location.reload();
}}
/>
<ImportModal classId={classId} importType="flashcards" onClose={() => setShowImport(false)} onImported={() => { setShowImport(false); setLoading(true); window.location.reload(); }} />
)}
</div>
);

View file

@ -4,20 +4,121 @@ import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { ImportModal } from "@/components/import/ImportModal";
import { ShareMenu } from "@/components/ui/ShareMenu";
import {
DndContext,
pointerWithin,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
DragOverEvent,
DragStartEvent,
DragOverlay,
useDroppable,
useDndContext,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface QuizItem {
id: string;
name: string;
description: string | null;
groupId: string | null;
sortOrder: number;
_count: { questions: number; attempts: number };
}
const cache: Record<string, QuizItem[]> = {};
interface MaterialGroup {
id: string;
name: string;
sortOrder: number;
}
const cache: Record<string, { quizzes: QuizItem[]; groups: MaterialGroup[] }> = {};
function SortableQuizCard({ quiz, onEdit, onDelete, classSlug }: any) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: quiz.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.4 : 1,
};
return (
<div ref={setNodeRef} style={style} className="group bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300">
<div className="p-4 flex flex-col flex-1 h-full">
<div className="flex items-start gap-2">
<div {...attributes} {...listeners} className="mt-1 cursor-grab active:cursor-grabbing text-text-muted hover:text-text-heading">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8h16M4 16h16" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-text-heading mb-1">{quiz.name}</h3>
{quiz.description && <p className="text-sm text-text-secondary mb-3 line-clamp-2">{quiz.description}</p>}
</div>
</div>
<div className="flex items-center gap-3 mt-auto pl-7">
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium">
{quiz._count.questions} {quiz._count.questions === 1 ? "question" : "questions"}
</span>
{quiz._count.attempts > 0 && (
<span className="text-xs text-text-muted">
{quiz._count.attempts} {quiz._count.attempts === 1 ? "attempt" : "attempts"}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-border-light pl-7">
<Link href={`/${classSlug}/quizzes/${quiz.id}`} className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200">
Start Quiz
</Link>
<button onClick={() => onEdit(quiz)} className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer" title="Edit">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button onClick={() => onDelete(quiz)} className="p-2 rounded-lg text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer" title="Delete">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
</div>
);
}
function DroppableContainer({ id, children }: { id: string; children: React.ReactNode }) {
const { setNodeRef } = useDroppable({ id });
const { over } = useDndContext();
const isOverContainer = over?.id === id;
const isOverChild = over?.data?.current?.sortable?.containerId === id;
const isOver = isOverContainer || isOverChild;
return (
<div ref={setNodeRef} className={`grid grid-cols-1 md:grid-cols-2 gap-4 min-h-[60px] p-2 -mx-2 rounded-lg border-2 transition-colors duration-200 ${isOver ? "border-primary/40 bg-primary/5 shadow-inner" : "border-transparent"}`}>
{children}
</div>
);
}
export default function QuizzesPage() {
const params = useParams();
const classSlug = params.classSlug as string;
const [quizzes, setQuizzes] = useState<QuizItem[]>(cache[classSlug] || []);
const [quizzes, setQuizzes] = useState<QuizItem[]>(cache[classSlug]?.quizzes || []);
const [groups, setGroups] = useState<MaterialGroup[]>(cache[classSlug]?.groups || []);
const [loading, setLoading] = useState(!cache[classSlug]);
const [animate] = useState(!cache[classSlug]);
const [showImport, setShowImport] = useState(false);
@ -25,22 +126,29 @@ export default function QuizzesPage() {
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState("");
const [editDescription, setEditDescription] = useState("");
const [isCreatingGroup, setIsCreatingGroup] = useState(false);
const [newGroupName, setNewGroupName] = useState("");
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
const [editGroupName, setEditGroupName] = useState("");
const fetchQuizzes = useCallback(async (cId: string) => {
const fetchAll = useCallback(async (cId: string) => {
try {
const res = await fetch(`/api/quizzes/list?classId=${cId}`);
if (res.ok) {
const data = await res.json();
const arr = Array.isArray(data) ? data : [];
setQuizzes(arr);
cache[classSlug] = arr;
}
const [quizRes, groupRes] = await Promise.all([
fetch(`/api/quizzes/list?classId=${cId}`),
fetch(`/api/material-groups?classId=${cId}&type=QUIZ`),
]);
const qs = quizRes.ok ? await quizRes.json() : [];
const gs = groupRes.ok ? await groupRes.json() : [];
setQuizzes(qs);
setGroups(gs);
cache[classSlug] = { quizzes: qs, groups: gs };
} catch {
setQuizzes([]);
setGroups([]);
} finally {
setLoading(false);
}
}, []);
}, [classSlug]);
useEffect(() => {
async function init() {
@ -53,21 +161,56 @@ export default function QuizzesPage() {
return;
}
setClassId(cls.id);
fetchQuizzes(cls.id);
fetchAll(cls.id);
} catch {
setLoading(false);
}
}
init();
}, [classSlug, fetchQuizzes]);
}, [classSlug, fetchAll]);
async function handleDelete(id: string, name: string) {
if (!confirm(`Delete "${name}" and all its questions and attempts?`)) return;
await fetch(`/api/quizzes/${id}`, { method: "DELETE" });
setQuizzes((prev) => prev.filter((q) => q.id !== id));
// Group Management
async function handleCreateGroup() {
if (!newGroupName.trim()) return;
const res = await fetch("/api/material-groups", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ classId, name: newGroupName.trim(), type: "QUIZ" }),
});
if (res.ok) {
const g = await res.json();
setGroups((prev) => [...prev, g]);
setIsCreatingGroup(false);
setNewGroupName("");
}
}
async function handleRename(id: string) {
async function handleRenameGroup(id: string) {
if (!editGroupName.trim()) return;
await fetch(`/api/material-groups/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: editGroupName.trim() }),
});
setGroups((prev) => prev.map((g) => (g.id === id ? { ...g, name: editGroupName.trim() } : g)));
setEditingGroupId(null);
}
async function handleDeleteGroup(id: string, name: string) {
if (!confirm(`Delete group "${name}"? Quizzes inside will be moved to Uncategorized.`)) return;
await fetch(`/api/material-groups/${id}`, { method: "DELETE" });
setGroups((prev) => prev.filter((g) => g.id !== id));
setQuizzes((prev) => prev.map((q) => (q.groupId === id ? { ...q, groupId: null } : q)));
}
// Quiz Management
async function handleDeleteQuiz(quiz: QuizItem) {
if (!confirm(`Delete "${quiz.name}" and all its questions and attempts?`)) return;
await fetch(`/api/quizzes/${quiz.id}`, { method: "DELETE" });
setQuizzes((prev) => prev.filter((q) => q.id !== quiz.id));
}
async function handleRenameQuiz(id: string) {
if (!editName.trim()) return;
await fetch(`/api/quizzes/${id}`, {
method: "PATCH",
@ -80,155 +223,247 @@ export default function QuizzesPage() {
setEditingId(null);
}
// Drag and drop setup
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
const [activeId, setActiveId] = useState<string | null>(null);
function handleDragStart(event: DragStartEvent) {
setActiveId(event.active.id as string);
}
function handleDragOver(event: DragOverEvent) {
const { active, over } = event;
if (!over) return;
const activeId = active.id as string;
const overId = over.id as string;
if (activeId === overId) return;
// Is it dropping over a container directly?
const isOverContainer = over.data.current?.sortable?.containerId || overId;
// ... we handle layout updates in dragEnd to keep it simple, or here for smooth lists.
// For simplicity, we just handle the final move in dragEnd.
}
async function handleDragEnd(event: DragEndEvent) {
setActiveId(null);
const { active, over } = event;
if (!over) return;
const activeId = active.id as string;
const overId = over.id as string;
const activeQuiz = quizzes.find((q) => q.id === activeId);
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) {
targetGroupId = overContainerId === "uncategorized" ? null : overContainerId;
} else if (groups.find(g => g.id === overId) || overId === "uncategorized") {
targetGroupId = overId === "uncategorized" ? null : overId;
} else {
const overQuiz = quizzes.find((q) => q.id === overId);
if (overQuiz) {
targetGroupId = overQuiz.groupId;
}
}
if (targetGroupId !== undefined) {
setQuizzes((items) => {
const oldIndex = items.findIndex((q) => q.id === activeId);
const overIndex = items.findIndex((q) => q.id === overId);
let newItems = [...items];
newItems[oldIndex].groupId = targetGroupId;
if (overIndex >= 0 && overIndex !== oldIndex) {
newItems = arrayMove(newItems, oldIndex, overIndex);
} else {
// just moved to end of a group
const oldItem = newItems.splice(oldIndex, 1)[0];
newItems.push(oldItem);
}
// Re-calculate sortOrder for the affected groups to persist
const affectedGroups = new Set([activeQuiz.groupId, targetGroupId]);
const updates: any[] = [];
affectedGroups.forEach(gId => {
const gItems = newItems.filter(q => q.groupId === gId);
gItems.forEach((item, index) => {
item.sortOrder = index;
updates.push({ id: item.id, sortOrder: index, groupId: item.groupId });
});
});
// Fire API
fetch("/api/quizzes/reorder", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: updates }),
});
return newItems;
});
}
}
// Derive grouped quizzes
const groupedQuizzes = groups.map(g => ({
...g,
quizzes: quizzes.filter(q => q.groupId === g.id).sort((a,b) => a.sortOrder - b.sortOrder)
}));
const uncategorizedQuizzes = quizzes.filter(q => q.groupId === null).sort((a,b) => a.sortOrder - b.sortOrder);
const activeQuiz = quizzes.find(q => q.id === activeId);
return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-text-heading">Quizzes</h2>
<button
onClick={() => setShowImport(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 shadow-sm cursor-pointer"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Import Quiz
</button>
<div className="flex items-center gap-3">
<button onClick={() => setIsCreatingGroup(true)} className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-bg-surface-alt text-text-heading font-medium border border-border-light hover:bg-border transition-all duration-200 shadow-sm cursor-pointer">
Add Group
</button>
<button onClick={() => setShowImport(true)} className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 shadow-sm cursor-pointer">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Import Quiz
</button>
</div>
</div>
{/* Empty state */}
{!loading && quizzes.length === 0 && (
<div className={`text-center py-16 ${animate ? "animate-slide-up" : ""}`}>
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-bg-callout mb-4">
<svg className="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-text-heading mb-1">No quizzes yet</h3>
<p className="text-text-secondary mb-4">Import your first quiz to start practicing</p>
<button
onClick={() => setShowImport(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-all duration-200 cursor-pointer"
>
Import Quiz
{isCreatingGroup && (
<div className="mb-6 p-4 bg-bg-surface rounded-xl border border-primary/20 shadow-sm flex items-center gap-3">
<input
autoFocus
type="text"
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
placeholder="e.g. Exam 1 Quizzes"
className="flex-1 px-3 py-2 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<button onClick={handleCreateGroup} className="px-4 py-2 bg-primary text-white font-medium rounded-md hover:bg-primary-hover cursor-pointer">
Save
</button>
<button onClick={() => { setIsCreatingGroup(false); setNewGroupName(""); }} className="px-4 py-2 bg-bg-surface-alt text-text-heading font-medium rounded-md hover:bg-border cursor-pointer">
Cancel
</button>
</div>
)}
{/* Quiz grid */}
{!loading && quizzes.length > 0 && (
<div className={`grid grid-cols-1 md:grid-cols-2 gap-4 ${animate ? "animate-slide-up" : ""}`}>
{quizzes.map((quiz) => (
<div
key={quiz.id}
className="group bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300"
>
<div className="p-6 flex flex-col flex-1 h-full">
{/* Name and Description (editable) */}
{editingId === quiz.id ? (
<div className="flex flex-col gap-3 mb-3">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="Quiz Name"
autoFocus
className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
placeholder="Description (optional)"
rows={2}
className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
<div className="flex gap-2 mt-1">
<button
onClick={() => handleRename(quiz.id)}
className="px-3 py-1.5 bg-primary text-white text-xs font-medium rounded-md hover:bg-primary-hover cursor-pointer"
>
Save Changes
</button>
<button
onClick={() => setEditingId(null)}
className="px-3 py-1.5 bg-bg-surface-alt text-text-heading text-xs font-medium rounded-md hover:bg-border cursor-pointer transition-colors"
>
Cancel
</button>
</div>
</div>
) : (
<>
<h3 className="text-lg font-semibold text-text-heading mb-1">
{quiz.name}
</h3>
{quiz.description && (
<p className="text-sm text-text-secondary mb-3 line-clamp-2">
{quiz.description}
</p>
)}
</>
)}
<div className="flex items-center gap-3 mt-auto">
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium">
{quiz._count.questions} {quiz._count.questions === 1 ? "question" : "questions"}
</span>
{quiz._count.attempts > 0 && (
<span className="text-xs text-text-muted">
{quiz._count.attempts} {quiz._count.attempts === 1 ? "attempt" : "attempts"}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-border-light">
<Link
href={`/${classSlug}/quizzes/${quiz.id}`}
className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200"
>
Start Quiz
</Link>
<button
onClick={() => {
setEditingId(quiz.id);
setEditName(quiz.name);
setEditDescription(quiz.description || "");
}}
className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer"
title="Edit"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDelete(quiz.id, quiz.name)}
className="p-2 rounded-lg text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer"
title="Delete"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
{/* Editing quiz modal/inline - simplified as a modal-like overlay for simplicity when editing */}
{editingId && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
<div className="bg-bg-surface rounded-xl p-6 w-full max-w-md shadow-lg border border-border">
<h3 className="text-lg font-semibold text-text-heading mb-4">Edit Quiz</h3>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="Quiz Name"
autoFocus
className="w-full px-3 py-2 mb-3 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
placeholder="Description (optional)"
rows={3}
className="w-full px-3 py-2 mb-4 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
<div className="flex gap-2 justify-end">
<button onClick={() => setEditingId(null)} className="px-4 py-2 bg-bg-surface-alt text-text-heading font-medium rounded-md hover:bg-border cursor-pointer">Cancel</button>
<button onClick={() => handleRenameQuiz(editingId)} className="px-4 py-2 bg-primary text-white font-medium rounded-md hover:bg-primary-hover cursor-pointer">Save Changes</button>
</div>
))}
</div>
</div>
)}
{/* Import Modal */}
{!loading && quizzes.length === 0 && groups.length === 0 && (
<div className="text-center py-16">
<h3 className="text-lg font-semibold text-text-heading mb-1">No quizzes yet</h3>
<p className="text-text-secondary mb-4">Import your first quiz or create a group</p>
</div>
)}
{!loading && (
<DndContext sensors={sensors} collisionDetection={pointerWithin} onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd}>
<div className="space-y-8">
{groupedQuizzes.map(group => (
<div key={group.id} className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light">
<div className="flex items-center justify-between mb-4">
{editingGroupId === group.id ? (
<div className="flex items-center gap-2">
<input autoFocus value={editGroupName} onChange={e => setEditGroupName(e.target.value)} className="px-2 py-1 rounded border border-border bg-bg-surface text-text-heading focus:ring-1 focus:ring-primary" />
<button onClick={() => handleRenameGroup(group.id)} className="text-xs px-2 py-1 bg-primary text-white rounded">Save</button>
<button onClick={() => setEditingGroupId(null)} className="text-xs px-2 py-1 bg-bg-surface text-text-heading rounded border border-border-light">Cancel</button>
</div>
) : (
<h3 className="text-lg font-semibold text-text-heading">{group.name}</h3>
)}
<div className="flex items-center gap-2">
<ShareMenu targetType="GROUP" contentId={group.id} classSlug={params.classSlug as string} />
<button onClick={() => { setEditingGroupId(group.id); setEditGroupName(group.name); }} className="p-1.5 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface transition-colors cursor-pointer"><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg></button>
<button onClick={() => handleDeleteGroup(group.id, group.name)} className="p-1.5 rounded-lg text-text-muted hover:text-error hover:bg-bg-surface transition-colors cursor-pointer"><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg></button>
</div>
</div>
<SortableContext id={group.id} items={group.quizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id={group.id}>
{group.quizzes.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">Drop quizzes here</div>
) : (
group.quizzes.map(quiz => (
<SortableQuizCard key={quiz.id} quiz={quiz} onEdit={(q: QuizItem) => { setEditingId(q.id); setEditName(q.name); setEditDescription(q.description || ""); }} onDelete={handleDeleteQuiz} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
</div>
))}
{/* Uncategorized */}
<div className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light">
<h3 className="text-lg font-semibold text-text-heading mb-4">Uncategorized</h3>
<SortableContext id="uncategorized" items={uncategorizedQuizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id="uncategorized">
{uncategorizedQuizzes.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">No uncategorized quizzes</div>
) : (
uncategorizedQuizzes.map(quiz => (
<SortableQuizCard key={quiz.id} quiz={quiz} onEdit={(q: QuizItem) => { setEditingId(q.id); setEditName(q.name); setEditDescription(q.description || ""); }} onDelete={handleDeleteQuiz} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
</div>
</div>
<DragOverlay>
{activeQuiz ? (
<div className="opacity-80 scale-105 shadow-xl rotate-2">
<SortableQuizCard quiz={activeQuiz} onEdit={()=>{}} onDelete={()=>{}} classSlug={classSlug} />
</div>
) : null}
</DragOverlay>
</DndContext>
)}
{showImport && (
<ImportModal
classId={classId}
importType="quizzes"
onClose={() => setShowImport(false)}
onImported={() => {
setShowImport(false);
window.location.reload();
}}
/>
<ImportModal classId={classId} importType="quizzes" onClose={() => setShowImport(false)} onImported={() => { setShowImport(false); window.location.reload(); }} />
)}
</div>
);

View file

@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { items } = body;
if (!Array.isArray(items)) {
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
}
// items should be an array of { id, sortOrder, groupId }
// Using a transaction to perform all updates
await prisma.$transaction(
items.map((item: any) =>
prisma.deck.update({
where: { id: item.id },
data: {
sortOrder: item.sortOrder,
groupId: item.groupId,
},
})
)
);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: "Failed to reorder decks" }, { status: 500 });
}
}

View file

@ -9,7 +9,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
}
const { classId, data, name } = body;
const { classId, data, name, groupId } = body;
if (!classId || typeof classId !== "string") {
return NextResponse.json({ error: "classId is required" }, { status: 400 });
@ -27,7 +27,8 @@ export async function POST(request: NextRequest) {
const deck = await deckService.createDeckFromImport(
classId,
parsed.data,
name?.trim() || undefined
name?.trim() || undefined,
groupId || null
);
return NextResponse.json(deck, { status: 201 });

View file

@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const body = await request.json();
const { name, sortOrder } = body;
const updateData: any = {};
if (name !== undefined) updateData.name = name;
if (sortOrder !== undefined) updateData.sortOrder = sortOrder;
const group = await prisma.materialGroup.update({
where: { id },
data: updateData,
});
return NextResponse.json(group);
} catch (error) {
return NextResponse.json({ error: "Failed to update material group" }, { status: 500 });
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
await prisma.materialGroup.delete({
where: { id },
});
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: "Failed to delete material group" }, { status: 500 });
}
}

View file

@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const classId = searchParams.get("classId");
const type = searchParams.get("type");
if (!classId) {
return NextResponse.json({ error: "classId is required" }, { status: 400 });
}
if (type && type !== "DECK" && type !== "QUIZ") {
return NextResponse.json({ error: "Invalid type" }, { status: 400 });
}
const whereClause: any = { classId };
if (type) {
whereClause.type = type;
}
try {
const groups = await prisma.materialGroup.findMany({
where: whereClause,
orderBy: { sortOrder: "asc" },
});
return NextResponse.json(groups);
} catch (error) {
return NextResponse.json({ error: "Failed to fetch material groups" }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { classId, name, type } = body;
if (!classId || !name || (type !== "DECK" && type !== "QUIZ")) {
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
}
const maxOrder = await prisma.materialGroup.aggregate({
where: { classId, type },
_max: { sortOrder: true },
});
const sortOrder = (maxOrder._max.sortOrder ?? -1) + 1;
const group = await prisma.materialGroup.create({
data: { classId, name, type, sortOrder },
});
return NextResponse.json(group, { status: 201 });
} catch (error) {
return NextResponse.json({ error: "Failed to create material group" }, { status: 500 });
}
}

View file

@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { items } = body;
if (!Array.isArray(items)) {
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
}
// items should be an array of { id, sortOrder, groupId }
// Using a transaction to perform all updates
await prisma.$transaction(
items.map((item: any) =>
prisma.quizSet.update({
where: { id: item.id },
data: {
sortOrder: item.sortOrder,
groupId: item.groupId,
},
})
)
);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: "Failed to reorder quizzes" }, { status: 500 });
}
}

View file

@ -9,7 +9,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
}
const { classId, data, name } = body;
const { classId, data, name, groupId } = body;
if (!classId || typeof classId !== "string") {
return NextResponse.json({ error: "classId is required" }, { status: 400 });
@ -27,7 +27,8 @@ export async function POST(request: NextRequest) {
const quizSet = await quizService.createQuizSetFromImport(
classId,
parsed.data,
name?.trim() || undefined
name?.trim() || undefined,
groupId || null
);
return NextResponse.json(quizSet, { status: 201 });

View file

@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { toggleShareLink, getShareLinkForContent } from "@/services/shareService";
import { toggleShareLink, getShareLinkForContent, isContentSharedViaGroup } from "@/services/shareService";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const targetType = searchParams.get("targetType") as "DECK" | "QUIZ";
const targetType = searchParams.get("targetType") as "DECK" | "QUIZ" | "GROUP";
const contentId = searchParams.get("contentId");
if (!targetType || !contentId) {
@ -14,7 +14,19 @@ export async function GET(request: NextRequest) {
}
const link = await getShareLinkForContent(targetType, contentId);
return NextResponse.json({ token: link?.id || null });
if (targetType === "DECK" || targetType === "QUIZ") {
const groupShare = await isContentSharedViaGroup(targetType, contentId);
if (groupShare) {
return NextResponse.json({
token: groupShare.token,
isGroupShared: true,
groupName: groupShare.groupName,
});
}
}
return NextResponse.json({ token: link?.id || null, isGroupShared: false });
}
export async function POST(request: NextRequest) {
@ -27,6 +39,6 @@ export async function POST(request: NextRequest) {
);
}
const link = await toggleShareLink(body.targetType, body.contentId);
const link = await toggleShareLink(body.targetType as "DECK" | "QUIZ" | "GROUP", body.contentId);
return NextResponse.json({ token: link?.id || null });
}

View file

@ -0,0 +1,133 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
export function SharedGroupViewer({ data }: { data: any }) {
const pathname = usePathname();
const router = useRouter();
// Combine decks and quizSets if needed, but a group is either DECK or QUIZ type.
const items = data.type === "DECK" ? data.decks : data.quizSets;
const [progressMap, setProgressMap] = useState<Record<string, { currentIndex: number, total: number }>>({});
useEffect(() => {
if (typeof window === 'undefined') return;
const newProgress: Record<string, { currentIndex: number, total: number }> = {};
items.forEach((item: any) => {
const key = data.type === "DECK" ? `flashcard_progress_${item.id}` : `quiz_progress_${item.id}`;
const saved = localStorage.getItem(key);
if (saved) {
try {
const parsed = JSON.parse(saved);
const total = data.type === "DECK" ? (parsed.order?.length || item.cards?.length || 0) : (parsed.order?.length || item.questions?.length || 0);
newProgress[item.id] = {
currentIndex: parsed.currentIndex || 0,
total,
};
} catch(e) {}
}
});
setProgressMap(newProgress);
}, [items, data.type]);
const handleRestart = (e: React.MouseEvent, itemId: string) => {
e.preventDefault();
const key = data.type === "DECK" ? `flashcard_progress_${itemId}` : `quiz_progress_${itemId}`;
localStorage.removeItem(key);
setProgressMap(prev => {
const next = { ...prev };
delete next[itemId];
return next;
});
router.push(`${pathname}?itemId=${itemId}`);
};
if (!items || items.length === 0) {
return (
<div className="text-center py-12">
<h2 className="text-xl font-medium text-text-muted mb-2">This group is empty</h2>
<p className="text-text-secondary">There are no study materials in this group yet.</p>
</div>
);
}
return (
<div>
<div className="mb-8 text-center md:text-left">
<h1 className="text-3xl font-bold font-serif text-text-heading mb-2">
{data.name}
</h1>
<p className="text-text-secondary">
From class: <span className="font-medium text-text-heading">{data.class.name}</span>
</p>
<div className="mt-4 inline-block px-3 py-1 bg-primary/10 text-primary rounded-full text-sm font-medium">
{data.type === "DECK" ? "Flashcard Group" : "Quiz Group"}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{items.map((item: any) => (
<div
key={item.id}
className="group flex flex-col bg-bg-surface border border-border-light rounded-xl shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300"
>
<div className="p-4 flex flex-col flex-1 h-full">
<div className="flex items-start gap-2">
<div className="flex-1">
<h3 className="text-lg font-semibold text-text-heading mb-1">{item.name}</h3>
{item.description && <p className="text-sm text-text-secondary mb-3 line-clamp-2">{item.description}</p>}
</div>
</div>
<div className="flex items-center gap-3 mt-auto pl-1">
{progressMap[item.id] ? (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium font-bold">
{progressMap[item.id].currentIndex} / {progressMap[item.id].total} {data.type === "DECK" ? "cards" : "questions"}
</span>
) : (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium">
{data.type === "DECK" ? `${item.cards?.length || 0} cards` : `${item.questions?.length || 0} questions`}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-border-light pl-1">
{progressMap[item.id] ? (
<>
<Link
href={`${pathname}?itemId=${item.id}`}
className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200"
>
Resume
</Link>
<button
onClick={(e) => handleRestart(e, item.id)}
className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer border border-border-light"
title="Restart"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</>
) : (
<Link
href={`${pathname}?itemId=${item.id}`}
className="flex-1 text-center py-2 px-4 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200"
>
Study
</Link>
)}
</div>
</div>
</div>
))}
</div>
</div>
);
}

View file

@ -1,6 +1,8 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FlashcardViewer } from "@/components/flashcards/FlashcardViewer";
import { QuizViewer } from "@/components/quizzes/QuizViewer";
import { CardList } from "@/components/flashcards/CardList";
@ -8,11 +10,29 @@ import { CardList } from "@/components/flashcards/CardList";
interface SharedViewerProps {
type: string;
data: any;
groupMode?: boolean;
}
export function SharedViewer({ type, data }: SharedViewerProps) {
export function SharedViewer({ type, data, groupMode = false }: SharedViewerProps) {
const pathname = usePathname();
const [restartKey, setRestartKey] = useState(0);
const [view, setView] = useState<"study" | "list">("study");
const [hasSavedSession, setHasSavedSession] = useState(false);
useEffect(() => {
// Check if there's a saved session for this item
const key = type === "flashcards" ? `flashcard_progress_${data.id}` : `quiz_progress_${data.id}`;
if (localStorage.getItem(key)) {
setHasSavedSession(true);
}
}, [type, data.id, restartKey]);
function handleRestart() {
const key = type === "flashcards" ? `flashcard_progress_${data.id}` : `quiz_progress_${data.id}`;
localStorage.removeItem(key);
setHasSavedSession(false);
setRestartKey(k => k + 1);
}
const [showTopics, setShowTopics] = useState(false);
const categories = type === "quizzes" && data.questions
@ -21,6 +41,14 @@ export function SharedViewer({ type, data }: SharedViewerProps) {
return (
<div>
{groupMode && (
<div className="mb-6">
<Link href={pathname} className="inline-flex items-center gap-2 text-sm font-medium text-text-muted hover:text-primary transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
Back to Group
</Link>
</div>
)}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8">
<div className="text-left">
{type === "quizzes" ? (
@ -63,15 +91,16 @@ export function SharedViewer({ type, data }: SharedViewerProps) {
</div>
<div className="flex items-center justify-between w-full md:w-auto gap-2 md:gap-4 self-end md:self-auto">
{view === "study" && (
{view === "study" && hasSavedSession && (
<button
onClick={() => setRestartKey(k => k + 1)}
className="p-1.5 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer"
onClick={handleRestart}
className="p-1.5 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer flex items-center gap-2 font-medium text-sm"
title="Restart Session"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span className="hidden md:inline">Restart</span>
</button>
)}

View file

@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
import { ThemeToggle } from "@/components/ui/ThemeToggle";
import Link from "next/link";
import { SharedViewer } from "./SharedViewer";
import { SharedGroupViewer } from "./SharedGroupViewer";
interface SharedPageProps {
params: Promise<{
@ -12,10 +13,11 @@ interface SharedPageProps {
}>;
}
export default async function SharedPage(props: SharedPageProps) {
export default async function SharedPage(props: SharedPageProps & { searchParams: Promise<{ itemId?: string }> }) {
const { classSlug, type, token } = await props.params;
const { itemId } = await props.searchParams;
if (type !== "flashcards" && type !== "quizzes") {
if (type !== "flashcards" && type !== "quizzes" && type !== "groups") {
notFound();
}
@ -28,12 +30,34 @@ export default async function SharedPage(props: SharedPageProps) {
// Validate that the link matches the URL structure
if (type === "flashcards" && !link.deck) notFound();
if (type === "quizzes" && !link.quizSet) notFound();
if (type === "groups" && !link.group) notFound();
// Validate class slug matches
const targetClassSlug = link.deck ? link.deck.class.slug : link.quizSet?.class.slug;
const targetClassSlug = link.deck ? link.deck.class.slug : (link.quizSet ? link.quizSet.class.slug : link.group?.class.slug);
if (targetClassSlug !== classSlug) {
notFound();
}
let targetData: any = null;
let targetType = type;
if (type === "groups" && link.group) {
if (itemId) {
// Find the specific item inside the group
if (link.group.type === "DECK") {
targetData = link.group.decks.find(d => d.id === itemId);
targetType = "flashcards";
} else {
targetData = link.group.quizSets.find(q => q.id === itemId);
targetType = "quizzes";
}
if (!targetData) notFound();
} else {
targetData = link.group;
}
} else {
targetData = type === "flashcards" ? link.deck : link.quizSet;
}
return (
<div className="min-h-screen bg-bg-base flex flex-col">
@ -45,7 +69,7 @@ export default async function SharedPage(props: SharedPageProps) {
<svg className="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
Shared {type === "flashcards" ? "Deck" : "Quiz"}
Shared {type === "groups" ? "Group" : (type === "flashcards" ? "Deck" : "Quiz")}
</div>
<div className="flex items-center gap-4">
@ -69,10 +93,15 @@ export default async function SharedPage(props: SharedPageProps) {
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<SharedViewer
type={type}
data={type === "flashcards" ? link.deck : link.quizSet}
/>
{type === "groups" && !itemId ? (
<SharedGroupViewer data={targetData} />
) : (
<SharedViewer
type={targetType}
data={targetData}
groupMode={type === "groups"}
/>
)}
</div>
</main>
</div>

View file

@ -27,6 +27,8 @@ interface FlashcardViewerProps {
type CardResult = "correct" | "missed";
export function FlashcardViewer({ cards, deckId, isShared = false, initialProgress }: FlashcardViewerProps) {
const [isLoaded, setIsLoaded] = useState(!isShared);
const [order, setOrder] = useState<string[]>(
initialProgress ? JSON.parse(initialProgress.orderJson) : cards.map((c) => c.id)
);
@ -70,6 +72,30 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre
}
}, [toastMessage]);
// Load from localStorage if shared
useEffect(() => {
if (isShared) {
try {
const saved = localStorage.getItem(`flashcard_progress_${deckId}`);
if (saved) {
const parsed = JSON.parse(saved);
setOrder(parsed.order || cards.map(c => c.id));
setCurrentIndex(parsed.currentIndex || 0);
setResults(parsed.results || {});
setIsShuffled(parsed.mode === "SHUFFLED");
if (parsed.currentIndex >= (parsed.order?.length || cards.length)) {
setCompleted(true);
}
setToastMessage("Session restored");
}
} catch (e) {
// Fallback to defaults
}
setIsLoaded(true);
}
}, [isShared, deckId, cards]);
function toggleShuffle() {
const newShuffled = !isShuffled;
setIsShuffled(newShuffled);
@ -184,7 +210,15 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre
cardResults: Record<string, CardResult>,
m: "SEQUENTIAL" | "SHUFFLED"
) {
if (isShared) return;
if (isShared) {
localStorage.setItem(`flashcard_progress_${deckId}`, JSON.stringify({
mode: m,
currentIndex: index,
order: orderArr,
results: cardResults,
}));
return;
}
fetch("/api/progress", {
method: "PATCH",
@ -238,6 +272,8 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre
}
// Completed summary
if (!isLoaded) return <div className="text-center py-12 text-text-muted">Loading session...</div>;
if (completed) {
return (
<div className="flex flex-col items-center justify-center py-16">

View file

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { parseAndRepairJson } from "@/lib/jsonRepair";
import {
flashcardImportSchema,
@ -23,12 +23,35 @@ interface PreviewData {
data: unknown;
}
interface MaterialGroup {
id: string;
name: string;
}
export function ImportTab({ classId, importType, onImported }: ImportTabProps) {
const [jsonText, setJsonText] = useState("");
const [name, setName] = useState("");
const [preview, setPreview] = useState<PreviewData | null>(null);
const [error, setError] = useState("");
const [importing, setImporting] = useState(false);
const [materialGroups, setMaterialGroups] = useState<MaterialGroup[]>([]);
const [selectedGroupId, setSelectedGroupId] = useState<string>("");
useEffect(() => {
async function fetchGroups() {
try {
const typeParam = importType === "flashcards" ? "DECK" : "QUIZ";
const res = await fetch(`/api/material-groups?classId=${classId}&type=${typeParam}`);
if (res.ok) {
const data = await res.json();
setMaterialGroups(data);
}
} catch (err) {
console.error("Failed to fetch material groups", err);
}
}
fetchGroups();
}, [classId, importType]);
function handleJsonChange(text: string) {
setJsonText(text);
@ -101,6 +124,7 @@ export function ImportTab({ classId, importType, onImported }: ImportTabProps) {
classId,
data: preview.data,
name: name.trim(),
groupId: selectedGroupId || null,
}),
});
@ -136,6 +160,25 @@ export function ImportTab({ classId, importType, onImported }: ImportTabProps) {
/>
</div>
{/* Group field */}
<div>
<label className="block text-sm font-medium text-text-secondary mb-1.5">
Group (Optional)
</label>
<select
value={selectedGroupId}
onChange={(e) => setSelectedGroupId(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200"
>
<option value="">Uncategorized</option>
{materialGroups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</div>
{/* JSON paste area */}
<div>
<label className="block text-sm font-medium text-text-secondary mb-1.5">

View file

@ -47,6 +47,15 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
const [loading, setLoading] = useState(true);
const [resultsData, setResultsData] = useState<any | null>(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
// Auto-hide toast
useEffect(() => {
if (toastMessage) {
const timer = setTimeout(() => setToastMessage(null), 3000);
return () => clearTimeout(timer);
}
}, [toastMessage]);
// Initialize session
useEffect(() => {
@ -67,6 +76,25 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
return;
}
// Shared links: load from localStorage
if (isShared) {
try {
const saved = localStorage.getItem(`quiz_progress_${quiz.id}`);
if (saved) {
const parsed = JSON.parse(saved);
setOrder(parsed.order || quiz.questions.map(q => q.id));
setCurrentIndex(parsed.currentIndex || 0);
setAnswers(parsed.answers || {});
setSubmittedAnswers(Object.keys(parsed.answers || {}));
setToastMessage("Session restored");
setLoading(false);
return;
}
} catch (e) {
// Fallback to defaults
}
}
// Otherwise, normal sequential logic checking for progress
const prog = quiz.progress?.find((p) => p.mode === "SEQUENTIAL");
if (prog) {
@ -96,14 +124,23 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
setSubmittedAnswers([]);
}
setLoading(false);
}, [quiz, retakeIds]);
}, [quiz, retakeIds, isShared]);
// Persist progress as you go
function saveProgress(
idx: number,
ans: Record<string, string[]>
) {
if (retakeIds || isShared) return; // Don't persist retake partial state or shared mode into the main progress row
if (retakeIds) return; // Don't persist retake partial state
if (isShared) {
localStorage.setItem(`quiz_progress_${quiz.id}`, JSON.stringify({
currentIndex: idx,
order: order,
answers: ans,
}));
return;
}
fetch("/api/progress", {
method: "PATCH",
@ -172,6 +209,10 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
}
async function handleFinish() {
if (isShared) {
localStorage.removeItem(`quiz_progress_${quiz.id}`);
}
// If shared, grade it locally and show results, don't ping backend
if (isShared) {
// Simulate an attempt object
@ -268,10 +309,18 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
}
return (
<div className="flex flex-col items-center">
<div className="flex flex-col items-center relative">
{/* Toast Notification */}
{toastMessage && (
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-30 animate-fade-in pointer-events-none">
<div className="bg-bg-surface-alt/95 backdrop-blur shadow-[var(--shadow-card)] text-text-heading text-sm font-semibold px-6 py-2.5 rounded-full whitespace-nowrap">
{toastMessage}
</div>
</div>
)}
{/* Main Unified Card */}
<div className="w-full max-w-3xl bg-bg-surface-alt rounded-2xl border border-border-light shadow-[var(--shadow-card)] overflow-hidden mb-8">
<div className="w-full max-w-3xl bg-bg-surface-alt rounded-2xl border border-border-light shadow-[var(--shadow-card)] overflow-hidden mb-8 mt-12">
{/* Progress Header */}
<div className="px-6 md:px-8 py-5 flex items-center gap-4">
@ -434,7 +483,7 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
<button
onClick={handleSubmit}
disabled={currentSelections.length === 0}
className="w-full py-3.5 rounded-xl bg-primary/60 hover:bg-primary/80 text-white font-bold text-lg tracking-wide disabled:opacity-50 disabled:hover:bg-primary/60 transition-all duration-200 cursor-pointer shadow-sm"
className="w-full py-3.5 rounded-xl bg-primary hover:bg-primary-hover text-white font-bold text-lg tracking-wide disabled:opacity-50 disabled:bg-primary/50 disabled:hover:bg-primary/50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm"
>
Submit Answer
</button>

View file

@ -3,7 +3,7 @@
import { useState, useEffect } from "react";
interface ShareMenuProps {
targetType: "DECK" | "QUIZ";
targetType: "DECK" | "QUIZ" | "GROUP";
contentId: string;
classSlug: string;
}
@ -13,12 +13,18 @@ export function ShareMenu({ targetType, contentId, classSlug }: ShareMenuProps)
const [loading, setLoading] = useState(true);
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const [isGroupShared, setIsGroupShared] = useState(false);
const [groupName, setGroupName] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
fetch(`/api/share?targetType=${targetType}&contentId=${contentId}`)
.then(res => res.json())
.then(data => setToken(data.token))
.then(data => {
setToken(data.token);
setIsGroupShared(data.isGroupShared || false);
setGroupName(data.groupName || null);
})
.finally(() => setLoading(false));
}, [open, targetType, contentId]);
@ -39,7 +45,7 @@ export function ShareMenu({ targetType, contentId, classSlug }: ShareMenuProps)
async function handleCopy() {
if (!token) return;
const typeStr = targetType === "DECK" ? "flashcards" : "quizzes";
const typeStr = targetType === "GROUP" ? "groups" : (targetType === "DECK" ? "flashcards" : "quizzes");
const url = `${window.location.origin}/shared/${classSlug}/${typeStr}/${token}`;
await navigator.clipboard.writeText(url);
setCopied(true);
@ -72,22 +78,29 @@ export function ShareMenu({ targetType, contentId, classSlug }: ShareMenuProps)
<span className="text-sm text-text-secondary">Enable link sharing</span>
<button
onClick={handleToggle}
disabled={isGroupShared}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
token ? 'bg-primary' : 'bg-border'
}`}
token || isGroupShared ? 'bg-primary' : 'bg-border'
} ${isGroupShared ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<span
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
token ? 'translate-x-5' : 'translate-x-1'
token || isGroupShared ? 'translate-x-5' : 'translate-x-1'
}`}
/>
</button>
</div>
{isGroupShared && (
<div className="text-xs text-primary font-medium px-3 py-2 bg-primary/10 rounded-lg">
This item is publicly shared via its group: <span className="font-bold">{groupName}</span>. Turn off group sharing to restrict access.
</div>
)}
{token && (
{(token || isGroupShared) && (
<div className="space-y-2 pt-2 border-t border-border-light">
<p className="text-xs text-text-muted">
Anyone with the link can view and study this {targetType === "DECK" ? "deck" : "quiz"}. No progress is saved.
Anyone with the link can view and study this {targetType === "GROUP" ? "group" : (targetType === "DECK" ? "deck" : "quiz")}. No progress is saved.
</p>
<button
onClick={handleCopy}

View file

@ -72,3 +72,8 @@ export type AuthSecurity = Prisma.AuthSecurityModel
*
*/
export type Setting = Prisma.SettingModel
/**
* Model MaterialGroup
*
*/
export type MaterialGroup = Prisma.MaterialGroupModel

View file

@ -96,3 +96,8 @@ export type AuthSecurity = Prisma.AuthSecurityModel
*
*/
export type Setting = Prisma.SettingModel
/**
* Model MaterialGroup
*
*/
export type MaterialGroup = Prisma.MaterialGroupModel

File diff suppressed because one or more lines are too long

View file

@ -394,7 +394,8 @@ export const ModelName = {
QuizAttempt: 'QuizAttempt',
ShareLink: 'ShareLink',
AuthSecurity: 'AuthSecurity',
Setting: 'Setting'
Setting: 'Setting',
MaterialGroup: 'MaterialGroup'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@ -410,7 +411,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "class" | "deck" | "flashcard" | "quizSet" | "question" | "answerOption" | "studyProgress" | "quizAttempt" | "shareLink" | "authSecurity" | "setting"
modelProps: "class" | "deck" | "flashcard" | "quizSet" | "question" | "answerOption" | "studyProgress" | "quizAttempt" | "shareLink" | "authSecurity" | "setting" | "materialGroup"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@ -1228,6 +1229,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
MaterialGroup: {
payload: Prisma.$MaterialGroupPayload<ExtArgs>
fields: Prisma.MaterialGroupFieldRefs
operations: {
findUnique: {
args: Prisma.MaterialGroupFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload> | null
}
findUniqueOrThrow: {
args: Prisma.MaterialGroupFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>
}
findFirst: {
args: Prisma.MaterialGroupFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload> | null
}
findFirstOrThrow: {
args: Prisma.MaterialGroupFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>
}
findMany: {
args: Prisma.MaterialGroupFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>[]
}
create: {
args: Prisma.MaterialGroupCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>
}
createMany: {
args: Prisma.MaterialGroupCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.MaterialGroupCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>[]
}
delete: {
args: Prisma.MaterialGroupDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>
}
update: {
args: Prisma.MaterialGroupUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>
}
deleteMany: {
args: Prisma.MaterialGroupDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.MaterialGroupUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.MaterialGroupUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>[]
}
upsert: {
args: Prisma.MaterialGroupUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$MaterialGroupPayload>
}
aggregate: {
args: Prisma.MaterialGroupAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateMaterialGroup>
}
groupBy: {
args: Prisma.MaterialGroupGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.MaterialGroupGroupByOutputType>[]
}
count: {
args: Prisma.MaterialGroupCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.MaterialGroupCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
@ -1281,7 +1356,8 @@ export const DeckScalarFieldEnum = {
name: 'name',
description: 'description',
sortOrder: 'sortOrder',
createdAt: 'createdAt'
createdAt: 'createdAt',
groupId: 'groupId'
} as const
export type DeckScalarFieldEnum = (typeof DeckScalarFieldEnum)[keyof typeof DeckScalarFieldEnum]
@ -1304,7 +1380,8 @@ export const QuizSetScalarFieldEnum = {
name: 'name',
description: 'description',
sortOrder: 'sortOrder',
createdAt: 'createdAt'
createdAt: 'createdAt',
groupId: 'groupId'
} as const
export type QuizSetScalarFieldEnum = (typeof QuizSetScalarFieldEnum)[keyof typeof QuizSetScalarFieldEnum]
@ -1368,6 +1445,7 @@ export const ShareLinkScalarFieldEnum = {
targetType: 'targetType',
deckId: 'deckId',
quizSetId: 'quizSetId',
groupId: 'groupId',
createdAt: 'createdAt'
} as const
@ -1392,6 +1470,18 @@ export const SettingScalarFieldEnum = {
export type SettingScalarFieldEnum = (typeof SettingScalarFieldEnum)[keyof typeof SettingScalarFieldEnum]
export const MaterialGroupScalarFieldEnum = {
id: 'id',
classId: 'classId',
name: 'name',
type: 'type',
sortOrder: 'sortOrder',
createdAt: 'createdAt'
} as const
export type MaterialGroupScalarFieldEnum = (typeof MaterialGroupScalarFieldEnum)[keyof typeof MaterialGroupScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@ -1569,6 +1659,7 @@ export type GlobalOmitConfig = {
shareLink?: Prisma.ShareLinkOmit
authSecurity?: Prisma.AuthSecurityOmit
setting?: Prisma.SettingOmit
materialGroup?: Prisma.MaterialGroupOmit
}
/* Types for Logging */

View file

@ -61,7 +61,8 @@ export const ModelName = {
QuizAttempt: 'QuizAttempt',
ShareLink: 'ShareLink',
AuthSecurity: 'AuthSecurity',
Setting: 'Setting'
Setting: 'Setting',
MaterialGroup: 'MaterialGroup'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@ -94,7 +95,8 @@ export const DeckScalarFieldEnum = {
name: 'name',
description: 'description',
sortOrder: 'sortOrder',
createdAt: 'createdAt'
createdAt: 'createdAt',
groupId: 'groupId'
} as const
export type DeckScalarFieldEnum = (typeof DeckScalarFieldEnum)[keyof typeof DeckScalarFieldEnum]
@ -117,7 +119,8 @@ export const QuizSetScalarFieldEnum = {
name: 'name',
description: 'description',
sortOrder: 'sortOrder',
createdAt: 'createdAt'
createdAt: 'createdAt',
groupId: 'groupId'
} as const
export type QuizSetScalarFieldEnum = (typeof QuizSetScalarFieldEnum)[keyof typeof QuizSetScalarFieldEnum]
@ -181,6 +184,7 @@ export const ShareLinkScalarFieldEnum = {
targetType: 'targetType',
deckId: 'deckId',
quizSetId: 'quizSetId',
groupId: 'groupId',
createdAt: 'createdAt'
} as const
@ -205,6 +209,18 @@ export const SettingScalarFieldEnum = {
export type SettingScalarFieldEnum = (typeof SettingScalarFieldEnum)[keyof typeof SettingScalarFieldEnum]
export const MaterialGroupScalarFieldEnum = {
id: 'id',
classId: 'classId',
name: 'name',
type: 'type',
sortOrder: 'sortOrder',
createdAt: 'createdAt'
} as const
export type MaterialGroupScalarFieldEnum = (typeof MaterialGroupScalarFieldEnum)[keyof typeof MaterialGroupScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'

View file

@ -19,4 +19,5 @@ export type * from './models/QuizAttempt'
export type * from './models/ShareLink'
export type * from './models/AuthSecurity'
export type * from './models/Setting'
export type * from './models/MaterialGroup'
export type * from './commonInputTypes'

View file

@ -218,6 +218,7 @@ export type ClassWhereInput = {
createdAt?: Prisma.DateTimeFilter<"Class"> | Date | string
decks?: Prisma.DeckListRelationFilter
quizSets?: Prisma.QuizSetListRelationFilter
materialGroups?: Prisma.MaterialGroupListRelationFilter
}
export type ClassOrderByWithRelationInput = {
@ -228,6 +229,7 @@ export type ClassOrderByWithRelationInput = {
createdAt?: Prisma.SortOrder
decks?: Prisma.DeckOrderByRelationAggregateInput
quizSets?: Prisma.QuizSetOrderByRelationAggregateInput
materialGroups?: Prisma.MaterialGroupOrderByRelationAggregateInput
}
export type ClassWhereUniqueInput = Prisma.AtLeast<{
@ -241,6 +243,7 @@ export type ClassWhereUniqueInput = Prisma.AtLeast<{
createdAt?: Prisma.DateTimeFilter<"Class"> | Date | string
decks?: Prisma.DeckListRelationFilter
quizSets?: Prisma.QuizSetListRelationFilter
materialGroups?: Prisma.MaterialGroupListRelationFilter
}, "id" | "slug">
export type ClassOrderByWithAggregationInput = {
@ -275,6 +278,7 @@ export type ClassCreateInput = {
createdAt?: Date | string
decks?: Prisma.DeckCreateNestedManyWithoutClassInput
quizSets?: Prisma.QuizSetCreateNestedManyWithoutClassInput
materialGroups?: Prisma.MaterialGroupCreateNestedManyWithoutClassInput
}
export type ClassUncheckedCreateInput = {
@ -285,6 +289,7 @@ export type ClassUncheckedCreateInput = {
createdAt?: Date | string
decks?: Prisma.DeckUncheckedCreateNestedManyWithoutClassInput
quizSets?: Prisma.QuizSetUncheckedCreateNestedManyWithoutClassInput
materialGroups?: Prisma.MaterialGroupUncheckedCreateNestedManyWithoutClassInput
}
export type ClassUpdateInput = {
@ -295,6 +300,7 @@ export type ClassUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
decks?: Prisma.DeckUpdateManyWithoutClassNestedInput
quizSets?: Prisma.QuizSetUpdateManyWithoutClassNestedInput
materialGroups?: Prisma.MaterialGroupUpdateManyWithoutClassNestedInput
}
export type ClassUncheckedUpdateInput = {
@ -305,6 +311,7 @@ export type ClassUncheckedUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
decks?: Prisma.DeckUncheckedUpdateManyWithoutClassNestedInput
quizSets?: Prisma.QuizSetUncheckedUpdateManyWithoutClassNestedInput
materialGroups?: Prisma.MaterialGroupUncheckedUpdateManyWithoutClassNestedInput
}
export type ClassCreateManyInput = {
@ -412,6 +419,20 @@ export type ClassUpdateOneRequiredWithoutQuizSetsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.ClassUpdateToOneWithWhereWithoutQuizSetsInput, Prisma.ClassUpdateWithoutQuizSetsInput>, Prisma.ClassUncheckedUpdateWithoutQuizSetsInput>
}
export type ClassCreateNestedOneWithoutMaterialGroupsInput = {
create?: Prisma.XOR<Prisma.ClassCreateWithoutMaterialGroupsInput, Prisma.ClassUncheckedCreateWithoutMaterialGroupsInput>
connectOrCreate?: Prisma.ClassCreateOrConnectWithoutMaterialGroupsInput
connect?: Prisma.ClassWhereUniqueInput
}
export type ClassUpdateOneRequiredWithoutMaterialGroupsNestedInput = {
create?: Prisma.XOR<Prisma.ClassCreateWithoutMaterialGroupsInput, Prisma.ClassUncheckedCreateWithoutMaterialGroupsInput>
connectOrCreate?: Prisma.ClassCreateOrConnectWithoutMaterialGroupsInput
upsert?: Prisma.ClassUpsertWithoutMaterialGroupsInput
connect?: Prisma.ClassWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ClassUpdateToOneWithWhereWithoutMaterialGroupsInput, Prisma.ClassUpdateWithoutMaterialGroupsInput>, Prisma.ClassUncheckedUpdateWithoutMaterialGroupsInput>
}
export type ClassCreateWithoutDecksInput = {
id?: string
slug: string
@ -419,6 +440,7 @@ export type ClassCreateWithoutDecksInput = {
sortOrder?: number
createdAt?: Date | string
quizSets?: Prisma.QuizSetCreateNestedManyWithoutClassInput
materialGroups?: Prisma.MaterialGroupCreateNestedManyWithoutClassInput
}
export type ClassUncheckedCreateWithoutDecksInput = {
@ -428,6 +450,7 @@ export type ClassUncheckedCreateWithoutDecksInput = {
sortOrder?: number
createdAt?: Date | string
quizSets?: Prisma.QuizSetUncheckedCreateNestedManyWithoutClassInput
materialGroups?: Prisma.MaterialGroupUncheckedCreateNestedManyWithoutClassInput
}
export type ClassCreateOrConnectWithoutDecksInput = {
@ -453,6 +476,7 @@ export type ClassUpdateWithoutDecksInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
quizSets?: Prisma.QuizSetUpdateManyWithoutClassNestedInput
materialGroups?: Prisma.MaterialGroupUpdateManyWithoutClassNestedInput
}
export type ClassUncheckedUpdateWithoutDecksInput = {
@ -462,6 +486,7 @@ export type ClassUncheckedUpdateWithoutDecksInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
quizSets?: Prisma.QuizSetUncheckedUpdateManyWithoutClassNestedInput
materialGroups?: Prisma.MaterialGroupUncheckedUpdateManyWithoutClassNestedInput
}
export type ClassCreateWithoutQuizSetsInput = {
@ -471,6 +496,7 @@ export type ClassCreateWithoutQuizSetsInput = {
sortOrder?: number
createdAt?: Date | string
decks?: Prisma.DeckCreateNestedManyWithoutClassInput
materialGroups?: Prisma.MaterialGroupCreateNestedManyWithoutClassInput
}
export type ClassUncheckedCreateWithoutQuizSetsInput = {
@ -480,6 +506,7 @@ export type ClassUncheckedCreateWithoutQuizSetsInput = {
sortOrder?: number
createdAt?: Date | string
decks?: Prisma.DeckUncheckedCreateNestedManyWithoutClassInput
materialGroups?: Prisma.MaterialGroupUncheckedCreateNestedManyWithoutClassInput
}
export type ClassCreateOrConnectWithoutQuizSetsInput = {
@ -505,6 +532,7 @@ export type ClassUpdateWithoutQuizSetsInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
decks?: Prisma.DeckUpdateManyWithoutClassNestedInput
materialGroups?: Prisma.MaterialGroupUpdateManyWithoutClassNestedInput
}
export type ClassUncheckedUpdateWithoutQuizSetsInput = {
@ -514,6 +542,63 @@ export type ClassUncheckedUpdateWithoutQuizSetsInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
decks?: Prisma.DeckUncheckedUpdateManyWithoutClassNestedInput
materialGroups?: Prisma.MaterialGroupUncheckedUpdateManyWithoutClassNestedInput
}
export type ClassCreateWithoutMaterialGroupsInput = {
id?: string
slug: string
name: string
sortOrder?: number
createdAt?: Date | string
decks?: Prisma.DeckCreateNestedManyWithoutClassInput
quizSets?: Prisma.QuizSetCreateNestedManyWithoutClassInput
}
export type ClassUncheckedCreateWithoutMaterialGroupsInput = {
id?: string
slug: string
name: string
sortOrder?: number
createdAt?: Date | string
decks?: Prisma.DeckUncheckedCreateNestedManyWithoutClassInput
quizSets?: Prisma.QuizSetUncheckedCreateNestedManyWithoutClassInput
}
export type ClassCreateOrConnectWithoutMaterialGroupsInput = {
where: Prisma.ClassWhereUniqueInput
create: Prisma.XOR<Prisma.ClassCreateWithoutMaterialGroupsInput, Prisma.ClassUncheckedCreateWithoutMaterialGroupsInput>
}
export type ClassUpsertWithoutMaterialGroupsInput = {
update: Prisma.XOR<Prisma.ClassUpdateWithoutMaterialGroupsInput, Prisma.ClassUncheckedUpdateWithoutMaterialGroupsInput>
create: Prisma.XOR<Prisma.ClassCreateWithoutMaterialGroupsInput, Prisma.ClassUncheckedCreateWithoutMaterialGroupsInput>
where?: Prisma.ClassWhereInput
}
export type ClassUpdateToOneWithWhereWithoutMaterialGroupsInput = {
where?: Prisma.ClassWhereInput
data: Prisma.XOR<Prisma.ClassUpdateWithoutMaterialGroupsInput, Prisma.ClassUncheckedUpdateWithoutMaterialGroupsInput>
}
export type ClassUpdateWithoutMaterialGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
slug?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
decks?: Prisma.DeckUpdateManyWithoutClassNestedInput
quizSets?: Prisma.QuizSetUpdateManyWithoutClassNestedInput
}
export type ClassUncheckedUpdateWithoutMaterialGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
slug?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
decks?: Prisma.DeckUncheckedUpdateManyWithoutClassNestedInput
quizSets?: Prisma.QuizSetUncheckedUpdateManyWithoutClassNestedInput
}
@ -524,11 +609,13 @@ export type ClassUncheckedUpdateWithoutQuizSetsInput = {
export type ClassCountOutputType = {
decks: number
quizSets: number
materialGroups: number
}
export type ClassCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
decks?: boolean | ClassCountOutputTypeCountDecksArgs
quizSets?: boolean | ClassCountOutputTypeCountQuizSetsArgs
materialGroups?: boolean | ClassCountOutputTypeCountMaterialGroupsArgs
}
/**
@ -555,6 +642,13 @@ export type ClassCountOutputTypeCountQuizSetsArgs<ExtArgs extends runtime.Types.
where?: Prisma.QuizSetWhereInput
}
/**
* ClassCountOutputType without action
*/
export type ClassCountOutputTypeCountMaterialGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.MaterialGroupWhereInput
}
export type ClassSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@ -564,6 +658,7 @@ export type ClassSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
createdAt?: boolean
decks?: boolean | Prisma.Class$decksArgs<ExtArgs>
quizSets?: boolean | Prisma.Class$quizSetsArgs<ExtArgs>
materialGroups?: boolean | Prisma.Class$materialGroupsArgs<ExtArgs>
_count?: boolean | Prisma.ClassCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["class"]>
@ -595,6 +690,7 @@ export type ClassOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = ru
export type ClassInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
decks?: boolean | Prisma.Class$decksArgs<ExtArgs>
quizSets?: boolean | Prisma.Class$quizSetsArgs<ExtArgs>
materialGroups?: boolean | Prisma.Class$materialGroupsArgs<ExtArgs>
_count?: boolean | Prisma.ClassCountOutputTypeDefaultArgs<ExtArgs>
}
export type ClassIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
@ -605,6 +701,7 @@ export type $ClassPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
objects: {
decks: Prisma.$DeckPayload<ExtArgs>[]
quizSets: Prisma.$QuizSetPayload<ExtArgs>[]
materialGroups: Prisma.$MaterialGroupPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@ -1008,6 +1105,7 @@ export interface Prisma__ClassClient<T, Null = never, ExtArgs extends runtime.Ty
readonly [Symbol.toStringTag]: "PrismaPromise"
decks<T extends Prisma.Class$decksArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Class$decksArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$DeckPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
quizSets<T extends Prisma.Class$quizSetsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Class$quizSetsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$QuizSetPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
materialGroups<T extends Prisma.Class$materialGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Class$materialGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MaterialGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@ -1480,6 +1578,30 @@ export type Class$quizSetsArgs<ExtArgs extends runtime.Types.Extensions.Internal
distinct?: Prisma.QuizSetScalarFieldEnum | Prisma.QuizSetScalarFieldEnum[]
}
/**
* Class.materialGroups
*/
export type Class$materialGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MaterialGroup
*/
select?: Prisma.MaterialGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the MaterialGroup
*/
omit?: Prisma.MaterialGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MaterialGroupInclude<ExtArgs> | null
where?: Prisma.MaterialGroupWhereInput
orderBy?: Prisma.MaterialGroupOrderByWithRelationInput | Prisma.MaterialGroupOrderByWithRelationInput[]
cursor?: Prisma.MaterialGroupWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.MaterialGroupScalarFieldEnum | Prisma.MaterialGroupScalarFieldEnum[]
}
/**
* Class without action
*/

View file

@ -41,6 +41,7 @@ export type DeckMinAggregateOutputType = {
description: string | null
sortOrder: number | null
createdAt: Date | null
groupId: string | null
}
export type DeckMaxAggregateOutputType = {
@ -50,6 +51,7 @@ export type DeckMaxAggregateOutputType = {
description: string | null
sortOrder: number | null
createdAt: Date | null
groupId: string | null
}
export type DeckCountAggregateOutputType = {
@ -59,6 +61,7 @@ export type DeckCountAggregateOutputType = {
description: number
sortOrder: number
createdAt: number
groupId: number
_all: number
}
@ -78,6 +81,7 @@ export type DeckMinAggregateInputType = {
description?: true
sortOrder?: true
createdAt?: true
groupId?: true
}
export type DeckMaxAggregateInputType = {
@ -87,6 +91,7 @@ export type DeckMaxAggregateInputType = {
description?: true
sortOrder?: true
createdAt?: true
groupId?: true
}
export type DeckCountAggregateInputType = {
@ -96,6 +101,7 @@ export type DeckCountAggregateInputType = {
description?: true
sortOrder?: true
createdAt?: true
groupId?: true
_all?: true
}
@ -192,6 +198,7 @@ export type DeckGroupByOutputType = {
description: string | null
sortOrder: number
createdAt: Date
groupId: string | null
_count: DeckCountAggregateOutputType | null
_avg: DeckAvgAggregateOutputType | null
_sum: DeckSumAggregateOutputType | null
@ -224,7 +231,9 @@ export type DeckWhereInput = {
description?: Prisma.StringNullableFilter<"Deck"> | string | null
sortOrder?: Prisma.IntFilter<"Deck"> | number
createdAt?: Prisma.DateTimeFilter<"Deck"> | Date | string
groupId?: Prisma.StringNullableFilter<"Deck"> | string | null
class?: Prisma.XOR<Prisma.ClassScalarRelationFilter, Prisma.ClassWhereInput>
group?: Prisma.XOR<Prisma.MaterialGroupNullableScalarRelationFilter, Prisma.MaterialGroupWhereInput> | null
cards?: Prisma.FlashcardListRelationFilter
shareLink?: Prisma.XOR<Prisma.ShareLinkNullableScalarRelationFilter, Prisma.ShareLinkWhereInput> | null
progress?: Prisma.StudyProgressListRelationFilter
@ -237,7 +246,9 @@ export type DeckOrderByWithRelationInput = {
description?: Prisma.SortOrderInput | Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
class?: Prisma.ClassOrderByWithRelationInput
group?: Prisma.MaterialGroupOrderByWithRelationInput
cards?: Prisma.FlashcardOrderByRelationAggregateInput
shareLink?: Prisma.ShareLinkOrderByWithRelationInput
progress?: Prisma.StudyProgressOrderByRelationAggregateInput
@ -253,7 +264,9 @@ export type DeckWhereUniqueInput = Prisma.AtLeast<{
description?: Prisma.StringNullableFilter<"Deck"> | string | null
sortOrder?: Prisma.IntFilter<"Deck"> | number
createdAt?: Prisma.DateTimeFilter<"Deck"> | Date | string
groupId?: Prisma.StringNullableFilter<"Deck"> | string | null
class?: Prisma.XOR<Prisma.ClassScalarRelationFilter, Prisma.ClassWhereInput>
group?: Prisma.XOR<Prisma.MaterialGroupNullableScalarRelationFilter, Prisma.MaterialGroupWhereInput> | null
cards?: Prisma.FlashcardListRelationFilter
shareLink?: Prisma.XOR<Prisma.ShareLinkNullableScalarRelationFilter, Prisma.ShareLinkWhereInput> | null
progress?: Prisma.StudyProgressListRelationFilter
@ -266,6 +279,7 @@ export type DeckOrderByWithAggregationInput = {
description?: Prisma.SortOrderInput | Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.DeckCountOrderByAggregateInput
_avg?: Prisma.DeckAvgOrderByAggregateInput
_max?: Prisma.DeckMaxOrderByAggregateInput
@ -283,6 +297,7 @@ export type DeckScalarWhereWithAggregatesInput = {
description?: Prisma.StringNullableWithAggregatesFilter<"Deck"> | string | null
sortOrder?: Prisma.IntWithAggregatesFilter<"Deck"> | number
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Deck"> | Date | string
groupId?: Prisma.StringNullableWithAggregatesFilter<"Deck"> | string | null
}
export type DeckCreateInput = {
@ -292,6 +307,7 @@ export type DeckCreateInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutDecksInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
@ -304,6 +320,7 @@ export type DeckUncheckedCreateInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
@ -316,6 +333,7 @@ export type DeckUpdateInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutDecksNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
@ -328,6 +346,7 @@ export type DeckUncheckedUpdateInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
@ -340,6 +359,7 @@ export type DeckCreateManyInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
}
export type DeckUpdateManyMutationInput = {
@ -357,6 +377,7 @@ export type DeckUncheckedUpdateManyInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type DeckListRelationFilter = {
@ -376,6 +397,7 @@ export type DeckCountOrderByAggregateInput = {
description?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type DeckAvgOrderByAggregateInput = {
@ -389,6 +411,7 @@ export type DeckMaxOrderByAggregateInput = {
description?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type DeckMinOrderByAggregateInput = {
@ -398,6 +421,7 @@ export type DeckMinOrderByAggregateInput = {
description?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type DeckSumOrderByAggregateInput = {
@ -506,12 +530,55 @@ export type DeckUpdateOneWithoutShareLinkNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.DeckUpdateToOneWithWhereWithoutShareLinkInput, Prisma.DeckUpdateWithoutShareLinkInput>, Prisma.DeckUncheckedUpdateWithoutShareLinkInput>
}
export type DeckCreateNestedManyWithoutGroupInput = {
create?: Prisma.XOR<Prisma.DeckCreateWithoutGroupInput, Prisma.DeckUncheckedCreateWithoutGroupInput> | Prisma.DeckCreateWithoutGroupInput[] | Prisma.DeckUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.DeckCreateOrConnectWithoutGroupInput | Prisma.DeckCreateOrConnectWithoutGroupInput[]
createMany?: Prisma.DeckCreateManyGroupInputEnvelope
connect?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
}
export type DeckUncheckedCreateNestedManyWithoutGroupInput = {
create?: Prisma.XOR<Prisma.DeckCreateWithoutGroupInput, Prisma.DeckUncheckedCreateWithoutGroupInput> | Prisma.DeckCreateWithoutGroupInput[] | Prisma.DeckUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.DeckCreateOrConnectWithoutGroupInput | Prisma.DeckCreateOrConnectWithoutGroupInput[]
createMany?: Prisma.DeckCreateManyGroupInputEnvelope
connect?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
}
export type DeckUpdateManyWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.DeckCreateWithoutGroupInput, Prisma.DeckUncheckedCreateWithoutGroupInput> | Prisma.DeckCreateWithoutGroupInput[] | Prisma.DeckUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.DeckCreateOrConnectWithoutGroupInput | Prisma.DeckCreateOrConnectWithoutGroupInput[]
upsert?: Prisma.DeckUpsertWithWhereUniqueWithoutGroupInput | Prisma.DeckUpsertWithWhereUniqueWithoutGroupInput[]
createMany?: Prisma.DeckCreateManyGroupInputEnvelope
set?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
disconnect?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
delete?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
connect?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
update?: Prisma.DeckUpdateWithWhereUniqueWithoutGroupInput | Prisma.DeckUpdateWithWhereUniqueWithoutGroupInput[]
updateMany?: Prisma.DeckUpdateManyWithWhereWithoutGroupInput | Prisma.DeckUpdateManyWithWhereWithoutGroupInput[]
deleteMany?: Prisma.DeckScalarWhereInput | Prisma.DeckScalarWhereInput[]
}
export type DeckUncheckedUpdateManyWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.DeckCreateWithoutGroupInput, Prisma.DeckUncheckedCreateWithoutGroupInput> | Prisma.DeckCreateWithoutGroupInput[] | Prisma.DeckUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.DeckCreateOrConnectWithoutGroupInput | Prisma.DeckCreateOrConnectWithoutGroupInput[]
upsert?: Prisma.DeckUpsertWithWhereUniqueWithoutGroupInput | Prisma.DeckUpsertWithWhereUniqueWithoutGroupInput[]
createMany?: Prisma.DeckCreateManyGroupInputEnvelope
set?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
disconnect?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
delete?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
connect?: Prisma.DeckWhereUniqueInput | Prisma.DeckWhereUniqueInput[]
update?: Prisma.DeckUpdateWithWhereUniqueWithoutGroupInput | Prisma.DeckUpdateWithWhereUniqueWithoutGroupInput[]
updateMany?: Prisma.DeckUpdateManyWithWhereWithoutGroupInput | Prisma.DeckUpdateManyWithWhereWithoutGroupInput[]
deleteMany?: Prisma.DeckScalarWhereInput | Prisma.DeckScalarWhereInput[]
}
export type DeckCreateWithoutClassInput = {
id?: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
@ -523,6 +590,7 @@ export type DeckUncheckedCreateWithoutClassInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
@ -563,6 +631,7 @@ export type DeckScalarWhereInput = {
description?: Prisma.StringNullableFilter<"Deck"> | string | null
sortOrder?: Prisma.IntFilter<"Deck"> | number
createdAt?: Prisma.DateTimeFilter<"Deck"> | Date | string
groupId?: Prisma.StringNullableFilter<"Deck"> | string | null
}
export type DeckCreateWithoutCardsInput = {
@ -572,6 +641,7 @@ export type DeckCreateWithoutCardsInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutDecksInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
}
@ -583,6 +653,7 @@ export type DeckUncheckedCreateWithoutCardsInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
}
@ -610,6 +681,7 @@ export type DeckUpdateWithoutCardsInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutDecksNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
}
@ -621,6 +693,7 @@ export type DeckUncheckedUpdateWithoutCardsInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
}
@ -632,6 +705,7 @@ export type DeckCreateWithoutProgressInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutDecksInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
}
@ -643,6 +717,7 @@ export type DeckUncheckedCreateWithoutProgressInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
}
@ -670,6 +745,7 @@ export type DeckUpdateWithoutProgressInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutDecksNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
}
@ -681,6 +757,7 @@ export type DeckUncheckedUpdateWithoutProgressInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
}
@ -692,6 +769,7 @@ export type DeckCreateWithoutShareLinkInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutDecksInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutDecksInput
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
}
@ -703,6 +781,7 @@ export type DeckUncheckedCreateWithoutShareLinkInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
}
@ -730,6 +809,7 @@ export type DeckUpdateWithoutShareLinkInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutDecksNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
}
@ -741,16 +821,67 @@ export type DeckUncheckedUpdateWithoutShareLinkInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
}
export type DeckCreateWithoutGroupInput = {
id?: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutDecksInput
cards?: Prisma.FlashcardCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutDeckInput
}
export type DeckUncheckedCreateWithoutGroupInput = {
id?: string
classId: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
cards?: Prisma.FlashcardUncheckedCreateNestedManyWithoutDeckInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutDeckInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutDeckInput
}
export type DeckCreateOrConnectWithoutGroupInput = {
where: Prisma.DeckWhereUniqueInput
create: Prisma.XOR<Prisma.DeckCreateWithoutGroupInput, Prisma.DeckUncheckedCreateWithoutGroupInput>
}
export type DeckCreateManyGroupInputEnvelope = {
data: Prisma.DeckCreateManyGroupInput | Prisma.DeckCreateManyGroupInput[]
}
export type DeckUpsertWithWhereUniqueWithoutGroupInput = {
where: Prisma.DeckWhereUniqueInput
update: Prisma.XOR<Prisma.DeckUpdateWithoutGroupInput, Prisma.DeckUncheckedUpdateWithoutGroupInput>
create: Prisma.XOR<Prisma.DeckCreateWithoutGroupInput, Prisma.DeckUncheckedCreateWithoutGroupInput>
}
export type DeckUpdateWithWhereUniqueWithoutGroupInput = {
where: Prisma.DeckWhereUniqueInput
data: Prisma.XOR<Prisma.DeckUpdateWithoutGroupInput, Prisma.DeckUncheckedUpdateWithoutGroupInput>
}
export type DeckUpdateManyWithWhereWithoutGroupInput = {
where: Prisma.DeckScalarWhereInput
data: Prisma.XOR<Prisma.DeckUpdateManyMutationInput, Prisma.DeckUncheckedUpdateManyWithoutGroupInput>
}
export type DeckCreateManyClassInput = {
id?: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
}
export type DeckUpdateWithoutClassInput = {
@ -759,6 +890,7 @@ export type DeckUpdateWithoutClassInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
group?: Prisma.MaterialGroupUpdateOneWithoutDecksNestedInput
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
@ -770,6 +902,7 @@ export type DeckUncheckedUpdateWithoutClassInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
@ -781,6 +914,49 @@ export type DeckUncheckedUpdateManyWithoutClassInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type DeckCreateManyGroupInput = {
id?: string
classId: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
}
export type DeckUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutDecksNestedInput
cards?: Prisma.FlashcardUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutDeckNestedInput
}
export type DeckUncheckedUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
classId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
cards?: Prisma.FlashcardUncheckedUpdateManyWithoutDeckNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutDeckNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutDeckNestedInput
}
export type DeckUncheckedUpdateManyWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
classId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@ -830,7 +1006,9 @@ export type DeckSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.Deck$groupArgs<ExtArgs>
cards?: boolean | Prisma.Deck$cardsArgs<ExtArgs>
shareLink?: boolean | Prisma.Deck$shareLinkArgs<ExtArgs>
progress?: boolean | Prisma.Deck$progressArgs<ExtArgs>
@ -844,7 +1022,9 @@ export type DeckSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.Deck$groupArgs<ExtArgs>
}, ExtArgs["result"]["deck"]>
export type DeckSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@ -854,7 +1034,9 @@ export type DeckSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.Deck$groupArgs<ExtArgs>
}, ExtArgs["result"]["deck"]>
export type DeckSelectScalar = {
@ -864,11 +1046,13 @@ export type DeckSelectScalar = {
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
}
export type DeckOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "classId" | "name" | "description" | "sortOrder" | "createdAt", ExtArgs["result"]["deck"]>
export type DeckOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "classId" | "name" | "description" | "sortOrder" | "createdAt" | "groupId", ExtArgs["result"]["deck"]>
export type DeckInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.Deck$groupArgs<ExtArgs>
cards?: boolean | Prisma.Deck$cardsArgs<ExtArgs>
shareLink?: boolean | Prisma.Deck$shareLinkArgs<ExtArgs>
progress?: boolean | Prisma.Deck$progressArgs<ExtArgs>
@ -876,15 +1060,18 @@ export type DeckInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
}
export type DeckIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.Deck$groupArgs<ExtArgs>
}
export type DeckIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.Deck$groupArgs<ExtArgs>
}
export type $DeckPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Deck"
objects: {
class: Prisma.$ClassPayload<ExtArgs>
group: Prisma.$MaterialGroupPayload<ExtArgs> | null
cards: Prisma.$FlashcardPayload<ExtArgs>[]
shareLink: Prisma.$ShareLinkPayload<ExtArgs> | null
progress: Prisma.$StudyProgressPayload<ExtArgs>[]
@ -896,6 +1083,7 @@ export type $DeckPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
description: string | null
sortOrder: number
createdAt: Date
groupId: string | null
}, ExtArgs["result"]["deck"]>
composites: {}
}
@ -1291,6 +1479,7 @@ readonly fields: DeckFieldRefs;
export interface Prisma__DeckClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
class<T extends Prisma.ClassDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ClassDefaultArgs<ExtArgs>>): Prisma.Prisma__ClassClient<runtime.Types.Result.GetResult<Prisma.$ClassPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
group<T extends Prisma.Deck$groupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$groupArgs<ExtArgs>>): Prisma.Prisma__MaterialGroupClient<runtime.Types.Result.GetResult<Prisma.$MaterialGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
cards<T extends Prisma.Deck$cardsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$cardsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$FlashcardPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
shareLink<T extends Prisma.Deck$shareLinkArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$shareLinkArgs<ExtArgs>>): Prisma.Prisma__ShareLinkClient<runtime.Types.Result.GetResult<Prisma.$ShareLinkPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
progress<T extends Prisma.Deck$progressArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Deck$progressArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$StudyProgressPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
@ -1329,6 +1518,7 @@ export interface DeckFieldRefs {
readonly description: Prisma.FieldRef<"Deck", 'String'>
readonly sortOrder: Prisma.FieldRef<"Deck", 'Int'>
readonly createdAt: Prisma.FieldRef<"Deck", 'DateTime'>
readonly groupId: Prisma.FieldRef<"Deck", 'String'>
}
@ -1727,6 +1917,25 @@ export type DeckDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
limit?: number
}
/**
* Deck.group
*/
export type Deck$groupArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MaterialGroup
*/
select?: Prisma.MaterialGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the MaterialGroup
*/
omit?: Prisma.MaterialGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MaterialGroupInclude<ExtArgs> | null
where?: Prisma.MaterialGroupWhereInput
}
/**
* Deck.cards
*/

File diff suppressed because it is too large Load diff

View file

@ -41,6 +41,7 @@ export type QuizSetMinAggregateOutputType = {
description: string | null
sortOrder: number | null
createdAt: Date | null
groupId: string | null
}
export type QuizSetMaxAggregateOutputType = {
@ -50,6 +51,7 @@ export type QuizSetMaxAggregateOutputType = {
description: string | null
sortOrder: number | null
createdAt: Date | null
groupId: string | null
}
export type QuizSetCountAggregateOutputType = {
@ -59,6 +61,7 @@ export type QuizSetCountAggregateOutputType = {
description: number
sortOrder: number
createdAt: number
groupId: number
_all: number
}
@ -78,6 +81,7 @@ export type QuizSetMinAggregateInputType = {
description?: true
sortOrder?: true
createdAt?: true
groupId?: true
}
export type QuizSetMaxAggregateInputType = {
@ -87,6 +91,7 @@ export type QuizSetMaxAggregateInputType = {
description?: true
sortOrder?: true
createdAt?: true
groupId?: true
}
export type QuizSetCountAggregateInputType = {
@ -96,6 +101,7 @@ export type QuizSetCountAggregateInputType = {
description?: true
sortOrder?: true
createdAt?: true
groupId?: true
_all?: true
}
@ -192,6 +198,7 @@ export type QuizSetGroupByOutputType = {
description: string | null
sortOrder: number
createdAt: Date
groupId: string | null
_count: QuizSetCountAggregateOutputType | null
_avg: QuizSetAvgAggregateOutputType | null
_sum: QuizSetSumAggregateOutputType | null
@ -224,7 +231,9 @@ export type QuizSetWhereInput = {
description?: Prisma.StringNullableFilter<"QuizSet"> | string | null
sortOrder?: Prisma.IntFilter<"QuizSet"> | number
createdAt?: Prisma.DateTimeFilter<"QuizSet"> | Date | string
groupId?: Prisma.StringNullableFilter<"QuizSet"> | string | null
class?: Prisma.XOR<Prisma.ClassScalarRelationFilter, Prisma.ClassWhereInput>
group?: Prisma.XOR<Prisma.MaterialGroupNullableScalarRelationFilter, Prisma.MaterialGroupWhereInput> | null
questions?: Prisma.QuestionListRelationFilter
attempts?: Prisma.QuizAttemptListRelationFilter
shareLink?: Prisma.XOR<Prisma.ShareLinkNullableScalarRelationFilter, Prisma.ShareLinkWhereInput> | null
@ -238,7 +247,9 @@ export type QuizSetOrderByWithRelationInput = {
description?: Prisma.SortOrderInput | Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
class?: Prisma.ClassOrderByWithRelationInput
group?: Prisma.MaterialGroupOrderByWithRelationInput
questions?: Prisma.QuestionOrderByRelationAggregateInput
attempts?: Prisma.QuizAttemptOrderByRelationAggregateInput
shareLink?: Prisma.ShareLinkOrderByWithRelationInput
@ -255,7 +266,9 @@ export type QuizSetWhereUniqueInput = Prisma.AtLeast<{
description?: Prisma.StringNullableFilter<"QuizSet"> | string | null
sortOrder?: Prisma.IntFilter<"QuizSet"> | number
createdAt?: Prisma.DateTimeFilter<"QuizSet"> | Date | string
groupId?: Prisma.StringNullableFilter<"QuizSet"> | string | null
class?: Prisma.XOR<Prisma.ClassScalarRelationFilter, Prisma.ClassWhereInput>
group?: Prisma.XOR<Prisma.MaterialGroupNullableScalarRelationFilter, Prisma.MaterialGroupWhereInput> | null
questions?: Prisma.QuestionListRelationFilter
attempts?: Prisma.QuizAttemptListRelationFilter
shareLink?: Prisma.XOR<Prisma.ShareLinkNullableScalarRelationFilter, Prisma.ShareLinkWhereInput> | null
@ -269,6 +282,7 @@ export type QuizSetOrderByWithAggregationInput = {
description?: Prisma.SortOrderInput | Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.QuizSetCountOrderByAggregateInput
_avg?: Prisma.QuizSetAvgOrderByAggregateInput
_max?: Prisma.QuizSetMaxOrderByAggregateInput
@ -286,6 +300,7 @@ export type QuizSetScalarWhereWithAggregatesInput = {
description?: Prisma.StringNullableWithAggregatesFilter<"QuizSet"> | string | null
sortOrder?: Prisma.IntWithAggregatesFilter<"QuizSet"> | number
createdAt?: Prisma.DateTimeWithAggregatesFilter<"QuizSet"> | Date | string
groupId?: Prisma.StringNullableWithAggregatesFilter<"QuizSet"> | string | null
}
export type QuizSetCreateInput = {
@ -295,6 +310,7 @@ export type QuizSetCreateInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutQuizSetsInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutQuizSetsInput
questions?: Prisma.QuestionCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutQuizSetInput
@ -308,6 +324,7 @@ export type QuizSetUncheckedCreateInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
questions?: Prisma.QuestionUncheckedCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptUncheckedCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutQuizSetInput
@ -321,6 +338,7 @@ export type QuizSetUpdateInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutQuizSetsNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutQuizSetsNestedInput
questions?: Prisma.QuestionUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutQuizSetNestedInput
@ -334,6 +352,7 @@ export type QuizSetUncheckedUpdateInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
questions?: Prisma.QuestionUncheckedUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUncheckedUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutQuizSetNestedInput
@ -347,6 +366,7 @@ export type QuizSetCreateManyInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
}
export type QuizSetUpdateManyMutationInput = {
@ -364,6 +384,7 @@ export type QuizSetUncheckedUpdateManyInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type QuizSetListRelationFilter = {
@ -383,6 +404,7 @@ export type QuizSetCountOrderByAggregateInput = {
description?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type QuizSetAvgOrderByAggregateInput = {
@ -396,6 +418,7 @@ export type QuizSetMaxOrderByAggregateInput = {
description?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type QuizSetMinOrderByAggregateInput = {
@ -405,6 +428,7 @@ export type QuizSetMinOrderByAggregateInput = {
description?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type QuizSetSumOrderByAggregateInput = {
@ -523,12 +547,55 @@ export type QuizSetUpdateOneWithoutShareLinkNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.QuizSetUpdateToOneWithWhereWithoutShareLinkInput, Prisma.QuizSetUpdateWithoutShareLinkInput>, Prisma.QuizSetUncheckedUpdateWithoutShareLinkInput>
}
export type QuizSetCreateNestedManyWithoutGroupInput = {
create?: Prisma.XOR<Prisma.QuizSetCreateWithoutGroupInput, Prisma.QuizSetUncheckedCreateWithoutGroupInput> | Prisma.QuizSetCreateWithoutGroupInput[] | Prisma.QuizSetUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.QuizSetCreateOrConnectWithoutGroupInput | Prisma.QuizSetCreateOrConnectWithoutGroupInput[]
createMany?: Prisma.QuizSetCreateManyGroupInputEnvelope
connect?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
}
export type QuizSetUncheckedCreateNestedManyWithoutGroupInput = {
create?: Prisma.XOR<Prisma.QuizSetCreateWithoutGroupInput, Prisma.QuizSetUncheckedCreateWithoutGroupInput> | Prisma.QuizSetCreateWithoutGroupInput[] | Prisma.QuizSetUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.QuizSetCreateOrConnectWithoutGroupInput | Prisma.QuizSetCreateOrConnectWithoutGroupInput[]
createMany?: Prisma.QuizSetCreateManyGroupInputEnvelope
connect?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
}
export type QuizSetUpdateManyWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.QuizSetCreateWithoutGroupInput, Prisma.QuizSetUncheckedCreateWithoutGroupInput> | Prisma.QuizSetCreateWithoutGroupInput[] | Prisma.QuizSetUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.QuizSetCreateOrConnectWithoutGroupInput | Prisma.QuizSetCreateOrConnectWithoutGroupInput[]
upsert?: Prisma.QuizSetUpsertWithWhereUniqueWithoutGroupInput | Prisma.QuizSetUpsertWithWhereUniqueWithoutGroupInput[]
createMany?: Prisma.QuizSetCreateManyGroupInputEnvelope
set?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
disconnect?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
delete?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
connect?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
update?: Prisma.QuizSetUpdateWithWhereUniqueWithoutGroupInput | Prisma.QuizSetUpdateWithWhereUniqueWithoutGroupInput[]
updateMany?: Prisma.QuizSetUpdateManyWithWhereWithoutGroupInput | Prisma.QuizSetUpdateManyWithWhereWithoutGroupInput[]
deleteMany?: Prisma.QuizSetScalarWhereInput | Prisma.QuizSetScalarWhereInput[]
}
export type QuizSetUncheckedUpdateManyWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.QuizSetCreateWithoutGroupInput, Prisma.QuizSetUncheckedCreateWithoutGroupInput> | Prisma.QuizSetCreateWithoutGroupInput[] | Prisma.QuizSetUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.QuizSetCreateOrConnectWithoutGroupInput | Prisma.QuizSetCreateOrConnectWithoutGroupInput[]
upsert?: Prisma.QuizSetUpsertWithWhereUniqueWithoutGroupInput | Prisma.QuizSetUpsertWithWhereUniqueWithoutGroupInput[]
createMany?: Prisma.QuizSetCreateManyGroupInputEnvelope
set?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
disconnect?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
delete?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
connect?: Prisma.QuizSetWhereUniqueInput | Prisma.QuizSetWhereUniqueInput[]
update?: Prisma.QuizSetUpdateWithWhereUniqueWithoutGroupInput | Prisma.QuizSetUpdateWithWhereUniqueWithoutGroupInput[]
updateMany?: Prisma.QuizSetUpdateManyWithWhereWithoutGroupInput | Prisma.QuizSetUpdateManyWithWhereWithoutGroupInput[]
deleteMany?: Prisma.QuizSetScalarWhereInput | Prisma.QuizSetScalarWhereInput[]
}
export type QuizSetCreateWithoutClassInput = {
id?: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
group?: Prisma.MaterialGroupCreateNestedOneWithoutQuizSetsInput
questions?: Prisma.QuestionCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutQuizSetInput
@ -541,6 +608,7 @@ export type QuizSetUncheckedCreateWithoutClassInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
questions?: Prisma.QuestionUncheckedCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptUncheckedCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutQuizSetInput
@ -582,6 +650,7 @@ export type QuizSetScalarWhereInput = {
description?: Prisma.StringNullableFilter<"QuizSet"> | string | null
sortOrder?: Prisma.IntFilter<"QuizSet"> | number
createdAt?: Prisma.DateTimeFilter<"QuizSet"> | Date | string
groupId?: Prisma.StringNullableFilter<"QuizSet"> | string | null
}
export type QuizSetCreateWithoutQuestionsInput = {
@ -591,6 +660,7 @@ export type QuizSetCreateWithoutQuestionsInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutQuizSetsInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutQuizSetsInput
attempts?: Prisma.QuizAttemptCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutQuizSetInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutQuizSetInput
@ -603,6 +673,7 @@ export type QuizSetUncheckedCreateWithoutQuestionsInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
attempts?: Prisma.QuizAttemptUncheckedCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutQuizSetInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutQuizSetInput
@ -631,6 +702,7 @@ export type QuizSetUpdateWithoutQuestionsInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutQuizSetsNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutQuizSetsNestedInput
attempts?: Prisma.QuizAttemptUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutQuizSetNestedInput
@ -643,6 +715,7 @@ export type QuizSetUncheckedUpdateWithoutQuestionsInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
attempts?: Prisma.QuizAttemptUncheckedUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutQuizSetNestedInput
@ -655,6 +728,7 @@ export type QuizSetCreateWithoutProgressInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutQuizSetsInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutQuizSetsInput
questions?: Prisma.QuestionCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutQuizSetInput
@ -667,6 +741,7 @@ export type QuizSetUncheckedCreateWithoutProgressInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
questions?: Prisma.QuestionUncheckedCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptUncheckedCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutQuizSetInput
@ -695,6 +770,7 @@ export type QuizSetUpdateWithoutProgressInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutQuizSetsNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutQuizSetsNestedInput
questions?: Prisma.QuestionUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutQuizSetNestedInput
@ -707,6 +783,7 @@ export type QuizSetUncheckedUpdateWithoutProgressInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
questions?: Prisma.QuestionUncheckedUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUncheckedUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutQuizSetNestedInput
@ -719,6 +796,7 @@ export type QuizSetCreateWithoutAttemptsInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutQuizSetsInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutQuizSetsInput
questions?: Prisma.QuestionCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutQuizSetInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutQuizSetInput
@ -731,6 +809,7 @@ export type QuizSetUncheckedCreateWithoutAttemptsInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
questions?: Prisma.QuestionUncheckedCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutQuizSetInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutQuizSetInput
@ -759,6 +838,7 @@ export type QuizSetUpdateWithoutAttemptsInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutQuizSetsNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutQuizSetsNestedInput
questions?: Prisma.QuestionUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutQuizSetNestedInput
@ -771,6 +851,7 @@ export type QuizSetUncheckedUpdateWithoutAttemptsInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
questions?: Prisma.QuestionUncheckedUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutQuizSetNestedInput
@ -783,6 +864,7 @@ export type QuizSetCreateWithoutShareLinkInput = {
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutQuizSetsInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutQuizSetsInput
questions?: Prisma.QuestionCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptCreateNestedManyWithoutQuizSetInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutQuizSetInput
@ -795,6 +877,7 @@ export type QuizSetUncheckedCreateWithoutShareLinkInput = {
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
questions?: Prisma.QuestionUncheckedCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptUncheckedCreateNestedManyWithoutQuizSetInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutQuizSetInput
@ -823,6 +906,7 @@ export type QuizSetUpdateWithoutShareLinkInput = {
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutQuizSetsNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutQuizSetsNestedInput
questions?: Prisma.QuestionUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUpdateManyWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutQuizSetNestedInput
@ -835,17 +919,70 @@ export type QuizSetUncheckedUpdateWithoutShareLinkInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
questions?: Prisma.QuestionUncheckedUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUncheckedUpdateManyWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutQuizSetNestedInput
}
export type QuizSetCreateWithoutGroupInput = {
id?: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
class: Prisma.ClassCreateNestedOneWithoutQuizSetsInput
questions?: Prisma.QuestionCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkCreateNestedOneWithoutQuizSetInput
progress?: Prisma.StudyProgressCreateNestedManyWithoutQuizSetInput
}
export type QuizSetUncheckedCreateWithoutGroupInput = {
id?: string
classId: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
questions?: Prisma.QuestionUncheckedCreateNestedManyWithoutQuizSetInput
attempts?: Prisma.QuizAttemptUncheckedCreateNestedManyWithoutQuizSetInput
shareLink?: Prisma.ShareLinkUncheckedCreateNestedOneWithoutQuizSetInput
progress?: Prisma.StudyProgressUncheckedCreateNestedManyWithoutQuizSetInput
}
export type QuizSetCreateOrConnectWithoutGroupInput = {
where: Prisma.QuizSetWhereUniqueInput
create: Prisma.XOR<Prisma.QuizSetCreateWithoutGroupInput, Prisma.QuizSetUncheckedCreateWithoutGroupInput>
}
export type QuizSetCreateManyGroupInputEnvelope = {
data: Prisma.QuizSetCreateManyGroupInput | Prisma.QuizSetCreateManyGroupInput[]
}
export type QuizSetUpsertWithWhereUniqueWithoutGroupInput = {
where: Prisma.QuizSetWhereUniqueInput
update: Prisma.XOR<Prisma.QuizSetUpdateWithoutGroupInput, Prisma.QuizSetUncheckedUpdateWithoutGroupInput>
create: Prisma.XOR<Prisma.QuizSetCreateWithoutGroupInput, Prisma.QuizSetUncheckedCreateWithoutGroupInput>
}
export type QuizSetUpdateWithWhereUniqueWithoutGroupInput = {
where: Prisma.QuizSetWhereUniqueInput
data: Prisma.XOR<Prisma.QuizSetUpdateWithoutGroupInput, Prisma.QuizSetUncheckedUpdateWithoutGroupInput>
}
export type QuizSetUpdateManyWithWhereWithoutGroupInput = {
where: Prisma.QuizSetScalarWhereInput
data: Prisma.XOR<Prisma.QuizSetUpdateManyMutationInput, Prisma.QuizSetUncheckedUpdateManyWithoutGroupInput>
}
export type QuizSetCreateManyClassInput = {
id?: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
groupId?: string | null
}
export type QuizSetUpdateWithoutClassInput = {
@ -854,6 +991,7 @@ export type QuizSetUpdateWithoutClassInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
group?: Prisma.MaterialGroupUpdateOneWithoutQuizSetsNestedInput
questions?: Prisma.QuestionUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutQuizSetNestedInput
@ -866,6 +1004,7 @@ export type QuizSetUncheckedUpdateWithoutClassInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
questions?: Prisma.QuestionUncheckedUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUncheckedUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutQuizSetNestedInput
@ -878,6 +1017,51 @@ export type QuizSetUncheckedUpdateManyWithoutClassInput = {
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type QuizSetCreateManyGroupInput = {
id?: string
classId: string
name: string
description?: string | null
sortOrder?: number
createdAt?: Date | string
}
export type QuizSetUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
class?: Prisma.ClassUpdateOneRequiredWithoutQuizSetsNestedInput
questions?: Prisma.QuestionUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUpdateOneWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUpdateManyWithoutQuizSetNestedInput
}
export type QuizSetUncheckedUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
classId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
questions?: Prisma.QuestionUncheckedUpdateManyWithoutQuizSetNestedInput
attempts?: Prisma.QuizAttemptUncheckedUpdateManyWithoutQuizSetNestedInput
shareLink?: Prisma.ShareLinkUncheckedUpdateOneWithoutQuizSetNestedInput
progress?: Prisma.StudyProgressUncheckedUpdateManyWithoutQuizSetNestedInput
}
export type QuizSetUncheckedUpdateManyWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
classId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@ -936,7 +1120,9 @@ export type QuizSetSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.QuizSet$groupArgs<ExtArgs>
questions?: boolean | Prisma.QuizSet$questionsArgs<ExtArgs>
attempts?: boolean | Prisma.QuizSet$attemptsArgs<ExtArgs>
shareLink?: boolean | Prisma.QuizSet$shareLinkArgs<ExtArgs>
@ -951,7 +1137,9 @@ export type QuizSetSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.QuizSet$groupArgs<ExtArgs>
}, ExtArgs["result"]["quizSet"]>
export type QuizSetSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@ -961,7 +1149,9 @@ export type QuizSetSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.QuizSet$groupArgs<ExtArgs>
}, ExtArgs["result"]["quizSet"]>
export type QuizSetSelectScalar = {
@ -971,11 +1161,13 @@ export type QuizSetSelectScalar = {
description?: boolean
sortOrder?: boolean
createdAt?: boolean
groupId?: boolean
}
export type QuizSetOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "classId" | "name" | "description" | "sortOrder" | "createdAt", ExtArgs["result"]["quizSet"]>
export type QuizSetOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "classId" | "name" | "description" | "sortOrder" | "createdAt" | "groupId", ExtArgs["result"]["quizSet"]>
export type QuizSetInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.QuizSet$groupArgs<ExtArgs>
questions?: boolean | Prisma.QuizSet$questionsArgs<ExtArgs>
attempts?: boolean | Prisma.QuizSet$attemptsArgs<ExtArgs>
shareLink?: boolean | Prisma.QuizSet$shareLinkArgs<ExtArgs>
@ -984,15 +1176,18 @@ export type QuizSetInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
}
export type QuizSetIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.QuizSet$groupArgs<ExtArgs>
}
export type QuizSetIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
class?: boolean | Prisma.ClassDefaultArgs<ExtArgs>
group?: boolean | Prisma.QuizSet$groupArgs<ExtArgs>
}
export type $QuizSetPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "QuizSet"
objects: {
class: Prisma.$ClassPayload<ExtArgs>
group: Prisma.$MaterialGroupPayload<ExtArgs> | null
questions: Prisma.$QuestionPayload<ExtArgs>[]
attempts: Prisma.$QuizAttemptPayload<ExtArgs>[]
shareLink: Prisma.$ShareLinkPayload<ExtArgs> | null
@ -1005,6 +1200,7 @@ export type $QuizSetPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
description: string | null
sortOrder: number
createdAt: Date
groupId: string | null
}, ExtArgs["result"]["quizSet"]>
composites: {}
}
@ -1400,6 +1596,7 @@ readonly fields: QuizSetFieldRefs;
export interface Prisma__QuizSetClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
class<T extends Prisma.ClassDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ClassDefaultArgs<ExtArgs>>): Prisma.Prisma__ClassClient<runtime.Types.Result.GetResult<Prisma.$ClassPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
group<T extends Prisma.QuizSet$groupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.QuizSet$groupArgs<ExtArgs>>): Prisma.Prisma__MaterialGroupClient<runtime.Types.Result.GetResult<Prisma.$MaterialGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
questions<T extends Prisma.QuizSet$questionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.QuizSet$questionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$QuestionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
attempts<T extends Prisma.QuizSet$attemptsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.QuizSet$attemptsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$QuizAttemptPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
shareLink<T extends Prisma.QuizSet$shareLinkArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.QuizSet$shareLinkArgs<ExtArgs>>): Prisma.Prisma__ShareLinkClient<runtime.Types.Result.GetResult<Prisma.$ShareLinkPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
@ -1439,6 +1636,7 @@ export interface QuizSetFieldRefs {
readonly description: Prisma.FieldRef<"QuizSet", 'String'>
readonly sortOrder: Prisma.FieldRef<"QuizSet", 'Int'>
readonly createdAt: Prisma.FieldRef<"QuizSet", 'DateTime'>
readonly groupId: Prisma.FieldRef<"QuizSet", 'String'>
}
@ -1837,6 +2035,25 @@ export type QuizSetDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
limit?: number
}
/**
* QuizSet.group
*/
export type QuizSet$groupArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MaterialGroup
*/
select?: Prisma.MaterialGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the MaterialGroup
*/
omit?: Prisma.MaterialGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MaterialGroupInclude<ExtArgs> | null
where?: Prisma.MaterialGroupWhereInput
}
/**
* QuizSet.questions
*/

View file

@ -29,6 +29,7 @@ export type ShareLinkMinAggregateOutputType = {
targetType: string | null
deckId: string | null
quizSetId: string | null
groupId: string | null
createdAt: Date | null
}
@ -37,6 +38,7 @@ export type ShareLinkMaxAggregateOutputType = {
targetType: string | null
deckId: string | null
quizSetId: string | null
groupId: string | null
createdAt: Date | null
}
@ -45,6 +47,7 @@ export type ShareLinkCountAggregateOutputType = {
targetType: number
deckId: number
quizSetId: number
groupId: number
createdAt: number
_all: number
}
@ -55,6 +58,7 @@ export type ShareLinkMinAggregateInputType = {
targetType?: true
deckId?: true
quizSetId?: true
groupId?: true
createdAt?: true
}
@ -63,6 +67,7 @@ export type ShareLinkMaxAggregateInputType = {
targetType?: true
deckId?: true
quizSetId?: true
groupId?: true
createdAt?: true
}
@ -71,6 +76,7 @@ export type ShareLinkCountAggregateInputType = {
targetType?: true
deckId?: true
quizSetId?: true
groupId?: true
createdAt?: true
_all?: true
}
@ -152,6 +158,7 @@ export type ShareLinkGroupByOutputType = {
targetType: string
deckId: string | null
quizSetId: string | null
groupId: string | null
createdAt: Date
_count: ShareLinkCountAggregateOutputType | null
_min: ShareLinkMinAggregateOutputType | null
@ -181,9 +188,11 @@ export type ShareLinkWhereInput = {
targetType?: Prisma.StringFilter<"ShareLink"> | string
deckId?: Prisma.StringNullableFilter<"ShareLink"> | string | null
quizSetId?: Prisma.StringNullableFilter<"ShareLink"> | string | null
groupId?: Prisma.StringNullableFilter<"ShareLink"> | string | null
createdAt?: Prisma.DateTimeFilter<"ShareLink"> | Date | string
deck?: Prisma.XOR<Prisma.DeckNullableScalarRelationFilter, Prisma.DeckWhereInput> | null
quizSet?: Prisma.XOR<Prisma.QuizSetNullableScalarRelationFilter, Prisma.QuizSetWhereInput> | null
group?: Prisma.XOR<Prisma.MaterialGroupNullableScalarRelationFilter, Prisma.MaterialGroupWhereInput> | null
}
export type ShareLinkOrderByWithRelationInput = {
@ -191,15 +200,18 @@ export type ShareLinkOrderByWithRelationInput = {
targetType?: Prisma.SortOrder
deckId?: Prisma.SortOrderInput | Prisma.SortOrder
quizSetId?: Prisma.SortOrderInput | Prisma.SortOrder
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
deck?: Prisma.DeckOrderByWithRelationInput
quizSet?: Prisma.QuizSetOrderByWithRelationInput
group?: Prisma.MaterialGroupOrderByWithRelationInput
}
export type ShareLinkWhereUniqueInput = Prisma.AtLeast<{
id?: string
deckId?: string
quizSetId?: string
groupId?: string
AND?: Prisma.ShareLinkWhereInput | Prisma.ShareLinkWhereInput[]
OR?: Prisma.ShareLinkWhereInput[]
NOT?: Prisma.ShareLinkWhereInput | Prisma.ShareLinkWhereInput[]
@ -207,13 +219,15 @@ export type ShareLinkWhereUniqueInput = Prisma.AtLeast<{
createdAt?: Prisma.DateTimeFilter<"ShareLink"> | Date | string
deck?: Prisma.XOR<Prisma.DeckNullableScalarRelationFilter, Prisma.DeckWhereInput> | null
quizSet?: Prisma.XOR<Prisma.QuizSetNullableScalarRelationFilter, Prisma.QuizSetWhereInput> | null
}, "id" | "deckId" | "quizSetId">
group?: Prisma.XOR<Prisma.MaterialGroupNullableScalarRelationFilter, Prisma.MaterialGroupWhereInput> | null
}, "id" | "deckId" | "quizSetId" | "groupId">
export type ShareLinkOrderByWithAggregationInput = {
id?: Prisma.SortOrder
targetType?: Prisma.SortOrder
deckId?: Prisma.SortOrderInput | Prisma.SortOrder
quizSetId?: Prisma.SortOrderInput | Prisma.SortOrder
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
_count?: Prisma.ShareLinkCountOrderByAggregateInput
_max?: Prisma.ShareLinkMaxOrderByAggregateInput
@ -228,6 +242,7 @@ export type ShareLinkScalarWhereWithAggregatesInput = {
targetType?: Prisma.StringWithAggregatesFilter<"ShareLink"> | string
deckId?: Prisma.StringNullableWithAggregatesFilter<"ShareLink"> | string | null
quizSetId?: Prisma.StringNullableWithAggregatesFilter<"ShareLink"> | string | null
groupId?: Prisma.StringNullableWithAggregatesFilter<"ShareLink"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"ShareLink"> | Date | string
}
@ -237,6 +252,7 @@ export type ShareLinkCreateInput = {
createdAt?: Date | string
deck?: Prisma.DeckCreateNestedOneWithoutShareLinkInput
quizSet?: Prisma.QuizSetCreateNestedOneWithoutShareLinkInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutShareLinkInput
}
export type ShareLinkUncheckedCreateInput = {
@ -244,6 +260,7 @@ export type ShareLinkUncheckedCreateInput = {
targetType: string
deckId?: string | null
quizSetId?: string | null
groupId?: string | null
createdAt?: Date | string
}
@ -253,6 +270,7 @@ export type ShareLinkUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deck?: Prisma.DeckUpdateOneWithoutShareLinkNestedInput
quizSet?: Prisma.QuizSetUpdateOneWithoutShareLinkNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutShareLinkNestedInput
}
export type ShareLinkUncheckedUpdateInput = {
@ -260,6 +278,7 @@ export type ShareLinkUncheckedUpdateInput = {
targetType?: Prisma.StringFieldUpdateOperationsInput | string
deckId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
quizSetId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@ -268,6 +287,7 @@ export type ShareLinkCreateManyInput = {
targetType: string
deckId?: string | null
quizSetId?: string | null
groupId?: string | null
createdAt?: Date | string
}
@ -282,6 +302,7 @@ export type ShareLinkUncheckedUpdateManyInput = {
targetType?: Prisma.StringFieldUpdateOperationsInput | string
deckId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
quizSetId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@ -295,6 +316,7 @@ export type ShareLinkCountOrderByAggregateInput = {
targetType?: Prisma.SortOrder
deckId?: Prisma.SortOrder
quizSetId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
}
@ -303,6 +325,7 @@ export type ShareLinkMaxOrderByAggregateInput = {
targetType?: Prisma.SortOrder
deckId?: Prisma.SortOrder
quizSetId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
}
@ -311,6 +334,7 @@ export type ShareLinkMinOrderByAggregateInput = {
targetType?: Prisma.SortOrder
deckId?: Prisma.SortOrder
quizSetId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
}
@ -378,17 +402,51 @@ export type ShareLinkUncheckedUpdateOneWithoutQuizSetNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.ShareLinkUpdateToOneWithWhereWithoutQuizSetInput, Prisma.ShareLinkUpdateWithoutQuizSetInput>, Prisma.ShareLinkUncheckedUpdateWithoutQuizSetInput>
}
export type ShareLinkCreateNestedOneWithoutGroupInput = {
create?: Prisma.XOR<Prisma.ShareLinkCreateWithoutGroupInput, Prisma.ShareLinkUncheckedCreateWithoutGroupInput>
connectOrCreate?: Prisma.ShareLinkCreateOrConnectWithoutGroupInput
connect?: Prisma.ShareLinkWhereUniqueInput
}
export type ShareLinkUncheckedCreateNestedOneWithoutGroupInput = {
create?: Prisma.XOR<Prisma.ShareLinkCreateWithoutGroupInput, Prisma.ShareLinkUncheckedCreateWithoutGroupInput>
connectOrCreate?: Prisma.ShareLinkCreateOrConnectWithoutGroupInput
connect?: Prisma.ShareLinkWhereUniqueInput
}
export type ShareLinkUpdateOneWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.ShareLinkCreateWithoutGroupInput, Prisma.ShareLinkUncheckedCreateWithoutGroupInput>
connectOrCreate?: Prisma.ShareLinkCreateOrConnectWithoutGroupInput
upsert?: Prisma.ShareLinkUpsertWithoutGroupInput
disconnect?: Prisma.ShareLinkWhereInput | boolean
delete?: Prisma.ShareLinkWhereInput | boolean
connect?: Prisma.ShareLinkWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ShareLinkUpdateToOneWithWhereWithoutGroupInput, Prisma.ShareLinkUpdateWithoutGroupInput>, Prisma.ShareLinkUncheckedUpdateWithoutGroupInput>
}
export type ShareLinkUncheckedUpdateOneWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.ShareLinkCreateWithoutGroupInput, Prisma.ShareLinkUncheckedCreateWithoutGroupInput>
connectOrCreate?: Prisma.ShareLinkCreateOrConnectWithoutGroupInput
upsert?: Prisma.ShareLinkUpsertWithoutGroupInput
disconnect?: Prisma.ShareLinkWhereInput | boolean
delete?: Prisma.ShareLinkWhereInput | boolean
connect?: Prisma.ShareLinkWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ShareLinkUpdateToOneWithWhereWithoutGroupInput, Prisma.ShareLinkUpdateWithoutGroupInput>, Prisma.ShareLinkUncheckedUpdateWithoutGroupInput>
}
export type ShareLinkCreateWithoutDeckInput = {
id?: string
targetType: string
createdAt?: Date | string
quizSet?: Prisma.QuizSetCreateNestedOneWithoutShareLinkInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutShareLinkInput
}
export type ShareLinkUncheckedCreateWithoutDeckInput = {
id?: string
targetType: string
quizSetId?: string | null
groupId?: string | null
createdAt?: Date | string
}
@ -413,12 +471,14 @@ export type ShareLinkUpdateWithoutDeckInput = {
targetType?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
quizSet?: Prisma.QuizSetUpdateOneWithoutShareLinkNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutShareLinkNestedInput
}
export type ShareLinkUncheckedUpdateWithoutDeckInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
targetType?: Prisma.StringFieldUpdateOperationsInput | string
quizSetId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@ -427,12 +487,14 @@ export type ShareLinkCreateWithoutQuizSetInput = {
targetType: string
createdAt?: Date | string
deck?: Prisma.DeckCreateNestedOneWithoutShareLinkInput
group?: Prisma.MaterialGroupCreateNestedOneWithoutShareLinkInput
}
export type ShareLinkUncheckedCreateWithoutQuizSetInput = {
id?: string
targetType: string
deckId?: string | null
groupId?: string | null
createdAt?: Date | string
}
@ -457,12 +519,62 @@ export type ShareLinkUpdateWithoutQuizSetInput = {
targetType?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deck?: Prisma.DeckUpdateOneWithoutShareLinkNestedInput
group?: Prisma.MaterialGroupUpdateOneWithoutShareLinkNestedInput
}
export type ShareLinkUncheckedUpdateWithoutQuizSetInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
targetType?: Prisma.StringFieldUpdateOperationsInput | string
deckId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ShareLinkCreateWithoutGroupInput = {
id?: string
targetType: string
createdAt?: Date | string
deck?: Prisma.DeckCreateNestedOneWithoutShareLinkInput
quizSet?: Prisma.QuizSetCreateNestedOneWithoutShareLinkInput
}
export type ShareLinkUncheckedCreateWithoutGroupInput = {
id?: string
targetType: string
deckId?: string | null
quizSetId?: string | null
createdAt?: Date | string
}
export type ShareLinkCreateOrConnectWithoutGroupInput = {
where: Prisma.ShareLinkWhereUniqueInput
create: Prisma.XOR<Prisma.ShareLinkCreateWithoutGroupInput, Prisma.ShareLinkUncheckedCreateWithoutGroupInput>
}
export type ShareLinkUpsertWithoutGroupInput = {
update: Prisma.XOR<Prisma.ShareLinkUpdateWithoutGroupInput, Prisma.ShareLinkUncheckedUpdateWithoutGroupInput>
create: Prisma.XOR<Prisma.ShareLinkCreateWithoutGroupInput, Prisma.ShareLinkUncheckedCreateWithoutGroupInput>
where?: Prisma.ShareLinkWhereInput
}
export type ShareLinkUpdateToOneWithWhereWithoutGroupInput = {
where?: Prisma.ShareLinkWhereInput
data: Prisma.XOR<Prisma.ShareLinkUpdateWithoutGroupInput, Prisma.ShareLinkUncheckedUpdateWithoutGroupInput>
}
export type ShareLinkUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
targetType?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deck?: Prisma.DeckUpdateOneWithoutShareLinkNestedInput
quizSet?: Prisma.QuizSetUpdateOneWithoutShareLinkNestedInput
}
export type ShareLinkUncheckedUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
targetType?: Prisma.StringFieldUpdateOperationsInput | string
deckId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
quizSetId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@ -473,9 +585,11 @@ export type ShareLinkSelect<ExtArgs extends runtime.Types.Extensions.InternalArg
targetType?: boolean
deckId?: boolean
quizSetId?: boolean
groupId?: boolean
createdAt?: boolean
deck?: boolean | Prisma.ShareLink$deckArgs<ExtArgs>
quizSet?: boolean | Prisma.ShareLink$quizSetArgs<ExtArgs>
group?: boolean | Prisma.ShareLink$groupArgs<ExtArgs>
}, ExtArgs["result"]["shareLink"]>
export type ShareLinkSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@ -483,9 +597,11 @@ export type ShareLinkSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Ext
targetType?: boolean
deckId?: boolean
quizSetId?: boolean
groupId?: boolean
createdAt?: boolean
deck?: boolean | Prisma.ShareLink$deckArgs<ExtArgs>
quizSet?: boolean | Prisma.ShareLink$quizSetArgs<ExtArgs>
group?: boolean | Prisma.ShareLink$groupArgs<ExtArgs>
}, ExtArgs["result"]["shareLink"]>
export type ShareLinkSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@ -493,9 +609,11 @@ export type ShareLinkSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Ext
targetType?: boolean
deckId?: boolean
quizSetId?: boolean
groupId?: boolean
createdAt?: boolean
deck?: boolean | Prisma.ShareLink$deckArgs<ExtArgs>
quizSet?: boolean | Prisma.ShareLink$quizSetArgs<ExtArgs>
group?: boolean | Prisma.ShareLink$groupArgs<ExtArgs>
}, ExtArgs["result"]["shareLink"]>
export type ShareLinkSelectScalar = {
@ -503,21 +621,25 @@ export type ShareLinkSelectScalar = {
targetType?: boolean
deckId?: boolean
quizSetId?: boolean
groupId?: boolean
createdAt?: boolean
}
export type ShareLinkOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "targetType" | "deckId" | "quizSetId" | "createdAt", ExtArgs["result"]["shareLink"]>
export type ShareLinkOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "targetType" | "deckId" | "quizSetId" | "groupId" | "createdAt", ExtArgs["result"]["shareLink"]>
export type ShareLinkInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
deck?: boolean | Prisma.ShareLink$deckArgs<ExtArgs>
quizSet?: boolean | Prisma.ShareLink$quizSetArgs<ExtArgs>
group?: boolean | Prisma.ShareLink$groupArgs<ExtArgs>
}
export type ShareLinkIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
deck?: boolean | Prisma.ShareLink$deckArgs<ExtArgs>
quizSet?: boolean | Prisma.ShareLink$quizSetArgs<ExtArgs>
group?: boolean | Prisma.ShareLink$groupArgs<ExtArgs>
}
export type ShareLinkIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
deck?: boolean | Prisma.ShareLink$deckArgs<ExtArgs>
quizSet?: boolean | Prisma.ShareLink$quizSetArgs<ExtArgs>
group?: boolean | Prisma.ShareLink$groupArgs<ExtArgs>
}
export type $ShareLinkPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@ -525,12 +647,14 @@ export type $ShareLinkPayload<ExtArgs extends runtime.Types.Extensions.InternalA
objects: {
deck: Prisma.$DeckPayload<ExtArgs> | null
quizSet: Prisma.$QuizSetPayload<ExtArgs> | null
group: Prisma.$MaterialGroupPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
targetType: string
deckId: string | null
quizSetId: string | null
groupId: string | null
createdAt: Date
}, ExtArgs["result"]["shareLink"]>
composites: {}
@ -928,6 +1052,7 @@ export interface Prisma__ShareLinkClient<T, Null = never, ExtArgs extends runtim
readonly [Symbol.toStringTag]: "PrismaPromise"
deck<T extends Prisma.ShareLink$deckArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ShareLink$deckArgs<ExtArgs>>): Prisma.Prisma__DeckClient<runtime.Types.Result.GetResult<Prisma.$DeckPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
quizSet<T extends Prisma.ShareLink$quizSetArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ShareLink$quizSetArgs<ExtArgs>>): Prisma.Prisma__QuizSetClient<runtime.Types.Result.GetResult<Prisma.$QuizSetPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
group<T extends Prisma.ShareLink$groupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ShareLink$groupArgs<ExtArgs>>): Prisma.Prisma__MaterialGroupClient<runtime.Types.Result.GetResult<Prisma.$MaterialGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@ -961,6 +1086,7 @@ export interface ShareLinkFieldRefs {
readonly targetType: Prisma.FieldRef<"ShareLink", 'String'>
readonly deckId: Prisma.FieldRef<"ShareLink", 'String'>
readonly quizSetId: Prisma.FieldRef<"ShareLink", 'String'>
readonly groupId: Prisma.FieldRef<"ShareLink", 'String'>
readonly createdAt: Prisma.FieldRef<"ShareLink", 'DateTime'>
}
@ -1398,6 +1524,25 @@ export type ShareLink$quizSetArgs<ExtArgs extends runtime.Types.Extensions.Inter
where?: Prisma.QuizSetWhereInput
}
/**
* ShareLink.group
*/
export type ShareLink$groupArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MaterialGroup
*/
select?: Prisma.MaterialGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the MaterialGroup
*/
omit?: Prisma.MaterialGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MaterialGroupInclude<ExtArgs> | null
where?: Prisma.MaterialGroupWhereInput
}
/**
* ShareLink without action
*/

View file

@ -33,10 +33,11 @@ export async function getDeckWithCards(deckId: string) {
export async function createDeckFromImport(
classId: string,
data: FlashcardImportData,
overrideName?: string
overrideName?: string,
groupId?: string | null
) {
const maxOrder = await prisma.deck.aggregate({
where: { classId },
where: { classId, groupId: groupId || null },
_max: { sortOrder: true },
});
const sortOrder = (maxOrder._max.sortOrder ?? -1) + 1;
@ -44,6 +45,7 @@ export async function createDeckFromImport(
return prisma.deck.create({
data: {
classId,
groupId: groupId || null,
name: overrideName || data.deckName,
description: data.description,
sortOrder,

View file

@ -38,10 +38,11 @@ export async function getQuizSetWithQuestions(quizSetId: string) {
export async function createQuizSetFromImport(
classId: string,
data: QuizImportData,
overrideName?: string
overrideName?: string,
groupId?: string | null
) {
const maxOrder = await prisma.quizSet.aggregate({
where: { classId },
where: { classId, groupId: groupId || null },
_max: { sortOrder: true },
});
const sortOrder = (maxOrder._max.sortOrder ?? -1) + 1;
@ -49,6 +50,7 @@ export async function createQuizSetFromImport(
return prisma.quizSet.create({
data: {
classId,
groupId: groupId || null,
name: overrideName || data.quizName,
description: data.description,
sortOrder,

View file

@ -21,18 +21,45 @@ export async function getShareLink(token: string) {
class: { select: { slug: true, name: true } },
},
},
group: {
include: {
class: { select: { slug: true, name: true } },
decks: {
include: {
cards: { orderBy: { sortOrder: "asc" } },
class: { select: { slug: true, name: true } },
},
orderBy: { sortOrder: "asc" },
},
quizSets: {
include: {
questions: {
orderBy: { sortOrder: "asc" },
include: {
options: { orderBy: { sortOrder: "asc" } },
},
},
class: { select: { slug: true, name: true } },
},
orderBy: { sortOrder: "asc" },
},
}
}
},
});
}
export async function getShareLinkForContent(targetType: "DECK" | "QUIZ", contentId: string) {
export async function getShareLinkForContent(targetType: "DECK" | "QUIZ" | "GROUP", contentId: string) {
if (targetType === "DECK") {
return prisma.shareLink.findUnique({ where: { deckId: contentId } });
}
if (targetType === "GROUP") {
return prisma.shareLink.findUnique({ where: { groupId: contentId } });
}
return prisma.shareLink.findUnique({ where: { quizSetId: contentId } });
}
export async function toggleShareLink(targetType: "DECK" | "QUIZ", contentId: string) {
export async function toggleShareLink(targetType: "DECK" | "QUIZ" | "GROUP", contentId: string) {
const existing = await getShareLinkForContent(targetType, contentId);
if (existing) {
@ -47,6 +74,28 @@ export async function toggleShareLink(targetType: "DECK" | "QUIZ", contentId: st
targetType,
deckId: targetType === "DECK" ? contentId : null,
quizSetId: targetType === "QUIZ" ? contentId : null,
groupId: targetType === "GROUP" ? contentId : null,
},
});
}
export async function isContentSharedViaGroup(targetType: "DECK" | "QUIZ", contentId: string) {
let groupId: string | null = null;
if (targetType === "DECK") {
const deck = await prisma.deck.findUnique({ where: { id: contentId }, select: { groupId: true } });
groupId = deck?.groupId || null;
} else {
const quiz = await prisma.quizSet.findUnique({ where: { id: contentId }, select: { groupId: true } });
groupId = quiz?.groupId || null;
}
if (!groupId) return null;
const groupLink = await prisma.shareLink.findUnique({
where: { groupId },
include: { group: true },
});
return groupLink ? { token: groupLink.id, groupName: groupLink.group?.name } : null;
}

2017
temp.css Normal file

File diff suppressed because it is too large Load diff

1
test.css Normal file
View file

@ -0,0 +1 @@
:root { --color-primary: #fff; }