From 19d77f3c068566d3f6a050deb7d027e7118f8c15 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:45:40 +0100 Subject: [PATCH 01/27] ref: added dummy exam results to `KnowCMenu` --- .../checks/(hamburger-menu)/KnowledgeCheckMenu.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/checks/(hamburger-menu)/KnowledgeCheckMenu.tsx b/src/components/checks/(hamburger-menu)/KnowledgeCheckMenu.tsx index 734aa5a3e..5e59fa76e 100644 --- a/src/components/checks/(hamburger-menu)/KnowledgeCheckMenu.tsx +++ b/src/components/checks/(hamburger-menu)/KnowledgeCheckMenu.tsx @@ -146,7 +146,11 @@ export default function KnowledgeCheckMenu({ id, questions, share_key, owner_id, {t('clone_check.label')} - {t('inspect_statistics.label')} + + + {t('inspect_statistics.label')} + + From b8e45eeb363467022f8a3ee37c3d802c5bb7d550 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:28:47 +0100 Subject: [PATCH 02/27] style: lowered max-w size of practice-q container Previously the width of the grid-cell that shows the practice question, practice progress had a max-width of 4/5 on large screens. Now the max-w is slightly smaller on larger screens, namely to 50% of the viewport width. --- src/app/[locale]/checks/[share_token]/practice/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/[locale]/checks/[share_token]/practice/page.tsx b/src/app/[locale]/checks/[share_token]/practice/page.tsx index ef7a98369..774dd3050 100644 --- a/src/app/[locale]/checks/[share_token]/practice/page.tsx +++ b/src/app/[locale]/checks/[share_token]/practice/page.tsx @@ -74,7 +74,7 @@ export default async function PracticePage({ params, searchParams }: { params: P
-
+
From d4e3df6b1f3c2ee710d7209119b4b832a7d59c23 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:12:11 +0100 Subject: [PATCH 03/27] chore: added translation synchronization to `pre-commit` This way the auto-generated locale-files and the `.json` files that are manually edited are always in sync when changes are committed. --- .husky/pre-commit | 2 ++ package.json | 1 + 2 files changed, 3 insertions(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index abf1181ca..7870ed985 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,6 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" +yarn sync-translations +git add src/i18n/locales/*.ts npx lint-staged --allow-empty diff --git a/package.json b/package.json index aa73cdb5a..7d6b1b696 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "semantic-release": "semantic-release", "predev": "yarn de-instrument-code", + "sync-translations": "yarn tsx --conditions=react-server src/i18n/scripts/prepare-i18n_ally-locales.ts", "dev": "concurrently 'NEXT_PUBLIC_MODE=development next dev --turbopack' 'nodemon --exec \"yarn tsx --conditions=react-server src/i18n/scripts/prepare-i18n_ally-locales.ts\" --watch src/i18n/locales/*.json'", "-------- build & start --------": "--------", "instrument-code": "cp ./config/.babelrc.json .babelrc.json", From 54c9de9257594a71ef071286b5bca1b25e013d4c Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:21:05 +0100 Subject: [PATCH 04/27] ref: added `edit/[id]` layout to render specific `Breadcrumbs` The reason for this change is that previously the edit-course page showed the generic breadcrumb which showed the actual course id. Since the course-id is not a good breadcrumb the name is used instead, thus a custom Breadcrumbs were defined in the new layout to override the generic breadcrumbs. --- src/app/[locale]/courses/edit/[id]/layout.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/app/[locale]/courses/edit/[id]/layout.tsx diff --git a/src/app/[locale]/courses/edit/[id]/layout.tsx b/src/app/[locale]/courses/edit/[id]/layout.tsx new file mode 100644 index 000000000..6fc3a88d5 --- /dev/null +++ b/src/app/[locale]/courses/edit/[id]/layout.tsx @@ -0,0 +1,36 @@ +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { getCourseById } from '@/database/course/select' +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/src/components/shadcn/breadcrumb' + +export default async function EditLayout({ children, params }: { children: React.ReactNode; params: Promise<{ id: string }> }) { + const { id } = await params + const course = await getCourseById(id) + + if (!course) notFound() + + return ( + <> + + + + Home + + + + + + Courses + + + + Edit + + + {course.name} + + + {children} + + ) +} From d96ff4d25a7cd6bfd26f7adb0ded5dec0d25a5f6 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:35:09 +0100 Subject: [PATCH 05/27] ref: added minimum drag-drop answer constraint The reason for this change is that previously users able to delete all answer-options when creating a drag-drop question. To prevent this edge-case a min constraint was introduces to the respective schema to disallow this case. --- src/schemas/QuestionSchema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/schemas/QuestionSchema.ts b/src/schemas/QuestionSchema.ts index 3da8540ba..1b23836c6 100644 --- a/src/schemas/QuestionSchema.ts +++ b/src/schemas/QuestionSchema.ts @@ -111,6 +111,7 @@ function getDragDropAnswerSchema(t: Translator) { position: z.number().min(0, t('schemas.Shared.number.positive')), }), ) + .min(1, t('schemas.Question.SingleChoice.answers.min_answer_count')) .superRefine((answers, ctx) => { const n = answers.length const seen = new Set() From 4a3d5ed54110bb77c09ad68f8a3f5769e93ec3ba Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:39:58 +0100 Subject: [PATCH 06/27] ref: enabled `showIcon` in `FormFieldError` usages The reason for this change is that this way the general errors for choice- and drag-drop answer errors also include an error-icon, just like field-errors using the `Field` component do. --- .../create/(create-question)/CreateQuestionDialog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/courses/create/(create-question)/CreateQuestionDialog.tsx b/src/components/courses/create/(create-question)/CreateQuestionDialog.tsx index 1cb553990..e3d8421d9 100644 --- a/src/components/courses/create/(create-question)/CreateQuestionDialog.tsx +++ b/src/components/courses/create/(create-question)/CreateQuestionDialog.tsx @@ -353,9 +353,9 @@ function ChoiceQuestionAnswers({ control, watch, register, errors }: AnswerOptio ))}
- field='answers' errors={errors} /> + field='answers' errors={errors} showIcon /> {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - field='answers.root' errors={errors} /> + field='answers.root' errors={errors} showIcon /> @@ -464,9 +464,9 @@ function DragDropQuestionAnswers({ register, errors, control, watch, setValue }: ))}
- field='answers' errors={errors} /> + field='answers' errors={errors} showIcon /> {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - field='answers.root' errors={errors} /> + field='answers.root' errors={errors} showIcon /> From 1b4d1626a9ea56c0a1d2eeba38d4397821ed197c Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:52:12 +0100 Subject: [PATCH 07/27] ref: improved tooltip accessibility by allowing toggling This way users on mobile devices are also able to access tooltips. At the same time this allows users on desktop screens to also toggle tooltips. --- src/components/Shared/Tooltip.tsx | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/Shared/Tooltip.tsx b/src/components/Shared/Tooltip.tsx index c7663a20f..1a86f459d 100644 --- a/src/components/Shared/Tooltip.tsx +++ b/src/components/Shared/Tooltip.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react' import * as TooltipPrimitive from '@radix-ui/react-tooltip' import { Tooltip as ShadcnTooltip, TooltipContent, TooltipTrigger } from '@/src/components/shadcn/tooltip' import { cn } from '@/src/lib/Shared/utils' @@ -12,9 +13,29 @@ export type TooltipProps = Omit { + if (config.open === undefined || config.open === isPinned) return + + // eslint-disable-next-line react-hooks/set-state-in-effect + setPinned(config.open) + }, [config.open]) + return ( - - {children} + { + if (!isPinned) setIsHovered(isOpen) + + config.onOpenChange?.(isOpen) + }}> + setPinned((prev) => !prev)} aria-label='toggle tooltip'> + {children} + Date: Fri, 27 Mar 2026 07:48:09 +0100 Subject: [PATCH 08/27] ref: added textarea support to `Field` component This way fields for e.g. descriptions can be rendered as a Textarea instead of a single-line input. This can be done by setting the `variant` to `textarea`. --- src/components/Shared/form/Field.tsx | 71 +++++++++++++++++----------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/src/components/Shared/form/Field.tsx b/src/components/Shared/form/Field.tsx index f17bd552c..34687a35f 100644 --- a/src/components/Shared/form/Field.tsx +++ b/src/components/Shared/form/Field.tsx @@ -1,18 +1,45 @@ -import { ChangeEvent, useEffect, useRef, useState } from 'react' +import { ChangeEvent, ComponentProps, JSX, useEffect, useRef, useState } from 'react' import { AnimatePresence, motion } from 'framer-motion' import { InfoIcon, TriangleAlertIcon } from 'lucide-react' import { FieldValues, UseFormReturn } from 'react-hook-form' import { FormControl, FormField, FormLabel, FormMessage } from '@/src/components/shadcn/form' import { Input as ShadcnInput } from '@/src/components/shadcn/input' +import { Textarea } from '@/src/components/shadcn/textarea' import Tooltip from '@/src/components/Shared/Tooltip' import { cn } from '@/src/lib/Shared/utils' import { DescriptionMap, getDescriptionForRhfName } from '@/src/schemas/utils/extractDescriptions' import { Any } from '@/types' +type BaseFieldProps = { + form: UseFormReturn + name: Parameters>['0']['name'] + label?: string + descriptions?: DescriptionMap + showLabel?: boolean + labelClassname?: string + children?: React.ReactNode + containerClassname?: string + modifyValue?: (value: Any) => Any +} + +type InputFieldProps = { + variant?: 'input' + onChange?: (values: ChangeEvent['target']) => unknown +} & Omit, 'onChange' | 'name' | 'form'> + +type TextareaFieldProps = { + variant: 'textarea' + onChange?: (values: ChangeEvent['target']) => unknown +} & Omit, 'onChange' | 'name' | 'form' | 'children' | 'type'> +type FieldProps = BaseFieldProps & (InputFieldProps | TextareaFieldProps) + +export default function Field(props: BaseFieldProps & InputFieldProps): JSX.Element +export default function Field(props: BaseFieldProps & TextareaFieldProps): JSX.Element export default function Field({ form, name, onChange, + variant = 'input', label, descriptions, showLabel = true, @@ -21,18 +48,7 @@ export default function Field({ children, modifyValue, ...props -}: { - form: UseFormReturn - name: Parameters>['0']['name'] - label?: string - descriptions?: DescriptionMap - showLabel?: boolean - labelClassname?: string - children?: React.ReactNode - containerClassname?: string - onChange?: (values: ChangeEvent['target']) => unknown - modifyValue?: (value: Any) => Any -} & Omit, 'onChange' | 'name' | 'form'>) { +}: FieldProps) { const [isFocused, setIsFocused] = useState(false) const [isHovered, setIsHovered] = useState(false) const previousFocusState = useRef(false) @@ -53,21 +69,23 @@ export default function Field({ const showDescription = (isFocused && !hasError) || isHovered const description = descriptions ? getDescriptionForRhfName(descriptions, field.name) : undefined + const ControlledComponent = variant === 'textarea' ? Textarea : ShadcnInput + // when true --> prevents layout shifts when switching between error / description by setting min-h to animation-container. Note this "feature" is only enabled when there is something to switch between (thus, when there is a description (&& !!description)) const keepAnimationContainerSize = previousFocusState.current && (showDescription || hasError) && !!description - const fieldOnChange = (e: ChangeEvent) => { + const fieldOnChange = (e: ChangeEvent) => { // use 'custom' onChange to override value if (onChange) { return field.onChange({ ...e, - target: { value: onChange(e.target) }, + target: { value: onChange(e.target as Any) }, }) } // auto-support number inputs to use `valueAsNumber` - if (props.type === 'number') { - return field.onChange({ ...e, target: { ...e.target, value: e.target.valueAsNumber } }) + if (variant === 'input' && (props as InputFieldProps).type === 'number') { + return field.onChange({ ...e, target: { ...e.target, value: (e as ChangeEvent).target.valueAsNumber } }) } return field.onChange(e) @@ -79,22 +97,19 @@ export default function Field({
- { - // this prevents the description from being shown when the checkbox is clicked --> thus has focus - if (props.type !== 'checkbox') setIsFocused(true) - props.onFocus?.(e) + setIsFocused(true) + props.onFocus?.(e as Any) }} onBlur={(e) => { setIsFocused(false) - props.onBlur?.(e) + props.onBlur?.(e as Any) field.onBlur() }} onChange={fieldOnChange} @@ -120,7 +135,7 @@ export default function Field({ // disabled state styles 'data-[disabled=true]:text-muted-foreground/60 data-[disabled=true]:hover:text-muted-foreground/70 dark:data-[disabled=true]:hover:text-muted-foreground', // positions the icon next to the checkbox - props.type === 'checkbox' && 'inset-y-0 right-auto left-7 items-center', + variant === 'input' && (props as InputFieldProps).type === 'checkbox' && 'inset-y-0 right-auto left-7 items-center', !description && 'hidden', )}> @@ -137,7 +152,7 @@ export default function Field({ className={cn( 'absolute inset-y-0 top-2.5 right-3 z-10 flex items-baseline text-destructive', // positions the icon next to the checkbox - props.type === 'checkbox' && 'top-0.5 right-auto bottom-0 left-7 items-baseline', + variant === 'input' && (props as InputFieldProps).type === 'checkbox' && 'top-0.5 right-auto bottom-0 left-7 items-baseline', )}> From 601b3dac7c0dcac9655feadadd042d6c912d7f01 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:58:16 +0100 Subject: [PATCH 09/27] ref: applied `Field` variant `textarea` for descriptions This way the descriptions fields used in the `GeneralSection` and the `CourseContentDialog` are now rendering a textarea instead of a single-line input. --- .../courses/create/(sections)/CourseContentDialog.tsx | 2 +- src/components/courses/create/(sections)/GeneralSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/courses/create/(sections)/CourseContentDialog.tsx b/src/components/courses/create/(sections)/CourseContentDialog.tsx index 9be4f5d2d..f9c4af10c 100644 --- a/src/components/courses/create/(sections)/CourseContentDialog.tsx +++ b/src/components/courses/create/(sections)/CourseContentDialog.tsx @@ -124,7 +124,7 @@ export default function CourseContentDialog({ children, ...rest }: ContentDialog 'grid-cols-1 items-baseline justify-baseline gap-3 *:last:mb-0 *:odd:mt-3 *:odd:first:mt-0', '@md:grid-cols-[auto_1fr] @md:gap-7 @md:gap-x-7 @md:*:last:mb-0 @md:*:odd:mt-0', )}> - +
diff --git a/src/components/courses/create/(sections)/GeneralSection.tsx b/src/components/courses/create/(sections)/GeneralSection.tsx index ec17754ec..722d5d34a 100644 --- a/src/components/courses/create/(sections)/GeneralSection.tsx +++ b/src/components/courses/create/(sections)/GeneralSection.tsx @@ -109,7 +109,7 @@ export default function GeneralSection({ jumpBackButton, ...config }: { jumpBack 'dark:**:[&::-webkit-inner-spin-button]:brightness-80', )}> - + valueAsNumber} /> From 66b746870a512a4ba9284bbdfe4f8d171abe395d Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:04:25 +0100 Subject: [PATCH 10/27] chore: resolved stripping of array effects in `stripDefaultValues` Previously the `stripZodDefaultValues` utility function stripped all effects from a given array when stripping it from default values. To prevent potential issues because of it in the future, the stripped checks are now re-applied. --- src/schemas/utils/stripZodDefaultValues.ts | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/schemas/utils/stripZodDefaultValues.ts b/src/schemas/utils/stripZodDefaultValues.ts index c2c3c08e7..db3a65a5e 100644 --- a/src/schemas/utils/stripZodDefaultValues.ts +++ b/src/schemas/utils/stripZodDefaultValues.ts @@ -104,8 +104,32 @@ export function stripZodDefault(schema: Schema): St const def = getDef(schema) const element = (schema as any).element ?? def?.element ?? def?.items const elementStripped = stripZodDefault(element) - console.warn('Warning stripping all checks and effects from array, while removing nested default-values.') - return z.array(elementStripped) as StripZodDefault + + let arrSchema = z.array(elementStripped) as z.ZodArray + const checks = (def?.checks ?? []) as any[] + + for (const check of checks) { + switch (check.kind) { + case 'min': + arrSchema = arrSchema.min(check.value, { message: check.message }) + break + case 'max': + arrSchema = arrSchema.max(check.value, { message: check.message }) + break + case 'length': + arrSchema = arrSchema.length(check.value, { message: check.message }) + break + case 'nonempty': + arrSchema = arrSchema.nonempty({ message: check.message }) + break + default: + if (typeof check.refinement === 'function') { + arrSchema = arrSchema.refine(check.refinement as any, { message: check.message }) + } + } + } + + return arrSchema as unknown as StripZodDefault } case 'optional': { From 58d59f9aed7fb8bdbcfb068743bbe4ec73f90f4c Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:05:55 +0100 Subject: [PATCH 11/27] ref: removed log statement in `schemaDefaults` Previously the `schemaDefaults` utility function logged "unknown schema type" when a property was of type `any`, `unknown` or `undefined`. However, since all these types are instantiated as `undefined` it is not worth logging. --- src/schemas/utils/schemaDefaults.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/schemas/utils/schemaDefaults.ts b/src/schemas/utils/schemaDefaults.ts index 948debb16..92bf111b7 100644 --- a/src/schemas/utils/schemaDefaults.ts +++ b/src/schemas/utils/schemaDefaults.ts @@ -162,7 +162,6 @@ export default function schemaDefaults(schema: Sche case 'undefined': case 'unknown': case 'any': - console.log('unknown schema type...') return undefined as z.output case 'array': { From 63da904037482d363884f0626eab5e1578793aac Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:08:32 +0100 Subject: [PATCH 12/27] chore: added development build script This development build script shows all build errors and not just a single one. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7d6b1b696..c633b9247 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build:instrumented": "next build", "postbuild:instrumented": "yarn postbuild && yarn de-instrument-code", " ": "", + "build:dev": "__NEXT_TEST_MODE=true next build", "build": "next build", "postbuild": "cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/", " ": "", From 4b368856f565e8defcd4a146684a1f6ed991696f Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:20:33 +0100 Subject: [PATCH 13/27] ref: localized `PracticeBreadcrumbs` texts --- .../practice/PracticeBreadcrumbs.tsx | 17 ++++++++++------- src/i18n/locales/de.json | 4 ++++ src/i18n/locales/de.ts | 4 ++++ src/i18n/locales/en.json | 5 +++++ src/i18n/locales/en.ts | 4 ++++ 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx b/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx index 278c6cf08..ff4fa9130 100644 --- a/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx +++ b/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx @@ -2,21 +2,24 @@ import { DetailedHTMLProps, HTMLAttributes } from 'react' import Link from 'next/link' import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/src/components/shadcn/breadcrumb' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/src/components/shadcn/dropdown-menu' -export function PracticeBreadcrumbs({ +import { getScopedI18n } from '@/src/i18n/server-localization' +export async function PracticeBreadcrumbs({ share_token, selectedCategory: category, categories, ...props }: { share_token: string; selectedCategory?: string; categories: string[] } & DetailedHTMLProps, HTMLElement>) { + const t = await getScopedI18n('Shared.Breadcrumbs') + return ( - Practice + {t('practice')} - Category + {t('practice_category')} @@ -24,8 +27,8 @@ export function PracticeBreadcrumbs({ - {category === '_none_' || category === undefined ? 'combined-questions' : category} - Toggle menu + {category === '_none_' || category === undefined ? t('practice_category_all_label').toLowerCase().replace(/ /g, '-') : category} + {t('practice_category_all_sr_only')} - Combined Questions + {t('practice_category_all_label')} {categories.map((categoryName) => ( - Questions + {t('practice_page')} ) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index bec865c47..6cc60a0e2 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -44,6 +44,10 @@ "courses": "Kurse", "discover": "Entdecken", "practice": "Üben", + "practice_category_all_label": "alle Fragen", + "practice_category_all_sr_only": "wechsle kategorie", + "practice_page": "Fragen", + "practice_category": "Kategorie", "edit": "Bearbeiten", "create": "Erstellen", "account": "Account", diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 85ea860eb..f91116728 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -45,6 +45,10 @@ export default { courses: 'Kurse', discover: 'Entdecken', practice: 'Üben', + practice_category_all_label: 'alle Fragen', + practice_category_all_sr_only: 'wechsle kategorie', + practice_page: 'Fragen', + practice_category: 'Kategorie', edit: 'Bearbeiten', create: 'Erstellen', account: 'Account', diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index ac3800e91..245b742ed 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -44,6 +44,11 @@ "courses": "Courses", "discover": "Discover", "practice": "Practice", + "practice_category_all_label": "Combined Questions", + "practice_category_all_sr_only": "switch category", + "practice_page": "Questions", + "practice_category": "Category", + "edit": "Edit", "create": "Create", "account": "Account", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index d0bb0788f..8c22a5e36 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -45,6 +45,10 @@ export default { courses: 'Courses', discover: 'Discover', practice: 'Practice', + practice_category_all_label: 'Combined Questions', + practice_category_all_sr_only: 'switch category', + practice_page: 'Questions', + practice_category: 'Category', edit: 'Edit', create: 'Create', account: 'Account', From ea02cfe29244a37d08f737ec38bdc6425f96e96b Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:21:14 +0100 Subject: [PATCH 14/27] ref: localized `StopWatch` duration format based on locale --- src/components/Shared/StopwatchTime.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Shared/StopwatchTime.tsx b/src/components/Shared/StopwatchTime.tsx index 85f1df8e9..99b989d86 100644 --- a/src/components/Shared/StopwatchTime.tsx +++ b/src/components/Shared/StopwatchTime.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useState } from 'react' import { differenceInMilliseconds, formatDuration } from 'date-fns' +import { de, enUS } from 'date-fns/locale' +import { useCurrentLocale } from '@/src/i18n/client-localization' /** * This component essentially returns / displays the time that a user e.g. spents on a page. @@ -10,6 +12,7 @@ import { differenceInMilliseconds, formatDuration } from 'date-fns' * @returns The time that has passed since the `start` */ export function StopwatchTime({ start: rawStartDate, delimiter = ' and ' }: { start: Date; delimiter?: string }) { + const currentLocale = useCurrentLocale() const [timeSpent, setTimeSpent] = useState(null) const [startDate] = useState(new Date(Date.parse(rawStartDate.toString()))) //* ensure date-object even if stringified @@ -18,9 +21,12 @@ export function StopwatchTime({ start: rawStartDate, delimiter = ' and ' }: { st const differenceDate = new Date(difference) setTimeSpent( - formatDuration({ seconds: differenceDate.getSeconds(), minutes: differenceDate.getMinutes() || undefined, hours: differenceDate.getHours() - 1 || undefined }, { zero: true, delimiter }), + formatDuration( + { seconds: differenceDate.getSeconds(), minutes: differenceDate.getMinutes() || undefined, hours: differenceDate.getHours() - 1 || undefined }, + { zero: true, delimiter, locale: currentLocale === 'de' ? de : enUS }, + ), ) - }, [startDate, setTimeSpent, delimiter]) + }, [startDate, setTimeSpent, delimiter, currentLocale]) useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect From 9a3c110e500540980548b32cbbd8f1ae2071a9ac Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:31:59 +0100 Subject: [PATCH 15/27] ref: extended `PracticeBreadcrumbs` to start from root This way these custom breadcrumbs align with the other breadcrumbs shown on other pages, which all start from the root page. --- .../courses/[share_token]/practice/page.tsx | 9 ++++++- .../practice/PracticeBreadcrumbs.tsx | 25 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/[locale]/courses/[share_token]/practice/page.tsx b/src/app/[locale]/courses/[share_token]/practice/page.tsx index 20afcd87a..83622a74f 100644 --- a/src/app/[locale]/courses/[share_token]/practice/page.tsx +++ b/src/app/[locale]/courses/[share_token]/practice/page.tsx @@ -65,7 +65,14 @@ export default async function PracticePage({ params, searchParams }: { params: P return ( - + diff --git a/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx b/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx index ff4fa9130..c1c75846d 100644 --- a/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx +++ b/src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx @@ -7,13 +7,36 @@ export async function PracticeBreadcrumbs({ share_token, selectedCategory: category, categories, + courseId, + courseName, ...props -}: { share_token: string; selectedCategory?: string; categories: string[] } & DetailedHTMLProps, HTMLElement>) { +}: { share_token: string; selectedCategory?: string; categories: string[]; courseId: string; courseName: string } & DetailedHTMLProps, HTMLElement>) { const t = await getScopedI18n('Shared.Breadcrumbs') return ( + + + {t('root')} + + + + + + + {t('courses')} + + + + + + + {courseName} + + + + {t('practice')} From 4b5e47037f5d33058e4abed6a79b7f856561fe06 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:33:25 +0100 Subject: [PATCH 16/27] ref: localized `GenericBreadcrumb` page segments The `GenericBreadcrumb` component now displays the translation for a given page-segement-key or if it does not exist it shows the segment (key) as before. --- .../Shared/Breadcrumb/GenericBreadcrumb.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx b/src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx index df35e9d71..e1848aee3 100644 --- a/src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx +++ b/src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx @@ -5,11 +5,13 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import { Fragment } from 'react/jsx-runtime' import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/src/components/shadcn/breadcrumb' -import { useCurrentLocale } from '@/src/i18n/client-localization' +import { useCurrentLocale, useScopedI18n } from '@/src/i18n/client-localization' import { cn } from '@/src/lib/Shared/utils' +import { Any } from '@/types' export function GenericBreadcrumb({ show = true }: { show?: boolean }) { const [breadcrumbExists, setBreadcrumbExists] = useState(false) + const t = useScopedI18n('Shared.Breadcrumbs') const locale = useCurrentLocale() const pathname = usePathname() const pages = pathname.split('?').at(0)!.split('/')! @@ -34,20 +36,20 @@ export function GenericBreadcrumb({ show = true }: { show?: boolean }) { - Home + {t('root')} - {pages?.map((p, i) => ( - + {pages?.map((segment, i) => ( + {isCurrentPage(i) ? ( - {p} + {t(segment as Any)} ) : ( - {p} + {t(segment as Any)} )} From 3dd06745018b6b0ff50255500a3de7a8d7d551b7 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:34:37 +0100 Subject: [PATCH 17/27] ref: localized `PracticeResultsBreadcrumbs` texts --- .../results/practice/PracticeResultsBreadcrumbs.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/results/practice/PracticeResultsBreadcrumbs.tsx b/src/components/results/practice/PracticeResultsBreadcrumbs.tsx index 116fd1bc7..3985ba72f 100644 --- a/src/components/results/practice/PracticeResultsBreadcrumbs.tsx +++ b/src/components/results/practice/PracticeResultsBreadcrumbs.tsx @@ -1,30 +1,33 @@ import { DetailedHTMLProps, HTMLAttributes } from 'react' import Link from 'next/link' import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/src/components/shadcn/breadcrumb' -export function PracticeResultsBreadcrumbs({ share_token, ...props }: { share_token: string } & DetailedHTMLProps, HTMLElement>) { +import { getScopedI18n } from '@/src/i18n/server-localization' +export async function PracticeResultsBreadcrumbs({ share_token, ...props }: { share_token: string } & DetailedHTMLProps, HTMLElement>) { + const t = await getScopedI18n('Shared.Breadcrumbs') + return ( - Home + {t('root')} - Courses + {t('courses')} - Practice + {t('practice')} - Results + {t('results')} From eccc8a3711763eeb9aa9be66e86a40f2bdeecf70 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:42:50 +0100 Subject: [PATCH 18/27] fix: resolved `jumpBackButton` stage indecies The reason for this is that with the introduction of the new `content` stage the index of the remaining stages was increased by one. This meant that the `Jumpback` button redirected users to the wrong stage when clicked. This issue is now resolved. --- src/components/courses/create/(sections)/QuestionsSection.tsx | 2 +- src/components/courses/create/(sections)/SettingsSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/courses/create/(sections)/QuestionsSection.tsx b/src/components/courses/create/(sections)/QuestionsSection.tsx index 381035bd9..07d03ff57 100644 --- a/src/components/courses/create/(sections)/QuestionsSection.tsx +++ b/src/components/courses/create/(sections)/QuestionsSection.tsx @@ -14,7 +14,7 @@ export default function QuestionsSection({ jumpBackButton, disabled }: { jumpBac return ( - {jumpBackButton && } + {jumpBackButton && }

{t('title')}

diff --git a/src/components/courses/create/(sections)/SettingsSection.tsx b/src/components/courses/create/(sections)/SettingsSection.tsx index 683d461bf..29356391b 100644 --- a/src/components/courses/create/(sections)/SettingsSection.tsx +++ b/src/components/courses/create/(sections)/SettingsSection.tsx @@ -38,7 +38,7 @@ export default function SettingsSection({
updateSettings(getValues())} className={cn('@container my-4 grid grid-cols-1 gap-10 @[700px]:grid-cols-[repeat(auto-fill,minmax(600px,1fr))]', className)}> - {jumpBackButtons && } + {jumpBackButtons && } From 7a39842e739fb645d3ebd694dd7aab9175a124cf Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:31:36 +0200 Subject: [PATCH 19/27] ref: localized `ContentPageBreadcrumbs` texts --- .../contents/[courseId]/ContentsPageBreadcrumbs.tsx | 11 +++++++---- src/i18n/locales/de.json | 1 + src/i18n/locales/de.ts | 1 + src/i18n/locales/en.json | 2 ++ src/i18n/locales/en.ts | 1 + 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/courses/contents/[courseId]/ContentsPageBreadcrumbs.tsx b/src/components/courses/contents/[courseId]/ContentsPageBreadcrumbs.tsx index cb7f075d8..30fa34531 100644 --- a/src/components/courses/contents/[courseId]/ContentsPageBreadcrumbs.tsx +++ b/src/components/courses/contents/[courseId]/ContentsPageBreadcrumbs.tsx @@ -1,20 +1,23 @@ import Link from 'next/link' 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' -export function ContentPageBreadcrumbs({ course }: { course: Course }) { +export async function ContentPageBreadcrumbs({ course }: { course: Course }) { + const t = await getScopedI18n('Shared.Breadcrumbs') + return ( - Home + {t('root')} - Courses + {t('courses')} @@ -25,7 +28,7 @@ export function ContentPageBreadcrumbs({ course }: { course: Course }) { - Contents + {t('contents')} ) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 4748dd5c5..c71037334 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -53,6 +53,7 @@ "account": "Account", "start": "Start", "results": "Ergebnisse", + "contents": "Inhalte", "examination_results": "Prüfungsergebnisse", "practice_results": "Üben", "not-allowed": "Nicht erlaubt", diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index cef80d289..62f775680 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -54,6 +54,7 @@ export default { account: 'Account', start: 'Start', results: 'Ergebnisse', + contents: 'Inhalte', examination_results: 'Prüfungsergebnisse', practice_results: 'Üben', 'not-allowed': 'Nicht erlaubt', diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b2770077f..7aa2c90d8 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -57,6 +57,8 @@ "examination_results": "Examination Results", "practice_results": "Practice", + "contents": "Contents", + "not-allowed": "Not Allowed", "attempt-limit": "Attempt Limit Reached", "attempt-not-possible": "Attempt aborted" diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 3614b9858..6a8192946 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -56,6 +56,7 @@ export default { results: 'Results', examination_results: 'Examination Results', practice_results: 'Practice', + contents: 'Contents', 'not-allowed': 'Not Allowed', 'attempt-limit': 'Attempt Limit Reached', 'attempt-not-possible': 'Attempt aborted' From cd68b4aa942133822df6c9c7a082ecbf5252a919 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:03:11 +0200 Subject: [PATCH 20/27] ref: improved DOM structure of `RichTextEditor` The reason for this change is that previously the editor-pane (`EditorContent`) in some cases causes its container to grow outside of its max-height constraint. By removing an obsolete outer wrapper and moving the constraints directly to the editor pane these issues are resolved. --- .../contents/[courseId]/ContentRenderer.tsx | 2 +- .../tiptap-examples/RichTextEditor.tsx | 55 +++++++++++-------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/components/courses/contents/[courseId]/ContentRenderer.tsx b/src/components/courses/contents/[courseId]/ContentRenderer.tsx index e9befad4b..9b798366a 100644 --- a/src/components/courses/contents/[courseId]/ContentRenderer.tsx +++ b/src/components/courses/contents/[courseId]/ContentRenderer.tsx @@ -7,5 +7,5 @@ import { cn } from '@/src/lib/Shared/utils' export function ContentRenderer() { const { contents, currentContentIndex } = useContentContext() - return + return } diff --git a/src/components/tiptap-examples/RichTextEditor.tsx b/src/components/tiptap-examples/RichTextEditor.tsx index 0d013abfd..1b3584c44 100644 --- a/src/components/tiptap-examples/RichTextEditor.tsx +++ b/src/components/tiptap-examples/RichTextEditor.tsx @@ -131,6 +131,13 @@ export const RichTextEditorExtensions: Extensions = [ Selection, ] +/** + * Renders a rich-text-editor that comes with several different format styles (heading, lists, ...). + * It also supports different size(-ing) variants for its texts and text-margins. + * @param size Defines the text-size and margins between texts, paragraphs, headings, ... + * @param growth Defines how the height of the container that wraps the editor should behave, either grow by content or fill the remaining space. + * @returns + */ export function RichTextEditor({ onUpdateAction, defaultContent, @@ -139,12 +146,14 @@ export function RichTextEditor({ size = 'md', editorPaneClassname, editorContainerClassname, + growth, }: { onUpdateAction?: (content: object) => void defaultContent?: Content disabled?: boolean readOnly?: boolean size?: 'sm' | 'md' | 'lg' + growth?: 'content' | 'fill' editorPaneClassname?: string editorContainerClassname?: string }) { @@ -183,30 +192,28 @@ export function RichTextEditor({ }, [isMobile, mobileView]) return ( -
-
- - - {mobileView === 'main' ? ( - setMobileView('font')} onHighlighterClick={() => setMobileView('highlighter')} isMobile={isMobile} /> - ) : ( - setMobileView('main')} onHighlighterClick={() => setMobileView('highlighter')} /> - )} - - - { - // Only focus at end if clicking outside the actual content area - if (e.target === e.currentTarget && editor) { - editor.commands.focus('end') - } - }} - editor={editor} - role='presentation' - className={cn('rounded-md border border-input-ring', 'flex flex-1 flex-col', 'min-h-72 p-5', 'cursor-text overflow-auto', editorPaneClassname)} - /> - -
+
+ + + {mobileView === 'main' ? ( + setMobileView('font')} onHighlighterClick={() => setMobileView('highlighter')} isMobile={isMobile} /> + ) : ( + setMobileView('main')} onHighlighterClick={() => setMobileView('highlighter')} /> + )} + + + { + // Only focus at end if clicking outside the actual content area + if (e.target === e.currentTarget && editor) { + editor.commands.focus('end') + } + }} + editor={editor} + role='presentation' + className={cn('rounded-md border border-input-ring', 'flex max-h-[30dvh] flex-1 flex-col', 'p-5', 'cursor-text overflow-auto', editorPaneClassname)} + /> +
) } From 1ac22cbd7ac33249a814a1083983c536a7451683 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sun, 5 Apr 2026 10:39:09 +0200 Subject: [PATCH 21/27] ref: added contents and categories to `PracticeStore` The reason for this change is that this way components wrapped within the store can access the contents and categories of a given course to e.g. show a given content. --- src/hooks/courses/[share_token]/practice/PracticeStore.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/hooks/courses/[share_token]/practice/PracticeStore.ts b/src/hooks/courses/[share_token]/practice/PracticeStore.ts index a253bd511..b6b5912d8 100644 --- a/src/hooks/courses/[share_token]/practice/PracticeStore.ts +++ b/src/hooks/courses/[share_token]/practice/PracticeStore.ts @@ -1,5 +1,6 @@ import { savePracticeResults } from '@/database/practice' import { createZustandStore } from '@/src/hooks/Shared/zustand/createZustandStore' +import { Course } from '@/src/schemas/CourseSchema' import { instantiatePracticeData, PracticeData } from '@/src/schemas/practice/PracticeSchema' import { Question } from '@/src/schemas/QuestionSchema' import { WithCaching, ZustandStore } from '@/types/Shared/ZustandStore' @@ -7,6 +8,8 @@ import { WithCaching, ZustandStore } from '@/types/Shared/ZustandStore' export type PracticeState = PracticeData & { practiceQuestions: Question[] currentQuestionIndex: number + contents: Course['contents'] + categories: Course['questionCategories'] } export type PracticeActions = { @@ -33,6 +36,8 @@ export const createPracticeStore: WithCaching Date: Sun, 5 Apr 2026 10:41:56 +0200 Subject: [PATCH 22/27] feat: added help-content button during practicing This commit adds this new help button to the `RenderPracticeQuestion` component, which renders the button more or less next to the `Check Answer` button. In the future this button will open a drawer showing the respective content. --- .../courses/[share_token]/practice/page.tsx | 4 +++- .../practice/RenderPracticeQuestion.tsx | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/app/[locale]/courses/[share_token]/practice/page.tsx b/src/app/[locale]/courses/[share_token]/practice/page.tsx index 83622a74f..89441f161 100644 --- a/src/app/[locale]/courses/[share_token]/practice/page.tsx +++ b/src/app/[locale]/courses/[share_token]/practice/page.tsx @@ -64,7 +64,9 @@ export default async function PracticePage({ params, searchParams }: { params: P } return ( - + store) + const { practiceQuestions: questions, questions: unfilteredQuestions, currentQuestionIndex, navigateToQuestion, storeAnswer, contents, categories } = usePracticeStore((store) => store) const pathname = usePathname() const logger = useLogger('RenderPracticeQuestion') @@ -39,6 +39,7 @@ export function RenderPracticeQuestion() { } else if (!question) { notFound() } + const categoryId = categories.find((c) => c.name === question.category)!.id const RHFForm = useRHF( getQuestionInputSchema(t), @@ -103,7 +104,7 @@ export function RenderPracticeQuestion() { show={isValidationComplete && (question.type === 'single-choice' || question.type === 'multiple-choice')} /> -
+
+ +
From 6a3899f9f1ada58e6a9ef67e1fa299b73982eff4 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sun, 5 Apr 2026 10:44:34 +0200 Subject: [PATCH 23/27] ref: wrapped show contents button in Tooltip This way users are shown an informative description on what the button is going to do. --- .../practice/RenderPracticeQuestion.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx index 3b3a9f9d4..31e23d0ef 100644 --- a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx +++ b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx @@ -14,6 +14,7 @@ import DisplayFeedbackText from '@/src/components/courses/[share_token]/practice import { usePracticeStore } from '@/src/components/courses/[share_token]/practice/PracticeStoreProvider' import { Button } from '@/src/components/shadcn/button' import FormFieldError from '@/src/components/Shared/form/FormFieldError' +import Tooltip from '@/src/components/Shared/Tooltip' import { usePracticeFeeback } from '@/src/hooks/courses/[share_token]/practice/usePracticeFeedback' import { useLogger } from '@/src/hooks/log/useLogger' import { RHFProvider, useRHFContext } from '@/src/hooks/Shared/form/react-hook-form/RHFProvider' @@ -120,16 +121,18 @@ export function RenderPracticeQuestion() { Continue - + + +
From 4d76e012da7112a986508a2cf0ec797f647c46fe Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:16:35 +0200 Subject: [PATCH 24/27] feat: created PracticeContentDrawer component This component shows the content for a given category, e.g. during practice. --- .../practice/PracticeContentDrawer.tsx | 41 +++++++++++++++++++ src/i18n/locales/de.json | 1 + src/i18n/locales/de.ts | 1 + src/i18n/locales/en.json | 1 + src/i18n/locales/en.ts | 1 + 5 files changed, 45 insertions(+) create mode 100644 src/components/courses/[share_token]/practice/PracticeContentDrawer.tsx diff --git a/src/components/courses/[share_token]/practice/PracticeContentDrawer.tsx b/src/components/courses/[share_token]/practice/PracticeContentDrawer.tsx new file mode 100644 index 000000000..0001a064f --- /dev/null +++ b/src/components/courses/[share_token]/practice/PracticeContentDrawer.tsx @@ -0,0 +1,41 @@ +'use client' + +import { ChevronRightIcon } from 'lucide-react' +import { Button } from '@/src/components/shadcn/button' +import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/src/components/shadcn/drawer' +import { RichTextEditor } from '@/src/components/tiptap-examples/RichTextEditor' +import { useIsMobile } from '@/src/hooks/use-mobile' +import { useScopedI18n } from '@/src/i18n/client-localization' +import { CourseContent } from '@/src/schemas/CourseContentSchema' + +export function PracticeContentDrawer({ children, content }: { children: React.ReactNode; content?: CourseContent }) { + const isMobile = useIsMobile() + const t = useScopedI18n('Shared') + + return ( + + {children} + +
+ + + +
+ + {content?.title} + {content?.description} + +
+ +
+ + + + + +
+
+ ) +} diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index c71037334..0052d9a49 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -2,6 +2,7 @@ "Shared": { "navigation_button_next": "Weiter", "navigation_button_previous": "Zurück", + "close_label": "Schließen", "Question": { "question_label": "Frage", "type_label": "Art der Frage", diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 62f775680..b69d1654a 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -3,6 +3,7 @@ export default { Shared: { navigation_button_next: 'Weiter', navigation_button_previous: 'Zurück', + close_label: 'Schließen', Question: { question_label: 'Frage', type_label: 'Art der Frage', diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7aa2c90d8..e079dc690 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -2,6 +2,7 @@ "Shared": { "navigation_button_next": "Next", "navigation_button_previous": "Previous", + "close_label": "Close", "Question": { "question_label": "Question", "type_label": "Question Type", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 6a8192946..1a7ea5c18 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -3,6 +3,7 @@ export default { Shared: { navigation_button_next: 'Next', navigation_button_previous: 'Previous', + close_label: 'Close', Question: { question_label: 'Question', type_label: 'Question Type', From 4efdeb87d32d0e036a18c4845588647bc12d67ba Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:18:22 +0200 Subject: [PATCH 25/27] ref: wrapped practice content button with new Drawer This way the drawer with the respective content appears when a user clicks on the previously introduced "content"-help button. --- .../practice/RenderPracticeQuestion.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx index 31e23d0ef..5463505a8 100644 --- a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx +++ b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx @@ -11,6 +11,7 @@ import { UseFormRegister } from 'react-hook-form' import { FeedbackOpenQuestion } from '@/src/components/courses/[share_token]/(shared)/(questions)/OpenQuestionAnswer' import DragDropAnswers from '@/src/components/courses/[share_token]/practice/DragDropAnswerOptions' import DisplayFeedbackText from '@/src/components/courses/[share_token]/practice/FeedbackText' +import { PracticeContentDrawer } from '@/src/components/courses/[share_token]/practice/PracticeContentDrawer' import { usePracticeStore } from '@/src/components/courses/[share_token]/practice/PracticeStoreProvider' import { Button } from '@/src/components/shadcn/button' import FormFieldError from '@/src/components/Shared/form/FormFieldError' @@ -122,16 +123,18 @@ export function RenderPracticeQuestion() { - + c.categoryId === categoryId)}> + +
From 08b9173dfca3dea553f70b380c2367d80a2ba035 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:20:47 +0200 Subject: [PATCH 26/27] ref: simplified "contents"-help button on small screens This way the label is not shown on small screens to prevent overlapping between the "Check" and "Contents" buttons. --- .../courses/[share_token]/practice/RenderPracticeQuestion.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx index 5463505a8..1033e921a 100644 --- a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx +++ b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx @@ -106,7 +106,7 @@ export function RenderPracticeQuestion() { show={isValidationComplete && (question.type === 'single-choice' || question.type === 'multiple-choice')} /> -
+
From 47ced7f351fc535aed898244c48bc0abff6e65ba Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:25:14 +0200 Subject: [PATCH 27/27] ref: localized `RenderPracticeQuestion` static texts This commit localizes the "Check Answer", "Continue" and new content-button labels. --- .../practice/RenderPracticeQuestion.tsx | 13 +++++++------ src/i18n/locales/de.json | 5 +++++ src/i18n/locales/de.ts | 5 +++++ src/i18n/locales/en.json | 5 +++++ src/i18n/locales/en.ts | 5 +++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx index 1033e921a..36701cd8c 100644 --- a/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx +++ b/src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx @@ -20,7 +20,7 @@ import { usePracticeFeeback } from '@/src/hooks/courses/[share_token]/practice/u import { useLogger } from '@/src/hooks/log/useLogger' import { RHFProvider, useRHFContext } from '@/src/hooks/Shared/form/react-hook-form/RHFProvider' import useRHF from '@/src/hooks/Shared/form/useRHF' -import { useI18n } from '@/src/i18n/client-localization' +import { useI18n, useScopedI18n } from '@/src/i18n/client-localization' import { EvaluateAnswer } from '@/src/lib/courses/[share_token]/practice/EvaluateAnswer' import { cn } from '@/src/lib/Shared/utils' import { ChoiceQuestion, Question } from '@/src/schemas/QuestionSchema' @@ -28,7 +28,8 @@ import { getQuestionInputSchema, QuestionInput } from '@/src/schemas/UserQuestio import { Any } from '@/types' export function RenderPracticeQuestion() { - const t = useI18n() + const tAll = useI18n() + const t = useScopedI18n('Components.RenderPracticeQuestion') const { practiceQuestions: questions, questions: unfilteredQuestions, currentQuestionIndex, navigateToQuestion, storeAnswer, contents, categories } = usePracticeStore((store) => store) const pathname = usePathname() const logger = useLogger('RenderPracticeQuestion') @@ -44,7 +45,7 @@ export function RenderPracticeQuestion() { const categoryId = categories.find((c) => c.name === question.category)!.id const RHFForm = useRHF( - getQuestionInputSchema(t), + getQuestionInputSchema(tAll), { // Warning: Type assertion is intentional. // By setting the question_id and type, the form-values are (re-) set when the question changes, setting `values` makes the form controlled. @@ -115,11 +116,11 @@ export function RenderPracticeQuestion() { variant='base' isLoading={isSubmitting || isPending} type='submit'> - Check Answer + {t('check_answer_button_label')} @@ -132,7 +133,7 @@ export function RenderPracticeQuestion() { type='button' size='sm'> - Content + {t('contents_button_label')} diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 0052d9a49..2e6468fd8 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -560,6 +560,11 @@ "justify_label": "Ausrichten" } } + }, + "RenderPracticeQuestion": { + "check_answer_button_label": "Antwort prüfen", + "continue_button_label": "Weiter", + "contents_button_label": "Hilfe" } }, "AccountPage": { diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index b69d1654a..8c3775d92 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -567,6 +567,11 @@ export default { justify_label: 'Ausrichten' } } + }, + RenderPracticeQuestion: { + check_answer_button_label: 'Antwort prüfen', + continue_button_label: 'Weiter', + contents_button_label: 'Hilfe' } }, AccountPage: { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e079dc690..281c89146 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -572,6 +572,11 @@ "justify_label": "Align justify" } } + }, + "RenderPracticeQuestion": { + "check_answer_button_label": "Check Answer", + "continue_button_label": "Continue", + "contents_button_label": "Help" } }, "schemas": { diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1a7ea5c18..26ed8214d 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -570,6 +570,11 @@ export default { justify_label: 'Align justify' } } + }, + RenderPracticeQuestion: { + check_answer_button_label: 'Check Answer', + continue_button_label: 'Continue', + contents_button_label: 'Help' } }, schemas: {