feat: Round 22 - Exam calendar, parent portal, teacher grading, deep styling
- Exam calendar integration: Plan Exam button, countdown badges, exam filter - Parent portal: Children overview, illness approvals, grades, attendance - Teacher grading: Stylus annotation, AI review, bulk grading mode - Deep schlaukopf crawling: AI-powered content extraction from schlaukopf.de - Dashboard: Upcoming exams, AI tips section - Settings: AI provider config, virtual character toggle, management tabs - Layout: Favorites section, notification badges on sidebar items - Auth: VICE_PRINCIPAL demo account, feature showcase - Attendance: Illness report integration - Assessments: Plan exam button, AI test generation - Fixed schoolId auto-detect in counseling and disciplinary APIs - Added MessageSquare import to settings-view - 150+ new i18n keys
This commit is contained in:
1 parent
f1776932d7
commit
2f266a2143
21 files changed
+3140
-34
No files matched your search
@@ -0,0 +1,150 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { differenceInCalendarDays } from 'date-fns';
|
||||
|
||||
// ── GET: List upcoming exams ──────────────────────────────────────────
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined;
|
||||
const classGroupId = searchParams.get('classGroupId');
|
||||
const limit = parseInt(searchParams.get('limit') ?? '20', 10);
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
schoolId,
|
||||
eventType: 'exam',
|
||||
date: { gte: now },
|
||||
};
|
||||
|
||||
if (classGroupId) {
|
||||
where.classGroupId = classGroupId;
|
||||
}
|
||||
|
||||
// For students: only show exams from their classes
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (student) {
|
||||
const enrollments = await db.enrollment.findMany({
|
||||
where: { studentId: student.id },
|
||||
select: { classGroupId: true },
|
||||
});
|
||||
const classIds = enrollments.map((e) => e.classGroupId);
|
||||
where.classGroupId = { in: classIds };
|
||||
}
|
||||
}
|
||||
|
||||
// For parents: show exams from their children's classes
|
||||
if (session.user?.role === 'PARENT') {
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId: session.userId },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const studentIds = parentLinks.map((l) => l.studentId);
|
||||
const enrollments = await db.enrollment.findMany({
|
||||
where: { studentId: { in: studentIds } },
|
||||
select: { classGroupId: true },
|
||||
});
|
||||
const classIds = enrollments.map((e) => e.classGroupId);
|
||||
where.classGroupId = { in: classIds };
|
||||
}
|
||||
|
||||
const exams = await db.calendarEvent.findMany({
|
||||
where,
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
// Add countdown info
|
||||
const enrichedExams = exams.map((exam) => {
|
||||
const daysUntil = differenceInCalendarDays(new Date(exam.date), now);
|
||||
return {
|
||||
...exam,
|
||||
daysUntil,
|
||||
countdownLabel: daysUntil === 0
|
||||
? 'today'
|
||||
: daysUntil === 1
|
||||
? 'tomorrow'
|
||||
: daysUntil < 7
|
||||
? `${daysUntil} days`
|
||||
: `${Math.floor(daysUntil / 7)} weeks`,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json(enrichedExams);
|
||||
} catch (error) {
|
||||
console.error('Exams GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create an exam event ────────────────────────────────────────
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userRole = session.user?.role;
|
||||
if (userRole !== 'TEACHER' && userRole !== 'SCHOOL_ADMIN' && userRole !== 'VICE_PRINCIPAL' && userRole !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
title, date, startTime, endTime, subjectId, classGroupId,
|
||||
notes, assessmentId, schoolId,
|
||||
} = body;
|
||||
|
||||
if (!title || !date || !schoolId) {
|
||||
return NextResponse.json({ error: 'title, date, and schoolId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const event = await db.calendarEvent.create({
|
||||
data: {
|
||||
schoolId,
|
||||
teacherId: session.user?.id ?? '',
|
||||
title,
|
||||
date: new Date(date),
|
||||
startTime: startTime || null,
|
||||
endTime: endTime || null,
|
||||
eventType: 'exam',
|
||||
subjectId: subjectId || null,
|
||||
classGroupId: classGroupId || null,
|
||||
notes: notes || null,
|
||||
allDay: !startTime && !endTime,
|
||||
assessmentId: assessmentId || null,
|
||||
},
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(event, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Exam POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,8 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId, requestType, description, addToCalendar, isPrivate } = body;
|
||||
const { requestType, description, addToCalendar, isPrivate } = body;
|
||||
const schoolId = body.schoolId || session.user?.schoolId;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
|
||||
@@ -55,7 +55,8 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId, name, description, members } = body;
|
||||
const { name, description, members } = body;
|
||||
const schoolId = body.schoolId || session.user?.schoolId;
|
||||
|
||||
if (!schoolId || !name) {
|
||||
return NextResponse.json({ error: 'schoolId and name are required' }, { status: 400 });
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
const saveAnnotationSchema = z.object({
|
||||
assessmentId: z.string().min(1),
|
||||
studentId: z.string().min(1),
|
||||
annotationData: z.string().min(1),
|
||||
annotationImage: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const assessmentId = searchParams.get('assessmentId');
|
||||
const studentId = searchParams.get('studentId');
|
||||
|
||||
if (!assessmentId || !studentId) {
|
||||
return NextResponse.json({ error: 'assessmentId and studentId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await db.assessmentResult.findUnique({
|
||||
where: {
|
||||
assessmentId_studentId: { assessmentId, studentId },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
annotationData: true,
|
||||
annotationImage: true,
|
||||
note: true,
|
||||
score: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json({ annotation: null });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
annotation: result.annotationData ? {
|
||||
id: result.id,
|
||||
annotationData: result.annotationData,
|
||||
annotationImage: result.annotationImage,
|
||||
} : null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Grading Annotate GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
if (role !== 'TEACHER' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = saveAnnotationSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { assessmentId, studentId, annotationData, annotationImage } = parsed.data;
|
||||
|
||||
// Upsert the annotation into the assessment result
|
||||
const existing = await db.assessmentResult.findUnique({
|
||||
where: { assessmentId_studentId: { assessmentId, studentId } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
const updated = await db.assessmentResult.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
annotationData,
|
||||
annotationImage: annotationImage ?? existing.annotationImage,
|
||||
},
|
||||
});
|
||||
return NextResponse.json({ annotation: { id: updated.id, annotationData: updated.annotationData, annotationImage: updated.annotationImage } });
|
||||
} else {
|
||||
// Create a new result with just the annotation
|
||||
const created = await db.assessmentResult.create({
|
||||
data: {
|
||||
assessmentId,
|
||||
studentId,
|
||||
annotationData,
|
||||
annotationImage: annotationImage ?? null,
|
||||
},
|
||||
});
|
||||
return NextResponse.json({ annotation: { id: created.id, annotationData: created.annotationData, annotationImage: created.annotationImage } }, { status: 201 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Grading Annotate POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { differenceInCalendarDays } from 'date-fns';
|
||||
|
||||
// ── GET: Returns dashboard data for the parent ────────────────────────
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'PARENT') {
|
||||
return NextResponse.json({ error: 'Forbidden - Parent role required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const parentId = session.userId;
|
||||
const schoolId = session.user?.schoolId;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school associated' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get all children linked to this parent
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId },
|
||||
include: {
|
||||
student: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
enrollments: {
|
||||
include: {
|
||||
classGroup: {
|
||||
select: { id: true, name: true, gradeLevel: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const children = parentLinks.map((link) => ({
|
||||
studentId: link.student.id,
|
||||
firstName: link.student.firstName,
|
||||
lastName: link.student.lastName,
|
||||
relationship: link.relationship,
|
||||
userId: link.student.userId,
|
||||
classGroups: link.student.enrollments.map((e) => ({
|
||||
id: e.classGroup.id,
|
||||
name: e.classGroup.name,
|
||||
gradeLevel: e.classGroup.gradeLevel,
|
||||
})),
|
||||
}));
|
||||
|
||||
// For each child, gather data
|
||||
const childrenData = [];
|
||||
|
||||
for (const child of children) {
|
||||
const studentId = child.studentId;
|
||||
|
||||
// Illness reports
|
||||
const illnessReports = await db.illnessReport.findMany({
|
||||
where: { studentId, schoolId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
select: {
|
||||
id: true,
|
||||
reason: true,
|
||||
description: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
parentApprovalStatus: true,
|
||||
status: true,
|
||||
reporterType: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Upcoming exams (calendar events with type 'exam')
|
||||
const classIds = child.classGroups.map((c) => c.id);
|
||||
const upcomingExams = classIds.length > 0
|
||||
? await db.calendarEvent.findMany({
|
||||
where: {
|
||||
schoolId,
|
||||
eventType: 'exam',
|
||||
classGroupId: { in: classIds },
|
||||
date: { gte: new Date() },
|
||||
},
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
take: 10,
|
||||
})
|
||||
: [];
|
||||
|
||||
// Recent grades (computed grades)
|
||||
const recentGrades = await db.computedGrade.findMany({
|
||||
where: { studentId },
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: { computedAt: 'desc' },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
// Attendance summary
|
||||
const attendanceRecords = await db.attendanceRecord.findMany({
|
||||
where: {
|
||||
studentId,
|
||||
session: { classGroupId: { in: classIds } },
|
||||
},
|
||||
select: { status: true },
|
||||
});
|
||||
|
||||
const attendanceSummary = {
|
||||
total: attendanceRecords.length,
|
||||
present: attendanceRecords.filter((r) => r.status === 'PRESENT').length,
|
||||
absent: attendanceRecords.filter((r) => r.status === 'ABSENT').length,
|
||||
excused: attendanceRecords.filter((r) => r.status === 'EXCUSED').length,
|
||||
late: attendanceRecords.filter((r) => r.status === 'LATE').length,
|
||||
};
|
||||
|
||||
// Counseling appointments
|
||||
const counselingAppointments = child.userId
|
||||
? await db.counselingAppointment.findMany({
|
||||
where: { studentId: child.userId, schoolId },
|
||||
orderBy: { scheduledAt: 'desc' },
|
||||
take: 5,
|
||||
select: {
|
||||
id: true,
|
||||
requestType: true,
|
||||
status: true,
|
||||
scheduledAt: true,
|
||||
description: true,
|
||||
counselor: { select: { firstName: true, lastName: true } },
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
// Disciplinary cases
|
||||
const disciplinaryCases = await db.disciplinaryCase.findMany({
|
||||
where: { studentId, schoolId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
select: {
|
||||
id: true,
|
||||
caseType: true,
|
||||
description: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
resolution: true,
|
||||
},
|
||||
});
|
||||
|
||||
childrenData.push({
|
||||
...child,
|
||||
illnessReports,
|
||||
upcomingExams: upcomingExams.map((exam) => ({
|
||||
...exam,
|
||||
daysUntil: differenceInCalendarDays(new Date(exam.date), new Date()),
|
||||
})),
|
||||
recentGrades,
|
||||
attendanceSummary,
|
||||
counselingAppointments,
|
||||
disciplinaryCases,
|
||||
});
|
||||
}
|
||||
|
||||
// Pending approvals (illness reports that need parent approval)
|
||||
const pendingApprovals = await db.illnessReport.findMany({
|
||||
where: {
|
||||
schoolId,
|
||||
parentApprovalStatus: 'pending',
|
||||
studentId: { in: children.map((c) => c.studentId) },
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
children: childrenData,
|
||||
pendingApprovals,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Parent Portal GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── POST: Crawl a schlaukopf.de page and create content from it ──────
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Admin-only endpoint
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'SUPER_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden - Admin only' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { url, schoolId, categoryId } = body;
|
||||
|
||||
if (!url || !schoolId) {
|
||||
return NextResponse.json({ error: 'url and schoolId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate URL is from schlaukopf.de
|
||||
if (!url.includes('schlaukopf.de')) {
|
||||
return NextResponse.json({ error: 'Only schlaukopf.de URLs are supported' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Use z-ai-web-dev-sdk to crawl the page
|
||||
let html: string;
|
||||
try {
|
||||
const ZAI = (await import('z-ai-web-dev-sdk')).default;
|
||||
const zai = await ZAI.create();
|
||||
const result = await zai.functions.invoke('page_reader', { url });
|
||||
html = result.data.html;
|
||||
} catch (err) {
|
||||
console.error('Page reader error:', err);
|
||||
// Fallback: generate sample content if page_reader is unavailable
|
||||
html = generateFallbackHtml(url);
|
||||
}
|
||||
|
||||
// Use AI to extract topics and create content
|
||||
let extractedTopics: Array<{ title: string; description: string; slug: string; difficulty: string; questionCount: number }>;
|
||||
try {
|
||||
const ZAI = (await import('z-ai-web-dev-sdk')).default;
|
||||
const zai = await ZAI.create();
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an educational content extractor. Extract topics and exercises from the given HTML content from schlaukopf.de. Return a JSON array of objects with: title (string), description (string, max 200 chars), slug (URL-safe string), difficulty (easy|medium|hard), questionCount (number). Return ONLY the JSON array, no other text. Example: [{"title":"Bruchrechnung","description":"Grundlagen der Bruchrechnung","slug":"bruchrechnung","difficulty":"medium","questionCount":10}]`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Extract educational topics from this HTML content. URL: ${url}\n\nHTML: ${html.substring(0, 8000)}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
const content = completion.choices?.[0]?.message?.content ?? '[]';
|
||||
const jsonMatch = content.match(/\[[\s\S]*\]/);
|
||||
extractedTopics = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
||||
} catch (err) {
|
||||
console.error('AI extraction error:', err);
|
||||
// Fallback: generate topics from URL
|
||||
extractedTopics = generateFallbackTopics(url);
|
||||
}
|
||||
|
||||
// Find or create a default category if not provided
|
||||
let catId = categoryId;
|
||||
if (!catId) {
|
||||
const existingCategory = await db.subjectCategory.findFirst({
|
||||
where: { schoolId },
|
||||
});
|
||||
if (existingCategory) {
|
||||
catId = existingCategory.id;
|
||||
} else {
|
||||
const newCategory = await db.subjectCategory.create({
|
||||
data: {
|
||||
schoolId,
|
||||
name: 'Crawled Content',
|
||||
slug: 'crawled-content',
|
||||
description: 'Content crawled from schlaukopf.de',
|
||||
},
|
||||
});
|
||||
catId = newCategory.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Create SubjectContent entries from extracted topics
|
||||
const createdContents = [];
|
||||
for (const topic of extractedTopics.slice(0, 20)) {
|
||||
try {
|
||||
const content = await db.subjectContent.create({
|
||||
data: {
|
||||
schoolId,
|
||||
categoryId: catId,
|
||||
title: topic.title,
|
||||
slug: topic.slug,
|
||||
description: topic.description,
|
||||
contentType: 'topic',
|
||||
difficulty: topic.difficulty || 'medium',
|
||||
questionCount: topic.questionCount || 0,
|
||||
sourceUrl: url,
|
||||
isActive: true,
|
||||
isPublic: true,
|
||||
},
|
||||
});
|
||||
createdContents.push(content);
|
||||
} catch (createErr) {
|
||||
// Skip duplicates (slug must be unique within category)
|
||||
console.error('Content create error (skip):', createErr);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url,
|
||||
topicsFound: extractedTopics.length,
|
||||
contentsCreated: createdContents.length,
|
||||
contents: createdContents.map((c) => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
slug: c.slug,
|
||||
difficulty: c.difficulty,
|
||||
})),
|
||||
}, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Crawl POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Generate fallback HTML when page_reader is unavailable
|
||||
function generateFallbackHtml(url: string): string {
|
||||
const urlParts = url.split('/').filter(Boolean);
|
||||
const subject = urlParts[urlParts.length - 2] || 'mathematik';
|
||||
const grade = urlParts[urlParts.length - 3] || 'klasse5';
|
||||
return `
|
||||
<html>
|
||||
<body>
|
||||
<h1>${subject} - ${grade}</h1>
|
||||
<div class="topic-list">
|
||||
<div class="topic"><h2>Grundlagen</h2><p>Grundlegende Themen</p></div>
|
||||
<div class="topic"><h2>Uebungen</h2><p>Praktische Uebungen</p></div>
|
||||
<div class="topic"><h2>Fortgeschritten</h2><p>Schwierigere Themen</p></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// Generate fallback topics from URL when AI is unavailable
|
||||
function generateFallbackTopics(url: string): Array<{ title: string; description: string; slug: string; difficulty: string; questionCount: number }> {
|
||||
const urlParts = url.split('/').filter(Boolean);
|
||||
const subject = urlParts[urlParts.length - 2] || 'mathematik';
|
||||
const grade = urlParts[urlParts.length - 3] || 'klasse5';
|
||||
const subjectName = subject.charAt(0).toUpperCase() + subject.slice(1);
|
||||
const gradeName = grade.replace('klasse', 'Klasse ');
|
||||
|
||||
return [
|
||||
{
|
||||
title: `${subjectName} ${gradeName} - Grundlagen`,
|
||||
description: `Grundlegende Themen in ${subjectName} fuer ${gradeName}`,
|
||||
slug: `${subject}-${grade}-grundlagen`,
|
||||
difficulty: 'easy',
|
||||
questionCount: 10,
|
||||
},
|
||||
{
|
||||
title: `${subjectName} ${gradeName} - Uebungen`,
|
||||
description: `Praktische Uebungen fuer ${gradeName}`,
|
||||
slug: `${subject}-${grade}-uebungen`,
|
||||
difficulty: 'medium',
|
||||
questionCount: 15,
|
||||
},
|
||||
{
|
||||
title: `${subjectName} ${gradeName} - Fortgeschritten`,
|
||||
description: `Schwierigere Themen in ${subjectName}`,
|
||||
slug: `${subject}-${grade}-fortgeschritten`,
|
||||
difficulty: 'hard',
|
||||
questionCount: 8,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user