diff --git a/src/app/[locale]/courses/[share_token]/results/[attemptId]/page.tsx b/src/app/[locale]/results/[courseId]/exam/[attemptId]/page.tsx similarity index 68% rename from src/app/[locale]/courses/[share_token]/results/[attemptId]/page.tsx rename to src/app/[locale]/results/[courseId]/exam/[attemptId]/page.tsx index 34e639fa5..93e0e29b0 100644 --- a/src/app/[locale]/courses/[share_token]/results/[attemptId]/page.tsx +++ b/src/app/[locale]/results/[courseId]/exam/[attemptId]/page.tsx @@ -1,20 +1,21 @@ import { forbidden, notFound } from 'next/navigation' -import { getCourseByShareToken } from '@/database/course/select' +import { getCourseById } from '@/database/course/select' import { getExaminationAttemptById } from '@/database/examination/select' import { QuestionScoresLineChart } from '@/src/components/charts/QuestionScoresLineChart' +import ExamQuestionResultTable from '@/src/components/results/examination/ExamQuestionResultTable' +import { ExamResultAttemptResultsBreadcrumbs } from '@/src/components/results/examination/ExamResultsBreadcrumbs' import PageHeading from '@/src/components/Shared/PageHeading' import { getScopedI18n } from '@/src/i18n/server-localization' import requireAuthentication from '@/src/lib/auth/requireAuthentication' import hasCollaborativePermissions from '@/src/lib/courses/hasCollaborativePermissions' import getDummyExamAttempts from '@/src/lib/dummy/getDummyExamAttempts' -import ExamQuestionResultTable from '../../../../../../components/courses/[share_token]/results/ExamQuestionResultTable' -export default async function ExamAttemptResultPage({ params }: { params: Promise<{ share_token: string; attemptId: string }> }) { +export default async function ExamAttemptResultPage({ params }: { params: Promise<{ courseId: string; attemptId: string }> }) { const t = await getScopedI18n('Courses.ExaminatonResults.ExamAttemptResultPage') - const { share_token, attemptId } = await params + const { courseId, attemptId } = await params const { user } = await requireAuthentication() - let [course, attempt] = await Promise.all([getCourseByShareToken(share_token), getExaminationAttemptById(parseInt(attemptId))]) + let [course, attempt] = await Promise.all([getCourseById(courseId), getExaminationAttemptById(parseInt(attemptId))]) // dummy examinationAttempt data when a given attempt does not exist if (!attempt) attempt = (await getDummyExamAttempts(1))[0] @@ -24,6 +25,7 @@ export default async function ExamAttemptResultPage({ params }: { params: Promis return ( <> +
diff --git a/src/app/[locale]/courses/[share_token]/results/page.tsx b/src/app/[locale]/results/[courseId]/exam/page.tsx similarity index 93% rename from src/app/[locale]/courses/[share_token]/results/page.tsx rename to src/app/[locale]/results/[courseId]/exam/page.tsx index dc27ba454..b30508b93 100644 --- a/src/app/[locale]/courses/[share_token]/results/page.tsx +++ b/src/app/[locale]/results/[courseId]/exam/page.tsx @@ -1,13 +1,14 @@ 'use server' import { forbidden, notFound } from 'next/navigation' -import { getCourseByShareToken } from '@/database/course/select' +import { getCourseById } from '@/database/course/select' import { getCourseExaminationAttempts } from '@/database/examination/select' import { ExaminationSuccessPieChart } from '@/src/components/charts/ExaminationSuccessPieChart' import { ExamQuestionDurationChart } from '@/src/components/charts/QuestionDurationChart' import { QuestionScoresLineChartCard } from '@/src/components/charts/QuestionScoresLineChart' import { UserTypePieChart } from '@/src/components/charts/UserTypePieChart' import { ExaminationAttemptTable } from '@/src/components/courses/[share_token]/ExaminationAttemptTable' +import { ExamResultsBreadcrumbs } from '@/src/components/results/examination/ExamResultsBreadcrumbs' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/shadcn/card' import PageHeading from '@/src/components/Shared/PageHeading' import { getScopedI18n } from '@/src/i18n/server-localization' @@ -24,11 +25,11 @@ export type ExaminationAttemptTableStructure = Pick }) { - const { share_token } = await params +export default async function ExaminationResultsPage({ params }: { params: Promise<{ courseId: string }> }) { + const { courseId } = await params const { user } = await requireAuthentication() - const course = await getCourseByShareToken(share_token) + const course = await getCourseById(courseId) if (!course) notFound() if (!hasCollaborativePermissions(course, user.id)) forbidden() @@ -38,6 +39,7 @@ export default async function ExaminationResultsPage({ params }: { params: Promi return ( <> +
diff --git a/src/app/[locale]/results/[courseId]/practice/page.tsx b/src/app/[locale]/results/[courseId]/practice/page.tsx index 2560ca55b..2b554e006 100644 --- a/src/app/[locale]/results/[courseId]/practice/page.tsx +++ b/src/app/[locale]/results/[courseId]/practice/page.tsx @@ -3,7 +3,7 @@ import { getCourseById } from '@/database/course/select' import { QuestionCorrectnessPieChart } from '@/src/components/charts/QuestionCorrectnessPieChart' import { ExamQuestionDurationChart } from '@/src/components/charts/QuestionDurationChart' import { QuestionScorePlotCard } from '@/src/components/charts/QuestionScorePlot' -import ExamQuestionResultTable from '@/src/components/courses/[share_token]/results/ExamQuestionResultTable' +import ExamQuestionResultTable from '@/src/components/results/examination/ExamQuestionResultTable' import { PracticeResultsBreadcrumbs } from '@/src/components/results/practice/PracticeResultsBreadcrumbs' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/shadcn/card' import PageHeading from '@/src/components/Shared/PageHeading' diff --git a/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx b/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx index 6af272500..467ae6961 100644 --- a/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx +++ b/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { ArrowUpRightIcon, CopyPlusIcon, EllipsisIcon, Share2Icon, SquarePenIcon, TrashIcon } from 'lucide-react' +import { ArrowUpRightIcon, ChartSplineIcon, CopyPlusIcon, EllipsisIcon, Share2Icon, SquarePenIcon, TrashIcon } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { toast } from 'react-toastify' @@ -142,6 +142,13 @@ export default function CourseActionMenu({ id, questions, share_key, owner_id, c + + + + {t('show_exam_results.label')} + + + {t('clone_course.label')} diff --git a/src/components/courses/[share_token]/ExaminationAttemptTable.tsx b/src/components/courses/[share_token]/ExaminationAttemptTable.tsx index c5b979e0a..3c9a96e51 100644 --- a/src/components/courses/[share_token]/ExaminationAttemptTable.tsx +++ b/src/components/courses/[share_token]/ExaminationAttemptTable.tsx @@ -157,7 +157,7 @@ export function ExaminationAttemptTable({ data: initialData }: { data: ExamAttem - + {t('columns.actions_menu.show_results_label')} diff --git a/src/components/courses/[share_token]/results/ExamQuestionResultTable.tsx b/src/components/results/examination/ExamQuestionResultTable.tsx similarity index 96% rename from src/components/courses/[share_token]/results/ExamQuestionResultTable.tsx rename to src/components/results/examination/ExamQuestionResultTable.tsx index 9c24f0794..26c73f3f6 100644 --- a/src/components/courses/[share_token]/results/ExamQuestionResultTable.tsx +++ b/src/components/results/examination/ExamQuestionResultTable.tsx @@ -3,8 +3,8 @@ import { IconCircleCheckFilled } from '@tabler/icons-react' import { ColumnDef } from '@tanstack/react-table' import { EyeIcon, XIcon } from 'lucide-react' -import ExamQuestionTable_ActionMenu from '@/src/components/courses/[share_token]/results/ExamQuestionTable_ActionMenu' -import ShowAnswerDrawer_TableCell from '@/src/components/courses/[share_token]/results/PreviewQuestionResultDrawer' +import ExamQuestionTable_ActionMenu from '@/src/components/results/examination/ExamQuestionTable_ActionMenu' +import ShowAnswerDrawer_TableCell from '@/src/components/results/examination/PreviewQuestionResultDrawer' import { Badge } from '@/src/components/shadcn/badge' import { Button } from '@/src/components/shadcn/button' import { Checkbox } from '@/src/components/shadcn/checkbox' diff --git a/src/components/courses/[share_token]/results/ExamQuestionTable_ActionMenu.tsx b/src/components/results/examination/ExamQuestionTable_ActionMenu.tsx similarity index 91% rename from src/components/courses/[share_token]/results/ExamQuestionTable_ActionMenu.tsx rename to src/components/results/examination/ExamQuestionTable_ActionMenu.tsx index 08b3b56ce..458cf2e38 100644 --- a/src/components/courses/[share_token]/results/ExamQuestionTable_ActionMenu.tsx +++ b/src/components/results/examination/ExamQuestionTable_ActionMenu.tsx @@ -2,8 +2,8 @@ import { useState } from 'react' import { IconDotsVertical } from '@tabler/icons-react' import { Row } from '@tanstack/react-table' import { ChartColumnIcon, TrashIcon } from 'lucide-react' -import { PreviewQuestionResult_QuestionItem } from '@/src/components/courses/[share_token]/results/ExamQuestionResultTable' -import ShowAnswerDrawer_TableCell from '@/src/components/courses/[share_token]/results/PreviewQuestionResultDrawer' +import { PreviewQuestionResult_QuestionItem } from '@/src/components/results/examination/ExamQuestionResultTable' +import ShowAnswerDrawer_TableCell from '@/src/components/results/examination/PreviewQuestionResultDrawer' import { Button } from '@/src/components/shadcn/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/src/components/shadcn/dropdown-menu' import { useCurrentLocale, useScopedI18n } from '@/src/i18n/client-localization' diff --git a/src/components/results/examination/ExamResultsBreadcrumbs.tsx b/src/components/results/examination/ExamResultsBreadcrumbs.tsx new file mode 100644 index 000000000..6462d0b9e --- /dev/null +++ b/src/components/results/examination/ExamResultsBreadcrumbs.tsx @@ -0,0 +1,79 @@ +'use server' + +import { DetailedHTMLProps, HTMLAttributes } from 'react' +import Link from 'next/link' +import { getCourseById } from '@/database/course/select' +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/src/components/shadcn/breadcrumb' +import { getScopedI18n } from '@/src/i18n/server-localization' +import { Course } from '@/src/schemas/CourseSchema' + +async function BaseBreadcrumbs({ + courseName, + courseId, + children, + ...props +}: { children: React.ReactNode; courseId: Course['id']; courseName: Course['name'] } & DetailedHTMLProps, HTMLElement>) { + const t = await getScopedI18n('Shared.Breadcrumbs') + return ( + + + + + {t('root')} + + + + + + {t('courses')} + + + + + + {courseName} + + + + {children} + + + ) +} + +export async function ExamResultsBreadcrumbs({ courseId, ...props }: { courseId: string } & DetailedHTMLProps, HTMLElement>) { + const t = await getScopedI18n('Shared.Breadcrumbs') + const course = await getCourseById(courseId) + if (!course) return null + + return ( + + + {t('examination_results')} + + + ) +} + +export async function ExamResultAttemptResultsBreadcrumbs({ courseId, userName, ...props }: { courseId: string; userName: string } & DetailedHTMLProps, HTMLElement>) { + const t = await getScopedI18n('Shared.Breadcrumbs') + const course = await getCourseById(courseId) + if (!course) return null + + return ( + + + + {t('examination_results')} + + + + + + + {userName}'s {t('results')} + + + + ) +} diff --git a/src/components/courses/[share_token]/results/PreviewQuestionResultDrawer.tsx b/src/components/results/examination/PreviewQuestionResultDrawer.tsx similarity index 99% rename from src/components/courses/[share_token]/results/PreviewQuestionResultDrawer.tsx rename to src/components/results/examination/PreviewQuestionResultDrawer.tsx index 496169df4..da8d351ca 100644 --- a/src/components/courses/[share_token]/results/PreviewQuestionResultDrawer.tsx +++ b/src/components/results/examination/PreviewQuestionResultDrawer.tsx @@ -3,7 +3,7 @@ import React, { Dispatch, SetStateAction, useState } from 'react' import { ChevronRightIcon, UserCheckIcon, UserIcon } from 'lucide-react' import { QuestionScoresLineChart } from '@/src/components/charts/QuestionScoresLineChart' -import { PreviewQuestionResult_QuestionItem } from '@/src/components/courses/[share_token]/results/ExamQuestionResultTable' +import { PreviewQuestionResult_QuestionItem } from '@/src/components/results/examination/ExamQuestionResultTable' import { Button } from '@/src/components/shadcn/button' import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/src/components/shadcn/drawer' import { Input } from '@/src/components/shadcn/input' diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index c80dd818c..bec865c47 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -38,7 +38,23 @@ "minute_label#other": "minuten", "minute_label": "minuten" }, - "jump_back_button_label": "Bearbeiten" + "jump_back_button_label": "Bearbeiten", + "Breadcrumbs": { + "root": "Home", + "courses": "Kurse", + "discover": "Entdecken", + "practice": "Üben", + "edit": "Bearbeiten", + "create": "Erstellen", + "account": "Account", + "start": "Start", + "results": "Ergebnisse", + "examination_results": "Prüfungsergebnisse", + "practice_results": "Üben", + "not-allowed": "Nicht erlaubt", + "attempt-limit": "Versuchslimit erreicht", + "attempt-not-possible": "Versuch abgebrochen" + } }, "Home": { "description": "Create your own Course to boost your knowledge to the next level. (de)" @@ -421,6 +437,10 @@ "clone_course": { "label": "Kurs duplizieren" }, + "show_exam_results": { + "label": "Prüfungs Ergebnisse", + "tooltip": "Dir fehlen Berechtigungen um Prüfungsergebnisse einzusehen!" + }, "inspect_statistics": { "label": "Statistik anzeigen" }, diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index ff514c093..85ea860eb 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -39,7 +39,23 @@ export default { 'minute_label#other': 'minuten', minute_label: 'minuten' }, - jump_back_button_label: 'Bearbeiten' + jump_back_button_label: 'Bearbeiten', + Breadcrumbs: { + root: 'Home', + courses: 'Kurse', + discover: 'Entdecken', + practice: 'Üben', + edit: 'Bearbeiten', + create: 'Erstellen', + account: 'Account', + start: 'Start', + results: 'Ergebnisse', + examination_results: 'Prüfungsergebnisse', + practice_results: 'Üben', + 'not-allowed': 'Nicht erlaubt', + 'attempt-limit': 'Versuchslimit erreicht', + 'attempt-not-possible': 'Versuch abgebrochen' + } }, Home: { description: 'Create your own Course to boost your knowledge to the next level. (de)' @@ -425,6 +441,10 @@ export default { clone_course: { label: 'Kurs duplizieren' }, + show_exam_results: { + label: 'Prüfungs Ergebnisse', + tooltip: 'Dir fehlen Berechtigungen um Prüfungsergebnisse einzusehen!' + }, inspect_statistics: { label: 'Statistik anzeigen' }, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e39d762c6..ac3800e91 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -38,7 +38,24 @@ "minute_label#other": "minutes", "minute_label": "minutes" }, - "jump_back_button_label": "Jump back" + "jump_back_button_label": "Jump back", + "Breadcrumbs": { + "root": "Home", + "courses": "Courses", + "discover": "Discover", + "practice": "Practice", + "edit": "Edit", + "create": "Create", + "account": "Account", + "start": "Start", + "results": "Results", + "examination_results": "Examination Results", + "practice_results": "Practice", + + "not-allowed": "Not Allowed", + "attempt-limit": "Attempt Limit Reached", + "attempt-not-possible": "Attempt aborted" + } }, "Home": { "description": "Create your own Courses to boost your knowledge to the next level. (en)" @@ -430,6 +447,10 @@ "clone_course": { "label": "Clone Course" }, + "show_exam_results": { + "label": "Show Examination Results", + "tooltip": "You are not allowed to view exam results!" + }, "inspect_statistics": { "label": "Inspect Statistics" }, diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index c434aa3a2..d0bb0788f 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -39,7 +39,23 @@ export default { 'minute_label#other': 'minutes', minute_label: 'minutes' }, - jump_back_button_label: 'Jump back' + jump_back_button_label: 'Jump back', + Breadcrumbs: { + root: 'Home', + courses: 'Courses', + discover: 'Discover', + practice: 'Practice', + edit: 'Edit', + create: 'Create', + account: 'Account', + start: 'Start', + results: 'Results', + examination_results: 'Examination Results', + practice_results: 'Practice', + 'not-allowed': 'Not Allowed', + 'attempt-limit': 'Attempt Limit Reached', + 'attempt-not-possible': 'Attempt aborted' + } }, Home: { description: 'Create your own Courses to boost your knowledge to the next level. (en)' @@ -431,6 +447,10 @@ export default { clone_course: { label: 'Clone Course' }, + show_exam_results: { + label: 'Show Examination Results', + tooltip: 'You are not allowed to view exam results!' + }, inspect_statistics: { label: 'Inspect Statistics' },