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:
1 parent
74404708e5
commit
849b48a5a4
22 files changed
+6543
-5
No files matched your search
+479
@@ -3067,3 +3067,482 @@ export function unpublishNewsletter(id: string): Promise<NewsletterData> {
|
||||
export function deleteNewsletter(id: string): Promise<{ success: boolean }> {
|
||||
return apiDelete<{ success: boolean }>(`/api/newsletters/${id}`);
|
||||
}
|
||||
|
||||
/* ── Competition System ───────────────────────────────────────────── */
|
||||
|
||||
export interface CompetitionData {
|
||||
id: string;
|
||||
schoolId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
competitionType: string;
|
||||
category: string;
|
||||
subjectId: string | null;
|
||||
status: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
registrationDeadline: string | null;
|
||||
maxParticipants: number | null;
|
||||
scoringType: string;
|
||||
rules: string | null;
|
||||
isPublic: boolean;
|
||||
isDemo: boolean;
|
||||
createdById: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
school?: { id: string; name: string };
|
||||
createdBy?: { id: string; firstName: string; lastName: string };
|
||||
subject?: { id: string; name: string } | null;
|
||||
_count?: { participants: number; rewards: number };
|
||||
}
|
||||
|
||||
export interface CompetitionParticipantData {
|
||||
id: string;
|
||||
competitionId: string;
|
||||
participantType: string;
|
||||
participantId: string;
|
||||
userId: string | null;
|
||||
registeredAt: string;
|
||||
score: number;
|
||||
rank: number | null;
|
||||
isDisqualified: boolean;
|
||||
notes: string | null;
|
||||
isDemo: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CompetitionRewardData {
|
||||
id: string;
|
||||
competitionId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
rewardType: string;
|
||||
rewardValue: string | null;
|
||||
rewardProvider: string | null;
|
||||
rankRequirement: number | null;
|
||||
pointsRequired: number | null;
|
||||
quantity: number;
|
||||
claimedCount: number;
|
||||
imageUrl: string | null;
|
||||
isDemo: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
_count?: { claims: number };
|
||||
}
|
||||
|
||||
export interface CompetitionLeaderboardEntry {
|
||||
id: string;
|
||||
competitionId: string;
|
||||
participantType: string;
|
||||
participantId: string;
|
||||
participantName: string;
|
||||
score: number;
|
||||
rank: number | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface RewardClaimData {
|
||||
id: string;
|
||||
schoolId: string;
|
||||
competitionId: string;
|
||||
rewardId: string;
|
||||
userId: string;
|
||||
claimedAt: string;
|
||||
code: string | null;
|
||||
status: string;
|
||||
expiresAt: string | null;
|
||||
notes: string | null;
|
||||
isDemo: boolean;
|
||||
createdAt: string;
|
||||
user?: { id: string; firstName: string; lastName: string };
|
||||
competition?: { id: string; title: string; competitionType?: string };
|
||||
reward?: { id: string; name: string; rewardType: string; rewardProvider: string | null; imageUrl: string | null };
|
||||
school?: { id: string; name: string };
|
||||
}
|
||||
|
||||
export function fetchCompetitions(
|
||||
schoolId: string,
|
||||
status?: string,
|
||||
competitionType?: string,
|
||||
category?: string,
|
||||
limit?: number,
|
||||
offset?: number
|
||||
): Promise<{ competitions: CompetitionData[]; total: number }> {
|
||||
const params = new URLSearchParams({ schoolId });
|
||||
if (status) params.set('status', status);
|
||||
if (competitionType) params.set('competitionType', competitionType);
|
||||
if (category) params.set('category', category);
|
||||
if (limit) params.set('limit', String(limit));
|
||||
if (offset) params.set('offset', String(offset));
|
||||
return apiGet<{ competitions: CompetitionData[]; total: number }>(`/api/competitions?${params.toString()}`);
|
||||
}
|
||||
|
||||
export function fetchPublicCompetitions(
|
||||
limit?: number,
|
||||
offset?: number
|
||||
): Promise<{ competitions: CompetitionData[]; total: number }> {
|
||||
const params = new URLSearchParams({ isPublic: 'true' });
|
||||
if (limit) params.set('limit', String(limit));
|
||||
if (offset) params.set('offset', String(offset));
|
||||
return apiGet<{ competitions: CompetitionData[]; total: number }>(`/api/competitions?${params.toString()}`);
|
||||
}
|
||||
|
||||
export function fetchCompetition(id: string): Promise<CompetitionData> {
|
||||
return apiGet<CompetitionData>(`/api/competitions/${id}`);
|
||||
}
|
||||
|
||||
export function createCompetition(data: {
|
||||
schoolId: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
competitionType: string;
|
||||
category: string;
|
||||
subjectId?: string | null;
|
||||
status?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
registrationDeadline?: string | null;
|
||||
maxParticipants?: number | null;
|
||||
scoringType?: string;
|
||||
rules?: string | null;
|
||||
isPublic?: boolean;
|
||||
isDemo?: boolean;
|
||||
}): Promise<CompetitionData> {
|
||||
return apiPost<CompetitionData>('/api/competitions', data);
|
||||
}
|
||||
|
||||
export function updateCompetition(id: string, data: Partial<{
|
||||
title: string;
|
||||
description: string | null;
|
||||
competitionType: string;
|
||||
category: string;
|
||||
subjectId: string | null;
|
||||
status: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
registrationDeadline: string | null;
|
||||
maxParticipants: number | null;
|
||||
scoringType: string;
|
||||
rules: string | null;
|
||||
isPublic: boolean;
|
||||
}>): Promise<CompetitionData> {
|
||||
return apiPut<CompetitionData>(`/api/competitions/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteCompetition(id: string): Promise<{ success: boolean }> {
|
||||
return apiDelete<{ success: boolean }>(`/api/competitions/${id}`);
|
||||
}
|
||||
|
||||
export function registerCompetitionParticipant(
|
||||
competitionId: string,
|
||||
data: {
|
||||
participantType: string;
|
||||
participantId: string;
|
||||
userId?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
): Promise<CompetitionParticipantData> {
|
||||
return apiPost<CompetitionParticipantData>(`/api/competitions/${competitionId}`, {
|
||||
action: 'register',
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateCompetitionScore(
|
||||
competitionId: string,
|
||||
data: {
|
||||
participantType: string;
|
||||
participantId: string;
|
||||
score: number;
|
||||
isDisqualified?: boolean;
|
||||
notes?: string | null;
|
||||
}
|
||||
): Promise<CompetitionParticipantData> {
|
||||
return apiPost<CompetitionParticipantData>(`/api/competitions/${competitionId}`, {
|
||||
action: 'update_score',
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchCompetitionLeaderboard(
|
||||
competitionId: string
|
||||
): Promise<{ leaderboard: CompetitionLeaderboardEntry[] }> {
|
||||
return apiGet<{ leaderboard: CompetitionLeaderboardEntry[] }>(`/api/competitions/${competitionId}/leaderboard`);
|
||||
}
|
||||
|
||||
export function fetchCompetitionRewards(
|
||||
competitionId: string
|
||||
): Promise<{ rewards: CompetitionRewardData[] }> {
|
||||
return apiGet<{ rewards: CompetitionRewardData[] }>(`/api/competitions/${competitionId}/rewards`);
|
||||
}
|
||||
|
||||
export function createCompetitionReward(
|
||||
competitionId: string,
|
||||
data: {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
rewardType: string;
|
||||
rewardValue?: string | null;
|
||||
rewardProvider?: string | null;
|
||||
rankRequirement?: number | null;
|
||||
pointsRequired?: number | null;
|
||||
quantity?: number;
|
||||
imageUrl?: string | null;
|
||||
isDemo?: boolean;
|
||||
}
|
||||
): Promise<CompetitionRewardData> {
|
||||
return apiPost<CompetitionRewardData>(`/api/competitions/${competitionId}/rewards`, data);
|
||||
}
|
||||
|
||||
export function fetchRewardClaims(
|
||||
schoolId?: string,
|
||||
competitionId?: string,
|
||||
status?: string
|
||||
): Promise<{ claims: RewardClaimData[] }> {
|
||||
const params = new URLSearchParams();
|
||||
if (schoolId) params.set('schoolId', schoolId);
|
||||
if (competitionId) params.set('competitionId', competitionId);
|
||||
if (status) params.set('status', status);
|
||||
const query = params.toString();
|
||||
return apiGet<{ claims: RewardClaimData[] }>(`/api/reward-claims${query ? `?${query}` : ''}`);
|
||||
}
|
||||
|
||||
export function claimReward(data: {
|
||||
competitionId: string;
|
||||
rewardId: string;
|
||||
code?: string | null;
|
||||
expiresAt?: string | null;
|
||||
notes?: string | null;
|
||||
isDemo?: boolean;
|
||||
}): Promise<RewardClaimData> {
|
||||
return apiPost<RewardClaimData>('/api/reward-claims', data);
|
||||
}
|
||||
|
||||
/* ── Subject Topics (German Curriculum) ─────────────────────────────── */
|
||||
|
||||
export interface SubjectTopicData {
|
||||
id: string;
|
||||
schoolId: string;
|
||||
subjectId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
gradeLevel: string | null;
|
||||
curriculumCode: string | null;
|
||||
sortOrder: number;
|
||||
icon: string | null;
|
||||
color: string;
|
||||
isDemo: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
subject?: { id: string; name: string };
|
||||
_count?: { lessons: number };
|
||||
}
|
||||
|
||||
export function fetchSubjectTopics(params?: {
|
||||
schoolId?: string;
|
||||
subjectId?: string;
|
||||
gradeLevel?: string;
|
||||
curriculumCode?: string;
|
||||
}): Promise<SubjectTopicData[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.schoolId) searchParams.set('schoolId', params.schoolId);
|
||||
if (params?.subjectId) searchParams.set('subjectId', params.subjectId);
|
||||
if (params?.gradeLevel) searchParams.set('gradeLevel', params.gradeLevel);
|
||||
if (params?.curriculumCode) searchParams.set('curriculumCode', params.curriculumCode);
|
||||
return apiGet<SubjectTopicData[]>(`/api/subject-topics?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
export function fetchSubjectTopic(id: string): Promise<SubjectTopicData> {
|
||||
return apiGet<SubjectTopicData>(`/api/subject-topics/${id}`);
|
||||
}
|
||||
|
||||
export function createSubjectTopic(data: {
|
||||
schoolId?: string;
|
||||
subjectId: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
gradeLevel?: string | null;
|
||||
curriculumCode?: string | null;
|
||||
sortOrder?: number;
|
||||
icon?: string | null;
|
||||
color?: string;
|
||||
}): Promise<SubjectTopicData> {
|
||||
return apiPost<SubjectTopicData>('/api/subject-topics', data);
|
||||
}
|
||||
|
||||
export function updateSubjectTopic(id: string, data: Partial<{
|
||||
title: string;
|
||||
description: string | null;
|
||||
gradeLevel: string | null;
|
||||
curriculumCode: string | null;
|
||||
sortOrder: number;
|
||||
icon: string | null;
|
||||
color: string;
|
||||
}>): Promise<SubjectTopicData> {
|
||||
return apiPut<SubjectTopicData>(`/api/subject-topics/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteSubjectTopic(id: string): Promise<{ message: string; id: string }> {
|
||||
return apiDelete<{ message: string; id: string }>(`/api/subject-topics/${id}`);
|
||||
}
|
||||
|
||||
/* ── Subject Lessons (German Curriculum) ────────────────────────────── */
|
||||
|
||||
export interface SubjectLessonData {
|
||||
id: string;
|
||||
topicId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
lessonType: string;
|
||||
content: string;
|
||||
difficulty: string;
|
||||
sortOrder: number;
|
||||
estimatedMinutes: number | null;
|
||||
isDemo: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
_count?: { questions: number };
|
||||
topic?: { id: string; title: string; schoolId: string; subjectId: string };
|
||||
questions?: LessonQuestionData[];
|
||||
}
|
||||
|
||||
export function fetchSubjectLessons(topicId: string, params?: {
|
||||
lessonType?: string;
|
||||
difficulty?: string;
|
||||
}): Promise<SubjectLessonData[]> {
|
||||
const searchParams = new URLSearchParams({ topicId });
|
||||
if (params?.lessonType) searchParams.set('lessonType', params.lessonType);
|
||||
if (params?.difficulty) searchParams.set('difficulty', params.difficulty);
|
||||
return apiGet<SubjectLessonData[]>(`/api/subject-lessons?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
export function fetchSubjectLesson(id: string): Promise<SubjectLessonData> {
|
||||
return apiGet<SubjectLessonData>(`/api/subject-lessons/${id}`);
|
||||
}
|
||||
|
||||
export function createSubjectLesson(data: {
|
||||
topicId: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
lessonType?: 'explanation' | 'exercise' | 'quiz' | 'flashcard' | 'video_link';
|
||||
content: string;
|
||||
difficulty?: 'easy' | 'medium' | 'hard';
|
||||
sortOrder?: number;
|
||||
estimatedMinutes?: number | null;
|
||||
}): Promise<SubjectLessonData> {
|
||||
return apiPost<SubjectLessonData>('/api/subject-lessons', data);
|
||||
}
|
||||
|
||||
export function updateSubjectLesson(id: string, data: Partial<{
|
||||
title: string;
|
||||
description: string | null;
|
||||
lessonType: 'explanation' | 'exercise' | 'quiz' | 'flashcard' | 'video_link';
|
||||
content: string;
|
||||
difficulty: 'easy' | 'medium' | 'hard';
|
||||
sortOrder: number;
|
||||
estimatedMinutes: number | null;
|
||||
}>): Promise<SubjectLessonData> {
|
||||
return apiPut<SubjectLessonData>(`/api/subject-lessons/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteSubjectLesson(id: string): Promise<{ message: string; id: string }> {
|
||||
return apiDelete<{ message: string; id: string }>(`/api/subject-lessons/${id}`);
|
||||
}
|
||||
|
||||
/* ── Lesson Questions (German Curriculum) ───────────────────────────── */
|
||||
|
||||
export interface LessonQuestionData {
|
||||
id: string;
|
||||
lessonId: string;
|
||||
questionType: string;
|
||||
question: string;
|
||||
options: string | null;
|
||||
correctAnswer: string;
|
||||
explanation: string | null;
|
||||
points: number;
|
||||
sortOrder: number;
|
||||
isDemo: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export function fetchLessonQuestions(lessonId: string, params?: {
|
||||
questionType?: string;
|
||||
}): Promise<LessonQuestionData[]> {
|
||||
const searchParams = new URLSearchParams({ lessonId });
|
||||
if (params?.questionType) searchParams.set('questionType', params.questionType);
|
||||
return apiGet<LessonQuestionData[]>(`/api/lesson-questions?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
export function fetchLessonQuestion(id: string): Promise<LessonQuestionData> {
|
||||
return apiGet<LessonQuestionData>(`/api/lesson-questions/${id}`);
|
||||
}
|
||||
|
||||
export function createLessonQuestion(data: {
|
||||
lessonId: string;
|
||||
questionType: 'multiple_choice' | 'true_false' | 'fill_blank' | 'short_answer' | 'matching';
|
||||
question: string;
|
||||
options?: string | null;
|
||||
correctAnswer: string;
|
||||
explanation?: string | null;
|
||||
points?: number;
|
||||
sortOrder?: number;
|
||||
}): Promise<LessonQuestionData> {
|
||||
return apiPost<LessonQuestionData>('/api/lesson-questions', data);
|
||||
}
|
||||
|
||||
export function updateLessonQuestion(id: string, data: Partial<{
|
||||
questionType: 'multiple_choice' | 'true_false' | 'fill_blank' | 'short_answer' | 'matching';
|
||||
question: string;
|
||||
options: string | null;
|
||||
correctAnswer: string;
|
||||
explanation: string | null;
|
||||
points: number;
|
||||
sortOrder: number;
|
||||
}>): Promise<LessonQuestionData> {
|
||||
return apiPut<LessonQuestionData>(`/api/lesson-questions/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteLessonQuestion(id: string): Promise<{ message: string; id: string }> {
|
||||
return apiDelete<{ message: string; id: string }>(`/api/lesson-questions/${id}`);
|
||||
}
|
||||
|
||||
/* ── Student Answers (German Curriculum) ────────────────────────────── */
|
||||
|
||||
export interface StudentAnswerData {
|
||||
id: string;
|
||||
questionId: string;
|
||||
studentId: string;
|
||||
answer: string;
|
||||
isCorrect: boolean | null;
|
||||
timeTakenMs: number | null;
|
||||
attempts: number;
|
||||
isDemo: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
correctAnswer?: string;
|
||||
explanation?: string | null;
|
||||
question?: LessonQuestionData;
|
||||
}
|
||||
|
||||
export function fetchStudentAnswers(params?: {
|
||||
questionId?: string;
|
||||
lessonId?: string;
|
||||
}): Promise<StudentAnswerData[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.questionId) searchParams.set('questionId', params.questionId);
|
||||
if (params?.lessonId) searchParams.set('lessonId', params.lessonId);
|
||||
return apiGet<StudentAnswerData[]>(`/api/student-answers?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
export function submitStudentAnswer(data: {
|
||||
questionId: string;
|
||||
answer: string;
|
||||
timeTakenMs?: number | null;
|
||||
}): Promise<StudentAnswerData> {
|
||||
return apiPost<StudentAnswerData>('/api/student-answers', data);
|
||||
}
|
||||
+286
@@ -2080,6 +2080,51 @@ const de: TranslationDict = {
|
||||
'notebooks.tab_all': 'Meine Hefte',
|
||||
'notebooks.owner': 'Von',
|
||||
|
||||
// ─── Learning Content (German Curriculum) ──────────────────────────
|
||||
'notebooks.learn': 'Lernen',
|
||||
'notebooks.topics': 'Themen',
|
||||
'notebooks.lessons': 'Lektionen',
|
||||
'notebooks.exercises': 'Uebungen',
|
||||
'notebooks.quiz': 'Quiz',
|
||||
'notebooks.flashcards': 'Karteikarten',
|
||||
'notebooks.progress': 'Fortschritt',
|
||||
'notebooks.grade_level': 'Klassenstufe',
|
||||
'notebooks.curriculum': 'Lehrplan',
|
||||
'notebooks.subject_topic': 'Thema',
|
||||
'notebooks.difficulty': 'Schwierigkeitsgrad',
|
||||
'notebooks.easy': 'Leicht',
|
||||
'notebooks.medium': 'Mittel',
|
||||
'notebooks.hard': 'Schwer',
|
||||
'notebooks.question': 'Frage',
|
||||
'notebooks.answer': 'Antwort',
|
||||
'notebooks.correct': 'Richtig',
|
||||
'notebooks.incorrect': 'Falsch',
|
||||
'notebooks.explanation': 'Erklaerung',
|
||||
'notebooks.try_again': 'Nochmal versuchen',
|
||||
'notebooks.next_question': 'Naechste Frage',
|
||||
'notebooks.score': 'Punktzahl',
|
||||
'notebooks.time_taken': 'Benoetigte Zeit',
|
||||
'notebooks.mastery': 'Beherrschung',
|
||||
'notebooks.practice_more': 'Mehr ueben',
|
||||
'notebooks.well_done': 'Gut gemacht!',
|
||||
'notebooks.keep_practicing': 'Weiter ueben!',
|
||||
'notebooks.subject_math': 'Mathematik',
|
||||
'notebooks.subject_german': 'Deutsch',
|
||||
'notebooks.subject_science': 'Sachkunde',
|
||||
'notebooks.subject_english': 'Englisch',
|
||||
'notebooks.subject_music': 'Musik',
|
||||
'notebooks.subject_art': 'Kunst',
|
||||
'notebooks.subject_religion': 'Religion/Ethik',
|
||||
'notebooks.subject_pe': 'Sport',
|
||||
'notebooks.papier_sparen': 'Papier sparen, Umwelt schuetzen',
|
||||
'notebooks.digital_instead_paper': 'Digitale Hefte statt Papierhefte',
|
||||
'notebooks.environment_tip': 'Mit digitalen Heften schuetzt du die Umwelt — kein Papierverschwendung!',
|
||||
'notebooks.type_lined': 'Liniert',
|
||||
'notebooks.type_grid': 'Kariert',
|
||||
'notebooks.type_blank': 'Unliniert',
|
||||
'notebooks.type_dotted': 'Punktiert',
|
||||
'notebooks.type_music': 'Notenlinien',
|
||||
|
||||
// ─── Enhanced Notifications ──────────────────────────────────────
|
||||
'notifications.today': 'Heute',
|
||||
'notifications.yesterday': 'Gestern',
|
||||
@@ -2123,6 +2168,19 @@ const de: TranslationDict = {
|
||||
'student.view_all': 'Alle anzeigen',
|
||||
'student.welcome_back': 'Willkommen zurueck',
|
||||
'student.progress_overview': 'Dein Lernfortschritt',
|
||||
'student.papier_sparen_title': 'Papier sparen, Umwelt schuetzen',
|
||||
'student.papier_sparen_subtitle': 'Digitale Hefte statt Papierhefte — Mit digitalen Heften schuetzt du die Umwelt!',
|
||||
'student.go_to_notebooks': 'Zu meinen Heften',
|
||||
'student.my_notebooks_quick': 'Meine Hefte',
|
||||
'student.show_all_notebooks': 'Alle Hefte anzeigen',
|
||||
'student.start_learning': 'Lernen starten',
|
||||
'student.learning_progress': 'Lernfortschritt',
|
||||
'student.continue_learning': 'Weiter lernen',
|
||||
'student.subject_progress': 'Fachfortschritt',
|
||||
'student.notebook_math': 'Mathematik',
|
||||
'student.notebook_german': 'Deutsch',
|
||||
'student.notebook_english': 'Englisch',
|
||||
'student.notebook_science': 'Naturwissenschaften',
|
||||
'nav.student_competencies': 'Meine Kompetenzen',
|
||||
'nav.student_grades': 'Meine Noten',
|
||||
'nav.student_notebooks': 'Meine Hefte',
|
||||
@@ -2593,6 +2651,7 @@ const de: TranslationDict = {
|
||||
'nav.portfolio': 'Portfolio',
|
||||
'nav.timetable': 'Stundenplan',
|
||||
'nav.resources': 'Mediathek',
|
||||
'nav.competitions': 'Wettbewerbe',
|
||||
|
||||
// Navigation - Homework
|
||||
'nav.homework': 'Hausaufgaben',
|
||||
@@ -3266,6 +3325,90 @@ const de: TranslationDict = {
|
||||
'avatar.change': 'Foto ändern',
|
||||
'avatar.remove': 'Foto entfernen',
|
||||
|
||||
// Competition System
|
||||
'competition.title': 'Wettbewerbe',
|
||||
'competition.create': 'Wettbewerb erstellen',
|
||||
'competition.edit': 'Wettbewerb bearbeiten',
|
||||
'competition.delete': 'Wettbewerb löschen',
|
||||
'competition.manage': 'Wettbewerbe verwalten',
|
||||
'competition.details': 'Wettbewerbsdetails',
|
||||
'competition.name': 'Wettbewerb',
|
||||
'competition.description': 'Beschreibung',
|
||||
'competition.type': 'Typ',
|
||||
'competition.category': 'Kategorie',
|
||||
'competition.status': 'Status',
|
||||
'competition.start_date': 'Startdatum',
|
||||
'competition.end_date': 'Enddatum',
|
||||
'competition.registration_deadline': 'Anmeldungsfrist',
|
||||
'competition.max_participants': 'Maximale Teilnehmer',
|
||||
'competition.scoring_type': 'Wertung',
|
||||
'competition.rules': 'Regeln',
|
||||
'competition.is_public': 'Öffentlich sichtbar',
|
||||
'competition.subject': 'Fach',
|
||||
'competition.created_by': 'Erstellt von',
|
||||
'competition.no_competitions': 'Keine Wettbewerbe vorhanden',
|
||||
'competition.type.class': 'Klassenwettbewerb',
|
||||
'competition.type.inter_class': 'Klassenübergreifend',
|
||||
'competition.type.inter_school': 'Schulübergreifend',
|
||||
'competition.category.academic': 'Akademisch',
|
||||
'competition.category.sports': 'Sport',
|
||||
'competition.category.creativity': 'Kreativität',
|
||||
'competition.category.citizenship': 'Gemeinschaft',
|
||||
'competition.category.digital': 'Digital',
|
||||
'competition.category.reading': 'Lesen',
|
||||
'competition.category.stem': 'MINT',
|
||||
'competition.category.other': 'Sonstiges',
|
||||
'competition.status.draft': 'Entwurf',
|
||||
'competition.status.registration': 'Anmeldung',
|
||||
'competition.status.active': 'Aktiv',
|
||||
'competition.status.completed': 'Abgeschlossen',
|
||||
'competition.status.cancelled': 'Abgesagt',
|
||||
'competition.scoring.points': 'Punkte',
|
||||
'competition.scoring.rank': 'Platzierung',
|
||||
'competition.scoring.time': 'Zeit',
|
||||
'competition.scoring.badge_count': 'Abzeichenanzahl',
|
||||
'competition.register': 'Anmelden',
|
||||
'competition.register_participant': 'Teilnehmer anmelden',
|
||||
'competition.update_score': 'Punkte aktualisieren',
|
||||
'competition.leaderboard': 'Bestenliste',
|
||||
'competition.participants': 'Teilnehmer',
|
||||
'competition.rewards': 'Preise',
|
||||
'competition.reward.create': 'Preis hinzufügen',
|
||||
'competition.reward.type': 'Preisart',
|
||||
'competition.reward.type.digital_code': 'Digitaler Code',
|
||||
'competition.reward.type.badge': 'Abzeichen',
|
||||
'competition.reward.type.certificate': 'Zertifikat',
|
||||
'competition.reward.type.experience': 'Erlebnis',
|
||||
'competition.reward.type.physical': 'Physischer Preis',
|
||||
'competition.reward.provider': 'Anbieter',
|
||||
'competition.reward.provider.netflix': 'Netflix',
|
||||
'competition.reward.provider.amazon': 'Amazon',
|
||||
'competition.reward.provider.cinema': 'Kino',
|
||||
'competition.reward.provider.theater': 'Theater',
|
||||
'competition.reward.provider.concert': 'Konzert',
|
||||
'competition.reward.provider.custom': 'Sonstige',
|
||||
'competition.reward.quantity': 'Menge',
|
||||
'competition.reward.claimed': 'Eingelöst',
|
||||
'competition.reward.available': 'Verfügbar',
|
||||
'competition.reward.rank_requirement': 'Mindestplatzierung',
|
||||
'competition.reward.points_required': 'Mindestpunkte',
|
||||
'competition.reward.image': 'Preisbild',
|
||||
'competition.reward.code': 'Code',
|
||||
'competition.reward.claim': 'Einlösen',
|
||||
'competition.reward.claimed_at': 'Eingelöst am',
|
||||
'competition.reward.expires': 'Läuft ab am',
|
||||
'competition.reward.status': 'Status',
|
||||
'competition.reward.status.pending': 'Ausstehend',
|
||||
'competition.reward.status.claimed': 'Eingelöst',
|
||||
'competition.reward.status.expired': 'Abgelaufen',
|
||||
'competition.reward.status.revoked': 'Widerrufen',
|
||||
'competition.reward.my_claims': 'Meine Preise',
|
||||
'competition.reward.already_claimed': 'Preis bereits eingelöst',
|
||||
'competition.reward.not_available': 'Preis nicht mehr verfügbar',
|
||||
'competition.reward.requirement_not_met': 'Voraussetzung nicht erfüllt',
|
||||
'competition.dsgvo_notice': 'Hinweis: Personenbezogene Daten werden gemäß DSGVO verarbeitet.',
|
||||
'competition.jugendschutz_notice': 'Hinweis: Dieser Wettbewerb entspricht den Anforderungen des Jugendschutzgesetzes.',
|
||||
|
||||
// German Grade Labels
|
||||
'grading.sehr_gut': 'Sehr gut',
|
||||
'grading.gut': 'Gut',
|
||||
@@ -5352,6 +5495,51 @@ const en: TranslationDict = {
|
||||
'notebooks.tab_all': 'My Notebooks',
|
||||
'notebooks.owner': 'By',
|
||||
|
||||
// ─── Learning Content (German Curriculum) ──────────────────────────
|
||||
'notebooks.learn': 'Learn',
|
||||
'notebooks.topics': 'Topics',
|
||||
'notebooks.lessons': 'Lessons',
|
||||
'notebooks.exercises': 'Exercises',
|
||||
'notebooks.quiz': 'Quiz',
|
||||
'notebooks.flashcards': 'Flashcards',
|
||||
'notebooks.progress': 'Progress',
|
||||
'notebooks.grade_level': 'Grade Level',
|
||||
'notebooks.curriculum': 'Curriculum',
|
||||
'notebooks.subject_topic': 'Topic',
|
||||
'notebooks.difficulty': 'Difficulty',
|
||||
'notebooks.easy': 'Easy',
|
||||
'notebooks.medium': 'Medium',
|
||||
'notebooks.hard': 'Hard',
|
||||
'notebooks.question': 'Question',
|
||||
'notebooks.answer': 'Answer',
|
||||
'notebooks.correct': 'Correct',
|
||||
'notebooks.incorrect': 'Incorrect',
|
||||
'notebooks.explanation': 'Explanation',
|
||||
'notebooks.try_again': 'Try Again',
|
||||
'notebooks.next_question': 'Next Question',
|
||||
'notebooks.score': 'Score',
|
||||
'notebooks.time_taken': 'Time Taken',
|
||||
'notebooks.mastery': 'Mastery',
|
||||
'notebooks.practice_more': 'Practice More',
|
||||
'notebooks.well_done': 'Well Done!',
|
||||
'notebooks.keep_practicing': 'Keep Practicing!',
|
||||
'notebooks.subject_math': 'Mathematics',
|
||||
'notebooks.subject_german': 'German',
|
||||
'notebooks.subject_science': 'General Studies',
|
||||
'notebooks.subject_english': 'English',
|
||||
'notebooks.subject_music': 'Music',
|
||||
'notebooks.subject_art': 'Art',
|
||||
'notebooks.subject_religion': 'Religion/Ethics',
|
||||
'notebooks.subject_pe': 'Physical Education',
|
||||
'notebooks.papier_sparen': 'Save Paper, Protect the Environment',
|
||||
'notebooks.digital_instead_paper': 'Digital Notebooks Instead of Paper',
|
||||
'notebooks.environment_tip': 'With digital notebooks you protect the environment — no paper waste!',
|
||||
'notebooks.type_lined': 'Lined',
|
||||
'notebooks.type_grid': 'Grid',
|
||||
'notebooks.type_blank': 'Blank',
|
||||
'notebooks.type_dotted': 'Dotted',
|
||||
'notebooks.type_music': 'Music Lines',
|
||||
|
||||
// ─── Enhanced Notifications ──────────────────────────────────────
|
||||
'notifications.today': 'Today',
|
||||
'notifications.yesterday': 'Yesterday',
|
||||
@@ -5395,6 +5583,19 @@ const en: TranslationDict = {
|
||||
'student.view_all': 'View all',
|
||||
'student.welcome_back': 'Welcome back',
|
||||
'student.progress_overview': 'Your Learning Progress',
|
||||
'student.papier_sparen_title': 'Save Paper, Protect the Environment',
|
||||
'student.papier_sparen_subtitle': 'Digital notebooks instead of paper — With digital notebooks you protect the environment!',
|
||||
'student.go_to_notebooks': 'Go to my Notebooks',
|
||||
'student.my_notebooks_quick': 'My Notebooks',
|
||||
'student.show_all_notebooks': 'Show all notebooks',
|
||||
'student.start_learning': 'Start learning',
|
||||
'student.learning_progress': 'Learning Progress',
|
||||
'student.continue_learning': 'Continue learning',
|
||||
'student.subject_progress': 'Subject Progress',
|
||||
'student.notebook_math': 'Mathematics',
|
||||
'student.notebook_german': 'German',
|
||||
'student.notebook_english': 'English',
|
||||
'student.notebook_science': 'Sciences',
|
||||
'nav.student_competencies': 'My Competencies',
|
||||
'nav.student_grades': 'My Grades',
|
||||
'nav.student_notebooks': 'My Notebooks',
|
||||
@@ -5865,6 +6066,7 @@ const en: TranslationDict = {
|
||||
'nav.portfolio': 'Portfolio',
|
||||
'nav.timetable': 'Timetable',
|
||||
'nav.resources': 'Resources',
|
||||
'nav.competitions': 'Competitions',
|
||||
|
||||
// Navigation - Homework
|
||||
'nav.homework': 'Homework',
|
||||
@@ -6538,6 +6740,90 @@ const en: TranslationDict = {
|
||||
'avatar.change': 'Change Photo',
|
||||
'avatar.remove': 'Remove Photo',
|
||||
|
||||
// Competition System
|
||||
'competition.title': 'Competitions',
|
||||
'competition.create': 'Create Competition',
|
||||
'competition.edit': 'Edit Competition',
|
||||
'competition.delete': 'Delete Competition',
|
||||
'competition.manage': 'Manage Competitions',
|
||||
'competition.details': 'Competition Details',
|
||||
'competition.name': 'Competition',
|
||||
'competition.description': 'Description',
|
||||
'competition.type': 'Type',
|
||||
'competition.category': 'Category',
|
||||
'competition.status': 'Status',
|
||||
'competition.start_date': 'Start Date',
|
||||
'competition.end_date': 'End Date',
|
||||
'competition.registration_deadline': 'Registration Deadline',
|
||||
'competition.max_participants': 'Max Participants',
|
||||
'competition.scoring_type': 'Scoring',
|
||||
'competition.rules': 'Rules',
|
||||
'competition.is_public': 'Publicly Visible',
|
||||
'competition.subject': 'Subject',
|
||||
'competition.created_by': 'Created by',
|
||||
'competition.no_competitions': 'No competitions available',
|
||||
'competition.type.class': 'Class Competition',
|
||||
'competition.type.inter_class': 'Inter-Class',
|
||||
'competition.type.inter_school': 'Inter-School',
|
||||
'competition.category.academic': 'Academic',
|
||||
'competition.category.sports': 'Sports',
|
||||
'competition.category.creativity': 'Creativity',
|
||||
'competition.category.citizenship': 'Citizenship',
|
||||
'competition.category.digital': 'Digital',
|
||||
'competition.category.reading': 'Reading',
|
||||
'competition.category.stem': 'STEM',
|
||||
'competition.category.other': 'Other',
|
||||
'competition.status.draft': 'Draft',
|
||||
'competition.status.registration': 'Registration',
|
||||
'competition.status.active': 'Active',
|
||||
'competition.status.completed': 'Completed',
|
||||
'competition.status.cancelled': 'Cancelled',
|
||||
'competition.scoring.points': 'Points',
|
||||
'competition.scoring.rank': 'Ranking',
|
||||
'competition.scoring.time': 'Time',
|
||||
'competition.scoring.badge_count': 'Badge Count',
|
||||
'competition.register': 'Register',
|
||||
'competition.register_participant': 'Register Participant',
|
||||
'competition.update_score': 'Update Score',
|
||||
'competition.leaderboard': 'Leaderboard',
|
||||
'competition.participants': 'Participants',
|
||||
'competition.rewards': 'Rewards',
|
||||
'competition.reward.create': 'Add Reward',
|
||||
'competition.reward.type': 'Reward Type',
|
||||
'competition.reward.type.digital_code': 'Digital Code',
|
||||
'competition.reward.type.badge': 'Badge',
|
||||
'competition.reward.type.certificate': 'Certificate',
|
||||
'competition.reward.type.experience': 'Experience',
|
||||
'competition.reward.type.physical': 'Physical Reward',
|
||||
'competition.reward.provider': 'Provider',
|
||||
'competition.reward.provider.netflix': 'Netflix',
|
||||
'competition.reward.provider.amazon': 'Amazon',
|
||||
'competition.reward.provider.cinema': 'Cinema',
|
||||
'competition.reward.provider.theater': 'Theater',
|
||||
'competition.reward.provider.concert': 'Concert',
|
||||
'competition.reward.provider.custom': 'Custom',
|
||||
'competition.reward.quantity': 'Quantity',
|
||||
'competition.reward.claimed': 'Claimed',
|
||||
'competition.reward.available': 'Available',
|
||||
'competition.reward.rank_requirement': 'Minimum Rank',
|
||||
'competition.reward.points_required': 'Minimum Points',
|
||||
'competition.reward.image': 'Reward Image',
|
||||
'competition.reward.code': 'Code',
|
||||
'competition.reward.claim': 'Claim',
|
||||
'competition.reward.claimed_at': 'Claimed on',
|
||||
'competition.reward.expires': 'Expires on',
|
||||
'competition.reward.status': 'Status',
|
||||
'competition.reward.status.pending': 'Pending',
|
||||
'competition.reward.status.claimed': 'Claimed',
|
||||
'competition.reward.status.expired': 'Expired',
|
||||
'competition.reward.status.revoked': 'Revoked',
|
||||
'competition.reward.my_claims': 'My Rewards',
|
||||
'competition.reward.already_claimed': 'Reward already claimed',
|
||||
'competition.reward.not_available': 'Reward no longer available',
|
||||
'competition.reward.requirement_not_met': 'Requirement not met',
|
||||
'competition.dsgvo_notice': 'Notice: Personal data is processed in accordance with GDPR.',
|
||||
'competition.jugendschutz_notice': 'Notice: This competition complies with the Youth Protection Act.',
|
||||
|
||||
// German Grade Labels
|
||||
'grading.sehr_gut': 'Sehr gut',
|
||||
'grading.gut': 'Gut',
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@ export type ViewName =
|
||||
| 'homework'
|
||||
| 'portfolio'
|
||||
| 'timetable'
|
||||
| 'resources';
|
||||
| 'resources'
|
||||
| 'competitions';
|
||||
|
||||
export interface CurrentUser {
|
||||
id: string;
|
||||
|
||||
Reference in new issue
Block a user