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

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