Round 19: Competition system, German curriculum learning, enhanced notebooks, student dashboard

- Fix portfolio-view.tsx: replace entry.isPublic with form.isPublic (runtime error)
- Add Competition system: 5 Prisma models (Competition, CompetitionParticipant, CompetitionReward, CompetitionLeaderboard, RewardClaim)
- Add German curriculum learning: 4 Prisma models (SubjectTopic, SubjectLesson, LessonQuestion, StudentAnswer)
- Add 12 API routes for competitions and rewards
- Add 7 API routes for subject topics, lessons, questions, and student answers
- Create competitions-view.tsx with full CRUD, leaderboard, rewards, registration
- Enhance notebooks-view.tsx with 'Lernen' tab for German curriculum subjects
- Add student-specific dashboard with Papier sparen banner, notebook quick access, learning progress
- Add competitions to teacher and student navigation
- Add 130+ i18n keys for DE and EN (competitions, rewards, learning, environment)
- Enhanced student nav with competitions and notebooks
- DSGVO-compliant reward claims with status lifecycle
This commit is contained in:
Mustafa Cetin committed 2026-07-29 14:18:18 +00:00
1 parent 74404708e5
commit 849b48a5a4
22 files changed
+6543 -5

No files matched your search

+228 -1
View File
@@ -74,6 +74,9 @@ model School {
badges Badge[]
studentBadges StudentBadge[]
newsletters Newsletter[]
competitions Competition[]
rewardClaims RewardClaim[]
subjectTopics SubjectTopic[]
district SchoolDistrict? @relation(fields: [districtId], references: [id])
districtId String?
}
@@ -122,6 +125,10 @@ model User {
eventRegistrations EventRegistration[]
awardedBadges StudentBadge[]
newsletters Newsletter[]
competitionsCreated Competition[]
competitionParticipations CompetitionParticipant[]
competitionClaims RewardClaim[]
studentAnswers StudentAnswer[]
}
model SchoolYear {
@@ -266,6 +273,8 @@ model Subject {
homeworks Homework[]
timetableSlots TimetableSlot[]
resources Resource[]
topics SubjectTopic[]
competitions Competition[]
}
model CompetencyTemplate {
@@ -850,7 +859,7 @@ model NotebookPage {
notebookId String
pageNumber Int
title String?
contentType String @default("text") // text | drawing | mixed
contentType String @default("text") // text | drawing | mixed | quiz | exercise | flashcard
textContent String? // markdown/rich text content
drawingData String? // JSON with strokes/paths for canvas
background String @default("lined") // lined | grid | blank | dotted | music
@@ -1593,6 +1602,134 @@ model StudentBadge {
@@map("student_badges")
}
// ─── Competition System ──────────────────────────────────────────────
model Competition {
id String @id @default(cuid())
schoolId String
title String
description String?
competitionType String // "class" | "inter_class" | "inter_school"
category String // "academic" | "sports" | "creativity" | "citizenship" | "digital" | "reading" | "stem" | "other"
subjectId String? // optional subject link
status String @default("draft") // "draft" | "registration" | "active" | "completed" | "cancelled"
startDate DateTime
endDate DateTime
registrationDeadline DateTime?
maxParticipants Int?
scoringType String @default("points") // "points" | "rank" | "time" | "badge_count"
rules String? // markdown rules/description
isPublic Boolean @default(false) // visible to all schools?
isDemo Boolean @default(false)
createdById String // teacher/admin who created
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
school School @relation(fields: [schoolId], references: [id])
createdBy User @relation(fields: [createdById], references: [id])
subject Subject? @relation(fields: [subjectId], references: [id])
participants CompetitionParticipant[]
rewards CompetitionReward[]
leaderboard CompetitionLeaderboard[]
claims RewardClaim[]
@@index([schoolId])
@@index([status])
@@index([competitionType])
@@index([startDate])
@@map("competitions")
}
model CompetitionParticipant {
id String @id @default(cuid())
competitionId String
participantType String // "student" | "class_group" | "school"
participantId String // studentId, classGroupId, or schoolId depending on type
userId String? // the student's userId (for individual participation)
registeredAt DateTime @default(now())
score Int @default(0)
rank Int?
isDisqualified Boolean @default(false)
notes String?
isDemo Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id])
@@unique([competitionId, participantType, participantId])
@@index([competitionId])
@@index([userId])
@@map("competition_participants")
}
model CompetitionReward {
id String @id @default(cuid())
competitionId String
name String
description String?
rewardType String // "digital_code" | "badge" | "certificate" | "experience" | "physical"
rewardValue String? // promo code, badge ID, certificate URL, etc.
rewardProvider String? // "netflix" | "amazon" | "cinema" | "theater" | "concert" | "custom"
rankRequirement Int? // minimum rank to receive (1=first place, etc.)
pointsRequired Int? // minimum points to receive
quantity Int @default(1) // how many available
claimedCount Int @default(0)
imageUrl String? // reward image
isDemo Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
claims RewardClaim[]
@@index([competitionId])
@@map("competition_rewards")
}
model CompetitionLeaderboard {
id String @id @default(cuid())
competitionId String
participantType String // "student" | "class_group" | "school"
participantId String
participantName String // denormalized for fast reads
score Int @default(0)
rank Int?
updatedAt DateTime @updatedAt
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
@@unique([competitionId, participantType, participantId])
@@index([competitionId, rank])
@@map("competition_leaderboard")
}
model RewardClaim {
id String @id @default(cuid())
schoolId String
competitionId String
rewardId String
userId String // student who claimed
claimedAt DateTime @default(now())
code String? // the actual promo code (encrypted for DSGVO)
status String @default("pending") // "pending" | "claimed" | "expired" | "revoked"
expiresAt DateTime?
notes String?
isDemo Boolean @default(false)
createdAt DateTime @default(now())
school School @relation(fields: [schoolId], references: [id])
user User @relation(fields: [userId], references: [id])
competition Competition @relation(fields: [competitionId], references: [id])
reward CompetitionReward @relation(fields: [rewardId], references: [id])
@@index([schoolId, userId])
@@index([competitionId])
@@map("reward_claims")
}
// ─── Newsletter / School Blog ──────────────────────────────────────
model Newsletter {
@@ -1618,3 +1755,93 @@ model Newsletter {
@@index([schoolId, isPublished, publishedAt])
@@map("newsletters")
}
// ─── Learning Content (German Curriculum) ─────────────────────────────
model SubjectTopic {
id String @id @default(cuid())
schoolId String
subjectId String
title String
description String?
gradeLevel String? // "1" | "2" | "3" | "4" | "5" | "6" (German Grundschule)
curriculumCode String? // e.g., "DE-NRW-Math-1-01" (German state curriculum code)
sortOrder Int @default(0)
icon String? // Lucide icon name
color String @default("#10b981")
isDemo Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
school School @relation(fields: [schoolId], references: [id])
subject Subject @relation(fields: [subjectId], references: [id])
lessons SubjectLesson[]
@@index([schoolId, subjectId])
@@index([gradeLevel])
@@map("subject_topics")
}
model SubjectLesson {
id String @id @default(cuid())
topicId String
title String
description String?
lessonType String @default("explanation") // "explanation" | "exercise" | "quiz" | "flashcard" | "video_link"
content String // markdown content for the lesson
difficulty String @default("medium") // "easy" | "medium" | "hard"
sortOrder Int @default(0)
estimatedMinutes Int? // estimated time to complete
isDemo Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
topic SubjectTopic @relation(fields: [topicId], references: [id], onDelete: Cascade)
questions LessonQuestion[]
@@index([topicId])
@@map("subject_lessons")
}
model LessonQuestion {
id String @id @default(cuid())
lessonId String
questionType String // "multiple_choice" | "true_false" | "fill_blank" | "short_answer" | "matching"
question String // the question text
options String? // JSON array of options for multiple choice
correctAnswer String // the correct answer(s)
explanation String? // why this answer is correct
points Int @default(1)
sortOrder Int @default(0)
isDemo Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lesson SubjectLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
answers StudentAnswer[]
@@index([lessonId])
@@map("lesson_questions")
}
model StudentAnswer {
id String @id @default(cuid())
questionId String
studentId String
answer String // the student's answer
isCorrect Boolean?
timeTakenMs Int? // how long the student took
attempts Int @default(1)
isDemo Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
question LessonQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
student User @relation(fields: [studentId], references: [id])
@@unique([questionId, studentId])
@@index([studentId])
@@map("student_answers")
}