Skip to content

Feat: Introduce content display to practice#296

Open
Marty-Byrde wants to merge 31 commits into
canaryfrom
feat/introduce-content-display-to-practice
Open

Feat: Introduce content display to practice#296
Marty-Byrde wants to merge 31 commits into
canaryfrom
feat/introduce-content-display-to-practice

Conversation

@Marty-Byrde

@Marty-Byrde Marty-Byrde commented Apr 5, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

Release Notes

  • New Features

    • Added help content drawer in practice questions for accessing course materials
    • Enabled course statistics and results navigation from practice menu
  • Improvements

    • Enhanced breadcrumb navigation across courses and practice sections with multi-level trails
    • Added textarea input support for description fields in forms
    • Improved tooltip interaction with pinned state capability
    • Enhanced layout constraints for better responsive design
  • Chores

    • Expanded internationalization support (German and English translations)
    • Updated build and translation synchronization scripts

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.
This way the auto-generated locale-files and the `.json` files that are manually edited are always in sync when changes are committed.
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.
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.
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.
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.
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`.
This way the descriptions fields used in the `GeneralSection` and the `CourseContentDialog` are now rendering a textarea instead of a single-line input.
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.
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.
This development build script shows all build errors and not just a single one.
This way these custom breadcrumbs align with the other breadcrumbs shown on other pages, which all start from the root page.
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.
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.
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.
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.
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.
This way users are shown an informative description on what the button is going to do.
This component shows the content for a given category, e.g. during practice.
This way the drawer with the respective content appears when a user clicks on the previously introduced "content"-help button.
This way the label is not shown on small screens to prevent overlapping between the "Check" and "Contents" buttons.
This commit localizes the "Check Answer", "Continue" and new content-button labels.
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR enhances the practice and course editing workflows by adding a content drawer for displaying course materials, converting multiple breadcrumb components to server-side rendered components with internationalization, improving form field flexibility with textarea support, and strengthening schema validation for drag-drop answers.

Changes

Cohort / File(s) Summary
Build Configuration
.husky/pre-commit, package.json
Added pre-commit hook to sync translations before lint-staged, and introduced new sync-translations and build:dev npm scripts.
Breadcrumb Components
src/components/.../Breadcrumb/GenericBreadcrumb.tsx, src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx, src/components/courses/contents/[courseId]/ContentsPageBreadcrumbs.tsx, src/components/results/practice/PracticeResultsBreadcrumbs.tsx
Converted breadcrumb components to async server components with i18n integration, replacing hardcoded labels with scoped translation keys (t('root'), t('courses'), etc.).
Practice Flow Components
src/components/courses/[share_token]/practice/PracticeContentDrawer.tsx, src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx
Added new PracticeContentDrawer component for displaying course content, and integrated it into RenderPracticeQuestion with conditional visibility based on available contents.
Practice Page & Store
src/app/[locale]/courses/[share_token]/practice/page.tsx, src/hooks/courses/[share_token]/practice/PracticeStore.ts
Extended practice store state to include contents and categories, and updated the practice page to pass course metadata into the store provider.
Form Fields & Input
src/components/Shared/form/Field.tsx, src/components/courses/create/(sections)/GeneralSection.tsx, src/components/courses/create/(sections)/CourseContentDialog.tsx
Added variant prop to Field component to support both input (default) and textarea rendering with proper type overloads and event handling.
UI Controls
src/components/Shared/Tooltip.tsx, src/components/Shared/StopwatchTime.tsx, src/components/tiptap-examples/RichTextEditor.tsx
Enhanced Tooltip with internal state management for pinning, added locale-aware formatting to StopwatchTime, and introduced growth prop to RichTextEditor for flexible sizing.
Course Management
src/app/[locale]/courses/edit/[id]/layout.tsx, src/components/courses/(hamburger-menu)/CourseActionMenu.tsx, src/components/courses/create/(sections)/QuestionsSection.tsx, src/components/courses/create/(sections)/SettingsSection.tsx, src/components/courses/contents/[courseId]/ContentRenderer.tsx
Added layout component for course edit flow, enabled navigation to results from course menu, adjusted step navigation targets, and updated content renderer styling.
Create Question Dialog
src/components/courses/create/(create-question)/CreateQuestionDialog.tsx
Updated FieldError components in question answer variants to pass explicit showIcon prop for enhanced error display.
Schema Validation
src/schemas/QuestionSchema.ts, src/schemas/utils/schemaDefaults.ts, src/schemas/utils/stripZodDefaultValues.ts
Added minimum answer constraint for drag-drop questions, removed debug logging, and improved array constraint preservation in Zod schema utilities.
Internationalization
src/i18n/locales/en.ts, src/i18n/locales/en.json, src/i18n/locales/de.ts, src/i18n/locales/de.json
Added translation keys for practice UI (close_label, practice_category, practice_page), breadcrumb navigation, and new practice question rendering controls in both English and German locales.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Feature

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: Introduce content display to practice' directly and clearly describes the main feature addition—enabling content display within the practice module, which is reflected throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/introduce-content-display-to-practice

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Apr 5, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (6)
src/components/courses/create/(create-question)/CreateQuestionDialog.tsx (1)

467-469: Consider using DragDropQuestion type for accuracy.

The generic type ChoiceQuestion is used on Line 467, but this is within DragDropQuestionAnswers. While both types have an answers field so this works at runtime, using DragDropQuestion would be more semantically accurate.

Suggested fix
-      <FieldError<ChoiceQuestion> field='answers' errors={errors} showIcon />
+      <FieldError<DragDropQuestion> field='answers' errors={errors} showIcon />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/create/`(create-question)/CreateQuestionDialog.tsx
around lines 467 - 469, Change the generic on the FieldError component to use
the DragDropQuestion type instead of ChoiceQuestion where it is rendering
drag-and-drop answers: locate the two usages of FieldError in
CreateQuestionDialog (the lines using FieldError<ChoiceQuestion> and
FieldError<any> for 'answers.root') and replace the generic to
FieldError<DragDropQuestion> (and change the second to
FieldError<DragDropQuestion> for 'answers.root') so the type accurately reflects
DragDropQuestionAnswers; ensure imports/types reference DragDropQuestion where
necessary.
src/components/Shared/form/Field.tsx (1)

77-92: Consider type narrowing instead of as Any cast on line 82.

The cast e.target as Any loses type safety. Since you already know the event type from the context (either input or textarea), you could leverage the discriminated union pattern more fully. However, the current approach is pragmatic and works correctly.

The guard on line 87 correctly restricts valueAsNumber access to input elements only.

♻️ Optional: Slightly tighter typing
 const fieldOnChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
   // use 'custom' onChange to override value
   if (onChange) {
     return field.onChange({
       ...e,
-      target: { value: onChange(e.target as Any) },
+      target: { value: onChange(e.target as typeof e.target) },
     })
   }

Though note: This still requires the caller to handle the union type in their onChange. The Any cast is acceptable given the overload signatures already constrain the actual types at call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Shared/form/Field.tsx` around lines 77 - 92, The helper
fieldOnChange currently casts e.target as Any when calling a custom onChange;
replace that unsafe cast with proper type narrowing: detect the element/event
shape before calling onChange (e.g., if variant === 'input' treat e as
ChangeEvent<HTMLInputElement> or use a runtime guard like e.target instanceof
HTMLInputElement to narrow to ChangeEvent<HTMLInputElement>, otherwise narrow to
ChangeEvent<HTMLTextAreaElement>) and pass the correctly-typed target value into
onChange; update the call sites in fieldOnChange so onChange receives a
properly-typed event instead of using the Any cast (references: fieldOnChange,
onChange, variant, InputFieldProps).
src/schemas/QuestionSchema.ts (1)

114-114: Use a DragDrop-specific i18n key for this min-answer message.

This currently reuses a SingleChoice translation key for a drag-drop rule, which couples unrelated namespaces and can break copy consistency across locales.

Suggested change
-      .min(1, t('schemas.Question.SingleChoice.answers.min_answer_count'))
+      .min(1, t('schemas.Question.DragDrop.answers.min_answer_count'))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/QuestionSchema.ts` at line 114, The min-answer validation for the
drag-drop rule is using the SingleChoice i18n key; update the schema to use a
DragDrop-specific key (e.g., replace
t('schemas.Question.SingleChoice.answers.min_answer_count') with
t('schemas.Question.DragDrop.answers.min_answer_count') in QuestionSchema where
the drag-drop rule is defined) and add the corresponding translation entries to
all locale files to keep copy consistent across namespaces.
src/components/Shared/Tooltip.tsx (2)

36-36: Internal isPinned toggle may conflict with parent-controlled config.open.

When a parent component (like FeedbackText.tsx) manages its own pinned state and passes it via config.open, clicking the trigger toggles the internal isPinned state independently. This creates dual sources of truth where the internal state diverges from the parent's intended state.

Consider making the click handler conditional on whether config.open is externally controlled:

Proposed fix
-<TooltipTrigger asChild onClick={() => setPinned((prev) => !prev)} aria-label='toggle tooltip'>
+<TooltipTrigger asChild onClick={() => config.open === undefined && setPinned((prev) => !prev)} aria-label='toggle tooltip'>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Shared/Tooltip.tsx` at line 36, The TooltipTrigger click
currently always toggles internal isPinned via setPinned, causing a conflict
when the parent controls visibility via config.open; modify the TooltipTrigger
onClick so it only toggles internal state when config.open is undefined
(uncontrolled), and when config.open is provided call the parent's handler
(e.g., config.onOpenChange or another provided callback) with the new boolean
instead of calling setPinned; update references in the TooltipTrigger handler
and ensure Tooltip's internal state (isPinned / setPinned) respects controlled
vs uncontrolled semantics using config.open and config.onOpenChange.

16-24: Missing isPinned in useEffect dependency array will trigger ESLint warning.

The isPinned check on line 20 creates a dependency that ESLint's exhaustive-deps rule will flag. Consider using a ref to track the previous config.open value instead, or add an eslint-disable comment if intentional.

Alternative using ref
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
...
 export default function Tooltip({ disabled, config = {}, delay = 250, variant = 'normal', content, children, ...props }: TooltipProps) {
   const [isHovered, setIsHovered] = useState(false)
   const [isPinned, setPinned] = useState(config.open ?? false)
+  const prevConfigOpen = useRef(config.open)

   useEffect(() => {
-    if (config.open === undefined || config.open === isPinned) return
-
-    // eslint-disable-next-line react-hooks/set-state-in-effect
-    setPinned(config.open)
-  }, [config.open])
+    if (config.open !== undefined && config.open !== prevConfigOpen.current) {
+      setPinned(config.open)
+    }
+    prevConfigOpen.current = config.open
+  }, [config.open])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Shared/Tooltip.tsx` around lines 16 - 24, The useEffect in
Tooltip uses isPinned (state from setPinned) in its conditional but doesn't
include it in the dependency array which causes an ESLint exhaustive-deps
warning; replace that conditional logic by tracking the previous config.open
with a ref (e.g., prevOpenRef) and compare config.open to prevOpenRef.current
instead of isPinned, update prevOpenRef.current after handling, and keep the
dependency array as [config.open] (or alternatively include isPinned in deps if
you intentionally want the state there). Ensure you update references to
isPinned/setPinned only when actually syncing from config.open to avoid
introducing a stale-closure bug in the useEffect.
src/app/[locale]/courses/edit/[id]/layout.tsx (1)

14-32: Breadcrumb labels are hardcoded in English, inconsistent with other breadcrumbs in this PR.

Other breadcrumb components in this PR (e.g., PracticeBreadcrumbs.tsx, GenericBreadcrumb.tsx) use scoped i18n translations via getScopedI18n('Shared.Breadcrumbs'). This layout uses hardcoded strings ("Home", "Courses", "Edit"), which won't translate for German users.

Suggested approach
+import { getScopedI18n } from '@/src/i18n/server-localization'
+
 export default async function EditLayout({ children, params }: { children: React.ReactNode; params: Promise<{ id: string }> }) {
   const { id } = await params
   const course = await getCourseById(id)
+  const t = await getScopedI18n('Shared.Breadcrumbs')

   if (!course) notFound()

   return (
     <>
       <Breadcrumb>
         <BreadcrumbList>
           <BreadcrumbLink asChild>
-            <Link href={`/`}>Home</Link>
+            <Link href={`/`}>{t('home')}</Link>
           </BreadcrumbLink>
           <BreadcrumbSeparator />

           <BreadcrumbItem>
             <BreadcrumbLink asChild>
-              <Link href={`/courses`}>Courses</Link>
+              <Link href={`/courses`}>{t('courses')}</Link>
             </BreadcrumbLink>
           </BreadcrumbItem>
           <BreadcrumbSeparator />
-          <BreadcrumbItem>Edit</BreadcrumbItem>
+          <BreadcrumbItem>{t('edit')}</BreadcrumbItem>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/courses/edit/[id]/layout.tsx around lines 14 - 32, Replace
the hardcoded breadcrumb labels ("Home", "Courses", "Edit") in the
Breadcrumb/BreadcrumbItem/BreadcrumbPage usage with scoped i18n lookups like
other components (see PracticeBreadcrumbs.tsx and GenericBreadcrumb.tsx); import
and use getScopedI18n('Shared.Breadcrumbs') in this layout, call the translation
keys (e.g., t('Home'), t('Courses'), t('Edit')) and pass the translated strings
to Link and BreadcrumbPage instead of literal text while keeping course.name as
before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.husky/pre-commit:
- Around line 4-5: After running yarn sync-translations the hook only stages
TypeScript locale files (git add src/i18n/locales/*.ts) but not JSON files;
update the pre-commit hook so the git add command also stages JSON locale files
(add src/i18n/locales/*.json or use a combined glob for .ts and .json) ensuring
any .json changes produced by sync-translations are included before commit.

In `@src/components/courses/`(hamburger-menu)/CourseActionMenu.tsx:
- Around line 168-170: The DropdownMenuItem builds a link using a nullable
variable share_key (in CourseActionMenu) which can produce
"/checks/null/results"; fix by guarding share_key before constructing the
route—only render the DropdownMenuItem (or set the Link href) when share_key is
truthy, otherwise omit the item or render a disabled/alternate UI; update the
Link usage inside CourseActionMenu so the href is computed conditionally from
share_key (or set href to undefined) to prevent invalid navigation.

In `@src/components/courses/`[share_token]/practice/PracticeBreadcrumbs.tsx:
- Line 53: In PracticeBreadcrumbs (BreadcrumbPage usage) stop slugifying the
translated “all categories” label: when category === '_none_' || category ===
undefined render t('practice_category_all_label') directly instead of
t(...).toLowerCase().replace(/ /g, '-'); keep existing behavior for the
non-empty category branch (i.e., render category as-is) so only remove the
.toLowerCase().replace(/ /g, '-') transformation on the translated label.

In `@src/components/courses/`[share_token]/practice/PracticeContentDrawer.tsx:
- Around line 21-23: The icon-only close button in PracticeContentDrawer (the
Button containing ChevronRightIcon) lacks an accessible name; update the Button
element in PracticeContentDrawer.tsx to include an explicit accessible label
such as aria-label="Close" (or aria-label={`Close ${context}`}/title as
appropriate) so screen readers can announce the action, ensuring the change is
applied to the Button component that wraps ChevronRightIcon.
- Line 11: The children prop of PracticeContentDrawer is currently typed as
React.ReactNode but is passed to DrawerTrigger with asChild (which requires a
single element), so narrow the prop type to a single React element: update the
signature of PracticeContentDrawer to accept children: React.ReactElement (or
ReactElement | null if you want to allow no child) instead of React.ReactNode,
and update any callers to wrap non-element children (strings/fragments) in a
proper element so DrawerTrigger asChild can safely clone the child.

In `@src/components/courses/`[share_token]/practice/RenderPracticeQuestion.tsx:
- Around line 126-129: Replace the hardcoded English tooltip and aria-label in
the Tooltip and Button around PracticeContentDrawer with localized strings using
the component's scoped i18n (e.g., the existing useTranslations/t function)
instead of inline text; build the tooltip text and aria-label by passing
question.category (or categoryId) into the translation key (e.g.,
"showCategoryContent" or similar) so Tooltip content and Button aria-label both
call t(...) with the category interpolation and use the same translation keys.
- Line 45: The lookup for the category and repeated content access in
RenderPracticeQuestion are unguarded and can throw if no match exists; locate
the categories.find((c) => c.name === question.category) usage (currently
assigned to categoryId) and the content lookups later in the component, compute
the foundCategory and foundContent once (e.g., const foundCategory =
categories.find(...); const content = foundCategory?.content ?? null), guard by
checking foundCategory/content before using them, replace the repeated lookups
with these single variables, and conditionally render the drawer trigger only
when the category/content is present to avoid runtime errors.

In `@src/components/courses/create/`(sections)/SettingsSection.tsx:
- Line 41: The jump-back target for settings is inconsistent: update all
CardStageJumpButton usages in the SettingsSection so they use targetStage={4}
instead of 3; specifically find the CardStageJumpButton instances rendered for
the Practice and Share cards (the same pattern as the existing {jumpBackButtons
&& <CardStageJumpButton targetStage={4} />}) and change their targetStage prop
to 4 so all Settings cards route to the Settings stage consistently.

In `@src/components/results/practice/PracticeResultsBreadcrumbs.tsx`:
- Around line 29-31: The breadcrumb nesting is invalid because BreadcrumbItem
(renders as <li>) is currently wrapping BreadcrumbPage (renders as <span>);
update the component tree so BreadcrumbPage is rendered inside BreadcrumbItem
for the footer breadcrumb — replace the current BreadcrumbPage><BreadcrumbItem
structure with BreadcrumbItem><BreadcrumbPage so the result matches the pattern
used earlier (lines with the other breadcrumb entries) and produces proper
<li><span>...</span></li> HTML semantics.

In `@src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx`:
- Around line 44-53: The breadcrumb renders every path segment through the i18n
function t (in GenericBreadcrumb.tsx inside the pages?.map loop using
isCurrentPage, BreadcrumbPage and BreadcrumbLink), which yields missing-key text
for dynamic segments; fix by defining a set/array of known static segment keys
(e.g., knownBreadcrumbKeys) and, inside the map where t(segment as Any) is used,
render t(segment) only if segment is in knownBreadcrumbKeys, otherwise render
decodeURIComponent(segment) (optionally apply the existing 'capitalize' class)
so dynamic tokens/IDs show readable text instead of missing translation keys.

In `@src/schemas/utils/stripZodDefaultValues.ts`:
- Around line 111-129: The switch over array constraint checks is using the
wrong property and outdated labels so arrSchema never gets min/max/length
applied; change the switch to inspect check.check (not check.kind) and update
case labels to Zod v4 names: handle 'min_length' -> arrSchema.min(...),
'max_length' -> arrSchema.max(...), 'length_equals' -> arrSchema.length(...),
keep 'nonempty' mapping to .nonempty(...), and leave the default to apply any
refinement via check.refinement on arrSchema; update references to check.message
and check.value accordingly in each branch for the functions used
(min/max/length/nonempty/refine).

---

Nitpick comments:
In `@src/app/`[locale]/courses/edit/[id]/layout.tsx:
- Around line 14-32: Replace the hardcoded breadcrumb labels ("Home", "Courses",
"Edit") in the Breadcrumb/BreadcrumbItem/BreadcrumbPage usage with scoped i18n
lookups like other components (see PracticeBreadcrumbs.tsx and
GenericBreadcrumb.tsx); import and use getScopedI18n('Shared.Breadcrumbs') in
this layout, call the translation keys (e.g., t('Home'), t('Courses'),
t('Edit')) and pass the translated strings to Link and BreadcrumbPage instead of
literal text while keeping course.name as before.

In `@src/components/courses/create/`(create-question)/CreateQuestionDialog.tsx:
- Around line 467-469: Change the generic on the FieldError component to use the
DragDropQuestion type instead of ChoiceQuestion where it is rendering
drag-and-drop answers: locate the two usages of FieldError in
CreateQuestionDialog (the lines using FieldError<ChoiceQuestion> and
FieldError<any> for 'answers.root') and replace the generic to
FieldError<DragDropQuestion> (and change the second to
FieldError<DragDropQuestion> for 'answers.root') so the type accurately reflects
DragDropQuestionAnswers; ensure imports/types reference DragDropQuestion where
necessary.

In `@src/components/Shared/form/Field.tsx`:
- Around line 77-92: The helper fieldOnChange currently casts e.target as Any
when calling a custom onChange; replace that unsafe cast with proper type
narrowing: detect the element/event shape before calling onChange (e.g., if
variant === 'input' treat e as ChangeEvent<HTMLInputElement> or use a runtime
guard like e.target instanceof HTMLInputElement to narrow to
ChangeEvent<HTMLInputElement>, otherwise narrow to
ChangeEvent<HTMLTextAreaElement>) and pass the correctly-typed target value into
onChange; update the call sites in fieldOnChange so onChange receives a
properly-typed event instead of using the Any cast (references: fieldOnChange,
onChange, variant, InputFieldProps).

In `@src/components/Shared/Tooltip.tsx`:
- Line 36: The TooltipTrigger click currently always toggles internal isPinned
via setPinned, causing a conflict when the parent controls visibility via
config.open; modify the TooltipTrigger onClick so it only toggles internal state
when config.open is undefined (uncontrolled), and when config.open is provided
call the parent's handler (e.g., config.onOpenChange or another provided
callback) with the new boolean instead of calling setPinned; update references
in the TooltipTrigger handler and ensure Tooltip's internal state (isPinned /
setPinned) respects controlled vs uncontrolled semantics using config.open and
config.onOpenChange.
- Around line 16-24: The useEffect in Tooltip uses isPinned (state from
setPinned) in its conditional but doesn't include it in the dependency array
which causes an ESLint exhaustive-deps warning; replace that conditional logic
by tracking the previous config.open with a ref (e.g., prevOpenRef) and compare
config.open to prevOpenRef.current instead of isPinned, update
prevOpenRef.current after handling, and keep the dependency array as
[config.open] (or alternatively include isPinned in deps if you intentionally
want the state there). Ensure you update references to isPinned/setPinned only
when actually syncing from config.open to avoid introducing a stale-closure bug
in the useEffect.

In `@src/schemas/QuestionSchema.ts`:
- Line 114: The min-answer validation for the drag-drop rule is using the
SingleChoice i18n key; update the schema to use a DragDrop-specific key (e.g.,
replace t('schemas.Question.SingleChoice.answers.min_answer_count') with
t('schemas.Question.DragDrop.answers.min_answer_count') in QuestionSchema where
the drag-drop rule is defined) and add the corresponding translation entries to
all locale files to keep copy consistent across namespaces.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a005e2c4-1c75-4238-8f8d-c154a99c82b4

📥 Commits

Reviewing files that changed from the base of the PR and between a567fce and 47ced7f.

📒 Files selected for processing (29)
  • .husky/pre-commit
  • package.json
  • src/app/[locale]/courses/[share_token]/practice/page.tsx
  • src/app/[locale]/courses/edit/[id]/layout.tsx
  • src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx
  • src/components/Shared/StopwatchTime.tsx
  • src/components/Shared/Tooltip.tsx
  • src/components/Shared/form/Field.tsx
  • src/components/courses/(hamburger-menu)/CourseActionMenu.tsx
  • src/components/courses/[share_token]/practice/PracticeBreadcrumbs.tsx
  • src/components/courses/[share_token]/practice/PracticeContentDrawer.tsx
  • src/components/courses/[share_token]/practice/RenderPracticeQuestion.tsx
  • src/components/courses/contents/[courseId]/ContentRenderer.tsx
  • src/components/courses/contents/[courseId]/ContentsPageBreadcrumbs.tsx
  • src/components/courses/create/(create-question)/CreateQuestionDialog.tsx
  • src/components/courses/create/(sections)/CourseContentDialog.tsx
  • src/components/courses/create/(sections)/GeneralSection.tsx
  • src/components/courses/create/(sections)/QuestionsSection.tsx
  • src/components/courses/create/(sections)/SettingsSection.tsx
  • src/components/results/practice/PracticeResultsBreadcrumbs.tsx
  • src/components/tiptap-examples/RichTextEditor.tsx
  • src/hooks/courses/[share_token]/practice/PracticeStore.ts
  • src/i18n/locales/de.json
  • src/i18n/locales/de.ts
  • src/i18n/locales/en.json
  • src/i18n/locales/en.ts
  • src/schemas/QuestionSchema.ts
  • src/schemas/utils/schemaDefaults.ts
  • src/schemas/utils/stripZodDefaultValues.ts
💤 Files with no reviewable changes (1)
  • src/schemas/utils/schemaDefaults.ts

Comment thread .husky/pre-commit
Comment on lines +4 to +5
yarn sync-translations
git add src/i18n/locales/*.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing *.json files in git add pattern.

The sync-translations script can modify both .ts and .json files when base keys are missing (the 'update-both' mode in prepare-i18n_ally-locales.ts). The current git add only stages *.ts files, leaving modified .json files unstaged.

Proposed fix
 yarn sync-translations 
-git add src/i18n/locales/*.ts
+git add src/i18n/locales/*.ts src/i18n/locales/*.json
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
yarn sync-translations
git add src/i18n/locales/*.ts
yarn sync-translations
git add src/i18n/locales/*.ts src/i18n/locales/*.json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.husky/pre-commit around lines 4 - 5, After running yarn sync-translations
the hook only stages TypeScript locale files (git add src/i18n/locales/*.ts) but
not JSON files; update the pre-commit hook so the git add command also stages
JSON locale files (add src/i18n/locales/*.json or use a combined glob for .ts
and .json) ensuring any .json changes produced by sync-translations are included
before commit.

Comment on lines +168 to +170
<DropdownMenuItem>
<Link href={`/checks/${share_key}/results`} className='flex flex-1 justify-between'>
{t('inspect_statistics.label')}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard nullable share_key before building the statistics route.

On Line 169, share_key can be null, which builds /checks/null/results and leads to invalid navigation for courses without a share token.

💡 Proposed fix
-          <DropdownMenuItem>
-            <Link href={`/checks/${share_key}/results`} className='flex flex-1 justify-between'>
-              {t('inspect_statistics.label')}
-            </Link>
-          </DropdownMenuItem>
+          <DropdownMenuItem
+            disabled={share_key === null}
+            enableTooltip={share_key === null}
+            tooltipOptions={{ ...baseTooltipOptions, content: t('remove_share_token.tooltip') }}
+            onClick={() => {
+              if (!share_key) return
+              router.push(`/checks/${share_key}/results`)
+            }}>
+            {t('inspect_statistics.label')}
+          </DropdownMenuItem>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/`(hamburger-menu)/CourseActionMenu.tsx around lines
168 - 170, The DropdownMenuItem builds a link using a nullable variable
share_key (in CourseActionMenu) which can produce "/checks/null/results"; fix by
guarding share_key before constructing the route—only render the
DropdownMenuItem (or set the Link href) when share_key is truthy, otherwise omit
the item or render a disabled/alternate UI; update the Link usage inside
CourseActionMenu so the href is computed conditionally from share_key (or set
href to undefined) to prevent invalid navigation.

<DropdownMenuTrigger className='flex items-center gap-1' id='category-switcher'>
<BreadcrumbPage>{category === '_none_' || category === undefined ? 'combined-questions' : category}</BreadcrumbPage>
<span className='sr-only'>Toggle menu</span>
<BreadcrumbPage>{category === '_none_' || category === undefined ? t('practice_category_all_label').toLowerCase().replace(/ /g, '-') : category}</BreadcrumbPage>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t slugify the translated “all categories” label in UI.

Line 53 converts a user-facing label into kebab-case, which degrades readability and localization quality.

✏️ Suggested patch
-              <BreadcrumbPage>{category === '_none_' || category === undefined ? t('practice_category_all_label').toLowerCase().replace(/ /g, '-') : category}</BreadcrumbPage>
+              <BreadcrumbPage>{category === '_none_' || category === undefined ? t('practice_category_all_label') : category}</BreadcrumbPage>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<BreadcrumbPage>{category === '_none_' || category === undefined ? t('practice_category_all_label').toLowerCase().replace(/ /g, '-') : category}</BreadcrumbPage>
<BreadcrumbPage>{category === '_none_' || category === undefined ? t('practice_category_all_label') : category}</BreadcrumbPage>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/`[share_token]/practice/PracticeBreadcrumbs.tsx at
line 53, In PracticeBreadcrumbs (BreadcrumbPage usage) stop slugifying the
translated “all categories” label: when category === '_none_' || category ===
undefined render t('practice_category_all_label') directly instead of
t(...).toLowerCase().replace(/ /g, '-'); keep existing behavior for the
non-empty category branch (i.e., render category as-is) so only remove the
.toLowerCase().replace(/ /g, '-') transformation on the translated label.

import { useScopedI18n } from '@/src/i18n/client-localization'
import { CourseContent } from '@/src/schemas/CourseContentSchema'

export function PracticeContentDrawer({ children, content }: { children: React.ReactNode; content?: CourseContent }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n "src/components/courses/[share_token]/practice/PracticeContentDrawer.tsx"

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 2332


🏁 Script executed:

# Find and check DrawerTrigger definition or imports to understand asChild expectations
rg "DrawerTrigger" --type tsx --type ts -B 2 -A 2

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 107


🏁 Script executed:

# Check if vaul or shadcn drawer package is used and what asChild requires
rg "from.*drawer\|from.*vaul" --type tsx --type ts

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 107


🏁 Script executed:

# Find the shadcn drawer component implementation
find . -name "drawer*" -type f | head -20

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 114


🏁 Script executed:

# Search for DrawerTrigger in the codebase
rg "DrawerTrigger" | head -20

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 1437


🏁 Script executed:

# Check the shadcn drawer implementation
cat -n src/components/shadcn/drawer.tsx 2>/dev/null || find . -path "*/shadcn/drawer*" -type f

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 4591


🏁 Script executed:

# Check other DrawerTrigger asChild usages to see how they type children
grep -B 5 "DrawerTrigger asChild" src/components/results/examination/PreviewQuestionResultDrawer.tsx | head -20

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 338


🏁 Script executed:

grep -B 5 "DrawerTrigger asChild" src/components/courses/[share_token]/ExaminationAttemptTable.tsx | head -20

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 278


🏁 Script executed:

# Check vaul documentation or package.json to understand asChild behavior
cat package.json | grep -A 5 -B 5 "vaul"

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 362


🏁 Script executed:

# Check if there are test files for these drawer components to see expected usage
find . -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "DrawerTrigger\|PracticeContentDrawer" 2>/dev/null

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 64


🏁 Script executed:

# Search for how children are actually passed to these drawer components to see real usage
rg "PracticeContentDrawer\|PreviewQuestionResultDrawer\|ExaminationAttemptTable" --type-add 'tsx:*.tsx' -g '*.tsx' -A 2 -B 2 | grep -A 5 -B 5 "DrawerTrigger\|children="

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 62


Narrow children type for DrawerTrigger asChild to avoid runtime errors.

On Line 11, children is typed as React.ReactNode, but on Line 17 it is passed to DrawerTrigger asChild, which expects a single element child. The asChild pattern in vaul requires cloning the child element, which fails at runtime if children is a string, fragment, or other non-element node type.

Proposed fix
-export function PracticeContentDrawer({ children, content }: { children: React.ReactNode; content?: CourseContent }) {
+export function PracticeContentDrawer({ children, content }: { children: React.ReactElement; content?: CourseContent }) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function PracticeContentDrawer({ children, content }: { children: React.ReactNode; content?: CourseContent }) {
export function PracticeContentDrawer({ children, content }: { children: React.ReactElement; content?: CourseContent }) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/`[share_token]/practice/PracticeContentDrawer.tsx at
line 11, The children prop of PracticeContentDrawer is currently typed as
React.ReactNode but is passed to DrawerTrigger with asChild (which requires a
single element), so narrow the prop type to a single React element: update the
signature of PracticeContentDrawer to accept children: React.ReactElement (or
ReactElement | null if you want to allow no child) instead of React.ReactNode,
and update any callers to wrap non-element children (strings/fragments) in a
proper element so DrawerTrigger asChild can safely clone the child.

Comment on lines +21 to +23
<Button variant='ghost' size='icon' className='size-4 bg-background text-neutral-600 hover:scale-115 dark:text-neutral-300'>
<ChevronRightIcon className='' />
</Button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an accessible label to the icon-only close button.

On Line 21, the icon-only button has no explicit accessible name. Add aria-label (or title) so screen readers can identify the action.

♿ Proposed fix
-            <Button variant='ghost' size='icon' className='size-4 bg-background text-neutral-600 hover:scale-115 dark:text-neutral-300'>
+            <Button
+              variant='ghost'
+              size='icon'
+              aria-label={t('close_label')}
+              className='size-4 bg-background text-neutral-600 hover:scale-115 dark:text-neutral-300'>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button variant='ghost' size='icon' className='size-4 bg-background text-neutral-600 hover:scale-115 dark:text-neutral-300'>
<ChevronRightIcon className='' />
</Button>
<Button
variant='ghost'
size='icon'
aria-label={t('close_label')}
className='size-4 bg-background text-neutral-600 hover:scale-115 dark:text-neutral-300'>
<ChevronRightIcon className='' />
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/`[share_token]/practice/PracticeContentDrawer.tsx
around lines 21 - 23, The icon-only close button in PracticeContentDrawer (the
Button containing ChevronRightIcon) lacks an accessible name; update the Button
element in PracticeContentDrawer.tsx to include an explicit accessible label
such as aria-label="Close" (or aria-label={`Close ${context}`}/title as
appropriate) so screen readers can announce the action, ensuring the change is
applied to the Button component that wraps ChevronRightIcon.

Comment on lines +126 to +129
<Tooltip content={`Show '${question.category}' learning materials`}>
<PracticeContentDrawer content={contents.find((c) => c.categoryId === categoryId)}>
<Button
aria-label={`show ${question.category} content`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize tooltip and aria text for the new content button.

Line 126 and Line 129 are hardcoded English strings while this component already uses scoped i18n. This creates a localization gap.

🌐 Suggested patch
-          <Tooltip content={`Show '${question.category}' learning materials`}>
+          <Tooltip content={t('contents_button_label')}>
...
-                aria-label={`show ${question.category} content`}
+                aria-label={t('contents_button_label')}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/`[share_token]/practice/RenderPracticeQuestion.tsx
around lines 126 - 129, Replace the hardcoded English tooltip and aria-label in
the Tooltip and Button around PracticeContentDrawer with localized strings using
the component's scoped i18n (e.g., the existing useTranslations/t function)
instead of inline text; build the tooltip text and aria-label by passing
question.category (or categoryId) into the translation key (e.g.,
"showCategoryContent" or similar) so Tooltip content and Button aria-label both
call t(...) with the category interpolation and use the same translation keys.

<form onChange={() => updateSettings(getValues())} className={cn('@container my-4 grid grid-cols-1 gap-10 @[700px]:grid-cols-[repeat(auto-fill,minmax(600px,1fr))]', className)}>
<Card className='relative row-span-2' disableInteractions>
{jumpBackButtons && <CardStageJumpButton targetStage={3} />}
{jumpBackButtons && <CardStageJumpButton targetStage={4} />}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Complete this stage-target fix across all Settings cards.

Line 41 correctly targets stage 4, but Practice/Share cards in this same section still target stage 3, which sends users to Questions instead of Settings.

Proposed follow-up patch
-        <Card className='relative' disableInteractions>
-          {jumpBackButtons && <CardStageJumpButton targetStage={3} />}
+        <Card className='relative' disableInteractions>
+          {jumpBackButtons && <CardStageJumpButton targetStage={4} />}
           <CardHeading title={t('PracticeSettings.title')} Icon={TbSettingsAutomation} />
           <PracticeSettings baseFieldProps={baseFieldProps} />
         </Card>
         <Card className='relative' disableInteractions>
-          {jumpBackButtons && <CardStageJumpButton targetStage={3} />}
+          {jumpBackButtons && <CardStageJumpButton targetStage={4} />}
           <CardHeading title={t('ShareSettings.title')} Icon={Share2Icon} iconClass='size-4.5' />
           <ShareSettings baseFieldProps={baseFieldProps} />
         </Card>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/create/`(sections)/SettingsSection.tsx at line 41, The
jump-back target for settings is inconsistent: update all CardStageJumpButton
usages in the SettingsSection so they use targetStage={4} instead of 3;
specifically find the CardStageJumpButton instances rendered for the Practice
and Share cards (the same pattern as the existing {jumpBackButtons &&
<CardStageJumpButton targetStage={4} />}) and change their targetStage prop to 4
so all Settings cards route to the Settings stage consistently.

Comment on lines 29 to 31
<BreadcrumbPage>
<BreadcrumbItem>Results</BreadcrumbItem>
<BreadcrumbItem>{t('results')}</BreadcrumbItem>
</BreadcrumbPage>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/results/practice/PracticeResultsBreadcrumbs.tsx

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 1689


🏁 Script executed:

fd -i breadcrumb --type f | head -20

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 437


🏁 Script executed:

cat -n src/components/shadcn/breadcrumb.tsx | head -100

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 2601


Fix breadcrumb component nesting.

BreadcrumbItem (renders as <li>) is incorrectly nested inside BreadcrumbPage (renders as <span>) on lines 29–31. This produces invalid HTML semantics (<span><li>...</li></span>). BreadcrumbPage should be nested inside BreadcrumbItem instead, consistent with the pattern used in lines 11–27.

Proposed fix
-        <BreadcrumbPage>
-          <BreadcrumbItem>{t('results')}</BreadcrumbItem>
-        </BreadcrumbPage>
+        <BreadcrumbItem>
+          <BreadcrumbPage>{t('results')}</BreadcrumbPage>
+        </BreadcrumbItem>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<BreadcrumbPage>
<BreadcrumbItem>Results</BreadcrumbItem>
<BreadcrumbItem>{t('results')}</BreadcrumbItem>
</BreadcrumbPage>
<BreadcrumbItem>
<BreadcrumbPage>{t('results')}</BreadcrumbPage>
</BreadcrumbItem>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/results/practice/PracticeResultsBreadcrumbs.tsx` around lines
29 - 31, The breadcrumb nesting is invalid because BreadcrumbItem (renders as
<li>) is currently wrapping BreadcrumbPage (renders as <span>); update the
component tree so BreadcrumbPage is rendered inside BreadcrumbItem for the
footer breadcrumb — replace the current BreadcrumbPage><BreadcrumbItem structure
with BreadcrumbItem><BreadcrumbPage so the result matches the pattern used
earlier (lines with the other breadcrumb entries) and produces proper
<li><span>...</span></li> HTML semantics.

Comment on lines +44 to 53
{pages?.map((segment, i) => (
<Fragment key={segment + i}>
<BreadcrumbSeparator />
<BreadcrumbItem className='capitalize'>
{isCurrentPage(i) ? (
<BreadcrumbPage>{p}</BreadcrumbPage>
<BreadcrumbPage>{t(segment as Any)}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href={'/' + pages.slice(0, i + 1).join('/')}>{p}</Link>
<Link href={'/' + pages.slice(0, i + 1).join('/')}>{t(segment as Any)}</Link>
</BreadcrumbLink>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 2679


🏁 Script executed:

# Search for i18n configuration files
find . -type f \( -name "i18n*" -o -name "*i18n*" -o -name "*.config*" -o -name "next-i18next*" \) 2>/dev/null | head -20

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 837


🏁 Script executed:

# Look for the i18n setup or locale files
fd -type f -name "*.json" | xargs grep -l "courses\|discover\|practice" 2>/dev/null | head -10

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 250


🏁 Script executed:

# Check for i18n usage patterns in the project
rg "useTranslation|t\(" src/ -A 2 -B 2 --max-count=5

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 50393


🏁 Script executed:

cat -n src/i18n/i18nConfig.ts

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 557


🏁 Script executed:

# Find breadcrumb translation files
find . -path ./node_modules -prune -o -type f \( -name "*.json" -o -name "*.ts" -o -name "*.js" \) -print | xargs grep -l "Shared.Breadcrumbs\|Breadcrumbs" 2>/dev/null | head -20

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 183


🏁 Script executed:

# Check client-localization implementation
cat -n src/i18n/client-localization.ts | head -100

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 617


🏁 Script executed:

# Look for translation file directories
find . -path ./node_modules -prune -o -type d -name "locales" -o -name "translations" -o -name "messages" -print 2>/dev/null

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 62


🏁 Script executed:

cat -n src/i18n/locales/en.json | grep -A 50 "Breadcrumb\|breadcrumb" -i

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 2172


🏁 Script executed:

# Check en.ts file for breadcrumb translations
cat -n src/i18n/locales/en.ts | head -200

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 9166


🏁 Script executed:

# Look for the actual structure of breadcrumb keys
rg "Shared.*Breadcrumb" src/i18n/locales/ -A 5 -B 5

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 62


🏁 Script executed:

# Search for usages of GenericBreadcrumb to see what paths contain
rg "GenericBreadcrumb|breadcrumb" src/ -A 3 -B 3 --type tsx --type ts | head -100

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 107


🏁 Script executed:

# Look for usages of breadcrumb and what paths might contain
rg "GenericBreadcrumb" src/ -l

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 209


🏁 Script executed:

# Check what pages might have IDs or slugs in their breadcrumbs
rg "courses/\|practice/\|edit/" src/app src/components --type ts --type js -A 2 -B 2 | head -80

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 62


🏁 Script executed:

# Look at next-international behavior for missing keys
rg "next-international\|useScopedI18n" src/ -A 3 -B 1 | head -150

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 62


🏁 Script executed:

# Search for examples of dynamic segments in breadcrumbs (like IDs, slugs)
find src/app -type f -name "page.tsx" -o -name "layout.tsx" | xargs grep -l "courses\|practice" 2>/dev/null | head -10

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 581


🏁 Script executed:

# Check the actual routes to understand what segments appear
cat -n src/app/[locale]/courses/edit/[id]/page.tsx | head -30

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 981


🏁 Script executed:

# Check one of the share_token pages
cat -n src/app/[locale]/courses/[share_token]/page.tsx | head -30

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 1981


🏁 Script executed:

# Look at how next-international handles missing keys - check documentation or implementation
find . -path ./node_modules/next-international -prune -o -type f -name "*.ts" -o -name "*.js" | xargs grep -l "missing.*key\|defaultValue" 2>/dev/null | grep -v node_modules | head -5

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 328


🏁 Script executed:

# Check if there's a fallback mechanism or if missing keys are logged
rg "defaultValue\|fallback\|missing" src/i18n/ -A 2 -B 2

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 62


🏁 Script executed:

# Look for how next-international handles missing translation keys
rg "key" node_modules/next-international -A 2 -B 2 --max-count=3 2>/dev/null | head -40

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 12198


🏁 Script executed:

# Search for any custom i18n setup or handling
find src/i18n -type f -name "*.ts" -o -name "*.js" | xargs cat -n 2>/dev/null

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 50392


🏁 Script executed:

# Check if there are any error boundaries or fallback handlers for translations
rg "Breadcrumb\|translation\|fallback" src/components/Shared/Breadcrumb/ -A 5 -B 5

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 62


🏁 Script executed:

# Look at actual test or usage to see what happens with dynamic segments
rg "GenericBreadcrumb" src/ -A 10 -B 5

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 5052


Translate only known breadcrumb segments; fall back to decoded segment text for dynamic values.

On lines 49 and 52, all path segments are unconditionally translated via t(segment as Any). Since routes contain dynamic segments (e.g., /courses/[share_token] or /edit/[id]), this causes missing translation keys for UUIDs and tokens. Without a configured fallback locale, these unknown segments will display as missing-key strings rather than human-readable text.

Maintain a set of known static breadcrumb keys and fall back to decodeURIComponent(segment) for any unknown segment.

💡 Proposed fix
-        {pages?.map((segment, i) => (
+        {pages?.map((segment, i) => (
           <Fragment key={segment + i}>
             <BreadcrumbSeparator />
             <BreadcrumbItem className='capitalize'>
+              {/** translate only known breadcrumb keys */}
+              {(() => {
+                const knownKeys = new Set([
+                  'courses',
+                  'discover',
+                  'practice',
+                  'edit',
+                  'create',
+                  'account',
+                  'start',
+                  'results',
+                  'contents',
+                  'not-allowed',
+                  'attempt-limit',
+                  'attempt-not-possible',
+                ])
+                const label = knownKeys.has(segment) ? t(segment as Any) : decodeURIComponent(segment)
+
+                return isCurrentPage(i) ? (
+                  <BreadcrumbPage>{label}</BreadcrumbPage>
+                ) : (
+                  <BreadcrumbLink asChild>
+                    <Link href={'/' + pages.slice(0, i + 1).join('/')}>{label}</Link>
+                  </BreadcrumbLink>
+                )
+              })()}
-              {isCurrentPage(i) ? (
-                <BreadcrumbPage>{t(segment as Any)}</BreadcrumbPage>
-              ) : (
-                <BreadcrumbLink asChild>
-                  <Link href={'/' + pages.slice(0, i + 1).join('/')}>{t(segment as Any)}</Link>
-                </BreadcrumbLink>
-              )}
             </BreadcrumbItem>
           </Fragment>
         ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{pages?.map((segment, i) => (
<Fragment key={segment + i}>
<BreadcrumbSeparator />
<BreadcrumbItem className='capitalize'>
{isCurrentPage(i) ? (
<BreadcrumbPage>{p}</BreadcrumbPage>
<BreadcrumbPage>{t(segment as Any)}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href={'/' + pages.slice(0, i + 1).join('/')}>{p}</Link>
<Link href={'/' + pages.slice(0, i + 1).join('/')}>{t(segment as Any)}</Link>
</BreadcrumbLink>
{pages?.map((segment, i) => (
<Fragment key={segment + i}>
<BreadcrumbSeparator />
<BreadcrumbItem className='capitalize'>
{/** translate only known breadcrumb keys */}
{(() => {
const knownKeys = new Set([
'courses',
'discover',
'practice',
'edit',
'create',
'account',
'start',
'results',
'contents',
'not-allowed',
'attempt-limit',
'attempt-not-possible',
])
const label = knownKeys.has(segment) ? t(segment as Any) : decodeURIComponent(segment)
return isCurrentPage(i) ? (
<BreadcrumbPage>{label}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href={'/' + pages.slice(0, i + 1).join('/')}>{label}</Link>
</BreadcrumbLink>
)
})()}
</BreadcrumbItem>
</Fragment>
))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Shared/Breadcrumb/GenericBreadcrumb.tsx` around lines 44 - 53,
The breadcrumb renders every path segment through the i18n function t (in
GenericBreadcrumb.tsx inside the pages?.map loop using isCurrentPage,
BreadcrumbPage and BreadcrumbLink), which yields missing-key text for dynamic
segments; fix by defining a set/array of known static segment keys (e.g.,
knownBreadcrumbKeys) and, inside the map where t(segment as Any) is used, render
t(segment) only if segment is in knownBreadcrumbKeys, otherwise render
decodeURIComponent(segment) (optionally apply the existing 'capitalize' class)
so dynamic tokens/IDs show readable text instead of missing translation keys.

Comment on lines +111 to +129
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 })
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For Zod v4, what is the internal structure of array checks in schema definitions (_def/_zod.def)? Specifically, are kindandvaluevalid fields, or should code read something likecheckwithminimum/maximum/size?

💡 Result:

In Zod v4, the internal structure of array schema definitions has changed from Zod v3. Schemas now use schema._zod.def instead of the deprecated schema._def. For a basic z.array(z.string) schema, the _zod.def object has a type: "array" field and an element field referencing the inner schema. It does not have top-level kind or value fields. Array length validations like .min, .max, and .length add checks to the schema. These checks are stored in schema._zod.def.checks, an array of check objects. Each check object has: - check: "min_length", "max_length", or "length_equals" (for arrays; similar to "min", "max" for numbers/strings) - value: the threshold value (e.g., minimum length) kind and value are not valid top-level fields on the array schema's _zod.def. Code should read the checks array and filter by check type to access minimum/maximum/size equivalents via the value property. Example (from community sources and Zod v4 migration notes): const arr = z.array(z.string).min(5); arr._zod.def.checks[0] // => { check: "min_length", value: 5, ... } Note: Zod internals (_zod.def) are unstable and subject to change. Prefer public APIs like .minValue, .maxValue where available, or schema.element for array inner type. For introspection, unwrap optional/nullables first: schema.unwrap._zod.def.checks?.find(c => c.check === "min_length")?.value. Sources confirm this pattern persists from v3 (where it was _def.checks) to v4, with the property path updated per migration guide.

Citations:


🏁 Script executed:

# Check the actual code at lines 111-129 in stripZodDefaultValues.ts
fd stripZodDefaultValues.ts -type f

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 250


🏁 Script executed:

# Also check for package.json to find Zod version
fd package.json -type f | head -5

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 250


🏁 Script executed:

fd stripZodDefaultValues.ts -t f

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 122


🏁 Script executed:

fd package.json -t f | head -1 | xargs cat

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 8189


🏁 Script executed:

cat -n src/schemas/utils/stripZodDefaultValues.ts | sed -n '100,140p'

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 1734


🏁 Script executed:

cat -n src/schemas/utils/stripZodDefaultValues.ts | sed -n '1,50p'

Repository: Master-Thesis-188199/KnowledgeCheckr

Length of output: 2975


Fix check.kindcheck.check and update case labels for Zod v4 compatibility.

The code uses check.kind and cases like 'min', 'max', 'length', but Zod v4.3.6 uses check.check with labels 'min_length', 'max_length', 'length_equals'. Since check.kind is undefined, all array constraint switches fail silently and only the refinement default path executes, dropping .min(), .max(), and .length() rules entirely.

Update the switch to inspect check.check instead and map case labels correctly:

Required fix
      for (const check of checks) {
-       switch (check.kind) {
-         case 'min':
+       switch (check.check) {
+         case 'min_length':
            arrSchema = arrSchema.min(check.value, { message: check.message })
            break
-         case 'max':
+         case 'max_length':
            arrSchema = arrSchema.max(check.value, { message: check.message })
            break
-         case 'length':
+         case 'length_equals':
            arrSchema = arrSchema.length(check.value, { message: check.message })
            break
           case 'nonempty':
             arrSchema = arrSchema.nonempty({ message: check.message })
             break
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/utils/stripZodDefaultValues.ts` around lines 111 - 129, The
switch over array constraint checks is using the wrong property and outdated
labels so arrSchema never gets min/max/length applied; change the switch to
inspect check.check (not check.kind) and update case labels to Zod v4 names:
handle 'min_length' -> arrSchema.min(...), 'max_length' -> arrSchema.max(...),
'length_equals' -> arrSchema.length(...), keep 'nonempty' mapping to
.nonempty(...), and leave the default to apply any refinement via
check.refinement on arrSchema; update references to check.message and
check.value accordingly in each branch for the functions used
(min/max/length/nonempty/refine).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant