Skip to content

Conversation

@ussaama
Copy link
Contributor

@ussaama ussaama commented Dec 9, 2025

Summary by CodeRabbit

  • New Features

    • Added tutorial slug option for customizable onboarding content with choices: Skip Consent, Default (privacy only), Basic, and Advanced levels.
    • Introduced localized onboarding content variations across supported languages.
  • Bug Fixes

    • Corrected option value normalization for tutorial selection.
  • Documentation

    • Updated UI labels for clarity: "Skip data privacy slide (Host manages consent)" and "Advanced (Tips and best practices)."

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

Walkthrough

This PR refactors the participant onboarding system to remove Directus API calls for tutorial cards and store card definitions as JSON in the frontend. Tutorial slides are now generated based on a slug field (default_conversation_tutorial_slug with values: skip-consent, none, basic, advanced) instead of fetching from a remote CMS.

Changes

Cohort / File(s) Summary
Directus Sync Configuration
echo/directus/sync/collections/operations.json
UUID and ID field swaps across multiple operation entries (resolve, reject, _syncId); no behavioral changes to operation code blocks.
Field Schema Definition
echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json
Added "Skip Consent" option (value: skip-consent), changed "None" option value from "None" to "none", nullified the note field.
Core Onboarding Components
echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx, echo/frontend/src/routes/participant/ParticipantStart.tsx
Removed initialCards prop and Directus tutorial card fetching; cards now derived solely from project.default_conversation_tutorial_slug; simplified loading and error handling to depend only on project state.
Hook Logic & System Cards
echo/frontend/src/components/participant/hooks/useOnboardingCards.ts
Replaced boolean hasDirectusCards parameter with optional tutorialSlug in getSystemCards; implemented slug-based routing (skip-consent → empty list, none → privacy only, basic → basic slides + privacy, advanced → advanced slides); added getBasicTutorialCards and getAdvancedTutorialCards helpers with language fallback to en-US.
Portal Editor UI
echo/frontend/src/components/project/ProjectPortalEditor.tsx
Added slug normalization logic, updated UI option labels ("No tutorial" → "Skip data privacy slide", new "Default - No tutorial" option, "Advanced (Tips and tricks)" → "Advanced (Tips and best practices)").
Localization Files
echo/frontend/src/locales/de-DE.po, en-US.po, es-ES.po, fr-FR.po, nl-NL.po
Added new translation entries for "Skip data privacy slide", "Default - No tutorial", and "Advanced (Tips and best practices)"; relocated/renumbered existing ProjectPortalEditor references; marked deprecated entries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • useOnboardingCards.ts requires careful attention: slug validation logic, four distinct control flow branches (skip-consent/none/basic/advanced), and new helper functions with language-specific content
  • ParticipantOnboardingCards.tsx and ParticipantStart.tsx: verify that removed Directus call logic doesn't inadvertently break fallback scenarios or error states
  • ProjectPortalEditor.tsx: confirm slug normalization handles all edge cases and that the default value wiring is correct
  • Locale files are repetitive but should be spot-checked for consistency across languages and correct source line references

Possibly related PRs

Suggested labels

feature, improvement


LGTM. Clean architectural move—decoupling from Directus CMS for onboarding and baking card logic into the frontend with slug-based routing. The slug normalization and multi-language fallback to en-US are solid patterns. Just watch the control flow paths in useOnboardingCards; four branches can hide edge cases.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main change: updating onboarding cards logic and removing Directus dependencies, with explicit issue references.
Linked Issues check ✅ Passed All ECHO-615 and ECHO-543 requirements met: Directus calls removed, onboarding cards now stored as JSON frontend logic with slug-based flow controlling card variants.
Out of Scope Changes check ✅ Passed All changes are scoped to onboarding cards refactoring, Directus removal, and related localization/config updates per linked issues.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch testing

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx (1)

78-204: Cards object definition is solid but rebuilt on every render.

The inline cards object with spread getSystemCards() works. If perf becomes a concern, wrap this in useMemo with [language, tutorialSlug, MicrophoneTestComponent, InitiateFormComponent] deps. For now, LGTM.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 441a45d and 2ccf5c6.

📒 Files selected for processing (11)
  • echo/directus/sync/collections/operations.json (12 hunks)
  • echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json (1 hunks)
  • echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx (6 hunks)
  • echo/frontend/src/components/participant/hooks/useOnboardingCards.ts (1 hunks)
  • echo/frontend/src/components/project/ProjectPortalEditor.tsx (3 hunks)
  • echo/frontend/src/locales/de-DE.po (42 hunks)
  • echo/frontend/src/locales/en-US.po (43 hunks)
  • echo/frontend/src/locales/es-ES.po (42 hunks)
  • echo/frontend/src/locales/fr-FR.po (42 hunks)
  • echo/frontend/src/locales/nl-NL.po (43 hunks)
  • echo/frontend/src/routes/participant/ParticipantStart.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
echo/frontend/**/src/routes/**/*.tsx

📄 CodeRabbit inference engine (echo/frontend/AGENTS.md)

Use Lingui macros t from @lingui/core/macro and Trans from @lingui/react/macro for localizing UI strings in routed screens

Files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
echo/frontend/**/src/{routes,components}/**/*.tsx

📄 CodeRabbit inference engine (echo/frontend/AGENTS.md)

echo/frontend/**/src/{routes,components}/**/*.tsx: Compose Mantine primitives (Stack, Group, ActionIcon, etc.) while layering Tailwind utility classes via className, alongside toast feedback via @/components/common/Toaster
For Tailwind classes that need dynamic values, replace with inline style props using CSS variables instead of hardcoded hex values

Files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
  • echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx
  • echo/frontend/src/components/project/ProjectPortalEditor.tsx
echo/frontend/**/src/**/*.{css,tsx,ts}

📄 CodeRabbit inference engine (echo/frontend/AGENTS.md)

Use CSS variables var(--app-background) and var(--app-text) instead of hardcoded colors like #F6F4F1 or #2D2D2C to ensure theme changes propagate

Files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
  • echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx
  • echo/frontend/src/components/project/ProjectPortalEditor.tsx
  • echo/frontend/src/components/participant/hooks/useOnboardingCards.ts
echo/frontend/**/src/{theme.tsx,components,routes}/**/*.tsx

📄 CodeRabbit inference engine (echo/frontend/AGENTS.md)

When adding new Mantine components, reference the global Mantine theme in src/theme.tsx with custom primary color palette and component defaults, preferring CSS variables in styles over hardcoded hex values

Files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
  • echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx
  • echo/frontend/src/components/project/ProjectPortalEditor.tsx
echo/frontend/**/src/{components,routes}/**/*.tsx

📄 CodeRabbit inference engine (echo/frontend/AGENTS.md)

UI mutations should surface inline feedback: pair toasts with contextual Mantine Alert components inside modals/forms for errors or warnings

Files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
  • echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx
  • echo/frontend/src/components/project/ProjectPortalEditor.tsx
🧠 Learnings (12)
📓 Common learnings
Learnt from: ussaama
Repo: Dembrane/echo PR: 205
File: echo/frontend/src/lib/query.ts:1444-1506
Timestamp: 2025-07-10T12:48:20.683Z
Learning: ussaama prefers string concatenation over template literals for simple cases where readability is clearer, even when linting tools suggest template literals. Human readability takes precedence over strict linting rules in straightforward concatenation scenarios.
📚 Learning: 2025-08-08T10:39:31.114Z
Learnt from: ussaama
Repo: Dembrane/echo PR: 259
File: echo/frontend/src/components/layout/ParticipantLayout.tsx:33-33
Timestamp: 2025-08-08T10:39:31.114Z
Learning: In echo/frontend/src/components/layout/ParticipantLayout.tsx, prefer using simple pathname.includes("start") and pathname.includes("finish") to control the settings button visibility. No need to switch to segment-based matching or add a useEffect to auto-close the modal for these routes, per ussaama’s preference in PR #259.

Applied to files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
  • echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx
  • echo/frontend/src/components/participant/hooks/useOnboardingCards.ts
  • echo/frontend/src/locales/en-US.po
📚 Learning: 2025-12-05T00:33:33.442Z
Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/routes/**/*.tsx : Use Lingui macros `t` from `lingui/core/macro` and `Trans` from `lingui/react/macro` for localizing UI strings in routed screens

Applied to files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
  • echo/frontend/src/locales/de-DE.po
  • echo/frontend/src/locales/es-ES.po
  • echo/frontend/src/locales/nl-NL.po
  • echo/frontend/src/locales/fr-FR.po
  • echo/frontend/src/locales/en-US.po
📚 Learning: 2025-12-05T00:33:33.442Z
Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/routes/auth/**/*.tsx : Gentle login/logout flows use `useTransitionCurtain().runTransition()` before navigation—animations expect Directus session mutations to await that promise

Applied to files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
📚 Learning: 2025-12-05T00:33:33.442Z
Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/routes/auth/**/*.tsx : Auth hero uses `/public/video/auth-hero.mp4` with `/public/video/auth-hero-poster.jpg` as poster; keep the bright blur overlay consistent when iterating on onboarding screens

Applied to files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
📚 Learning: 2025-12-05T00:33:33.442Z
Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/routes/settings/**/*.tsx : Provide ergonomic navigation in settings-like routes: breadcrumb + back action (ActionIcon + navigate(-1)) with relevant iconography is the default

Applied to files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
📚 Learning: 2025-12-05T00:33:33.442Z
Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/routes/auth/**/*.tsx : Auth surfaces reuse `HeaderView` by passing `isAuthenticated`/`loading` props—avoid rolling bespoke headers inside layouts

Applied to files:

  • echo/frontend/src/routes/participant/ParticipantStart.tsx
📚 Learning: 2025-07-10T12:47:06.269Z
Learnt from: ussaama
Repo: Dembrane/echo PR: 205
File: echo/frontend/src/locales/nl-NL.po:1246-1247
Timestamp: 2025-07-10T12:47:06.269Z
Learning: .po files (gettext internationalization files) are auto-generated by i18n tools like lingui/cli and should not be manually edited. The format of component paths and line numbers in these files is determined by the tool configuration, not manually written. Do not suggest manual edits to .po files.

Applied to files:

  • echo/frontend/src/locales/de-DE.po
  • echo/frontend/src/locales/es-ES.po
  • echo/frontend/src/locales/en-US.po
📚 Learning: 2025-05-30T15:38:44.413Z
Learnt from: ussaama
Repo: Dembrane/echo PR: 169
File: echo/frontend/src/components/project/ProjectPortalEditor.tsx:409-464
Timestamp: 2025-05-30T15:38:44.413Z
Learning: Badge-based selectors in ProjectPortalEditor.tsx: Keyboard navigation enhancements for accessibility are considered optional improvements rather than critical issues. The user acknowledges these suggestions but doesn't prioritize them as blockers.

Applied to files:

  • echo/frontend/src/locales/de-DE.po
  • echo/frontend/src/locales/es-ES.po
  • echo/frontend/src/locales/nl-NL.po
  • echo/frontend/src/locales/fr-FR.po
  • echo/frontend/src/locales/en-US.po
📚 Learning: 2025-10-28T13:47:02.926Z
Learnt from: ussaama
Repo: Dembrane/echo PR: 350
File: echo/frontend/src/components/participant/ParticipantConversationText.tsx:82-85
Timestamp: 2025-10-28T13:47:02.926Z
Learning: In text mode (echo/frontend/src/components/participant/ParticipantConversationText.tsx), participants only submit PORTAL_TEXT chunks (no audio). The “Finish” button is shown only after at least one text message is saved to Directus.

Applied to files:

  • echo/frontend/src/locales/de-DE.po
  • echo/frontend/src/locales/nl-NL.po
  • echo/frontend/src/locales/fr-FR.po
  • echo/frontend/src/locales/en-US.po
📚 Learning: 2025-12-05T00:33:33.442Z
Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/lib/{directus,api}.ts : Standardize Directus error handling and add localization polish in `src/lib/directus.ts`; custom Directus POSTs should call `directus.request` with a function signature rather than `restRequest`

Applied to files:

  • echo/directus/sync/collections/operations.json
📚 Learning: 2025-09-16T08:34:38.109Z
Learnt from: ussaama
Repo: Dembrane/echo PR: 293
File: echo/frontend/src/components/chat/ChatTemplatesMenu.tsx:15-16
Timestamp: 2025-09-16T08:34:38.109Z
Learning: In ChatTemplatesMenu.tsx, titles are preferred over IDs for template selection logic since titles are unique one-liners and work effectively as identifiers. IDs were added primarily for better list rendering rather than selection purposes.

Applied to files:

  • echo/frontend/src/locales/en-US.po
🧬 Code graph analysis (1)
echo/frontend/src/components/participant/hooks/useOnboardingCards.ts (1)
echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx (1)
  • LanguageCards (41-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-check-server
🔇 Additional comments (20)
echo/frontend/src/locales/es-ES.po (1)

1-4036: LGTM! Auto-generated i18n updates ship clean.

This .po file is auto-generated by @lingui/cli and reflects the translation updates for the onboarding cards refactor. The new tutorial options (skip-consent, none, basic, advanced) are properly translated, obsolete entries are correctly marked with #~, and the coordinate with ProjectPortalEditor.tsx changes tracks perfectly.

Based on learnings, manual edits to .po files are not the way—these get regenerated on the next extraction run. Translation quality improvements should flow through the source strings, not patched here.

echo/frontend/src/locales/fr-FR.po (2)

229-236: LGTM, localization strings are on point.

New tutorial slug options (skip-consent, none, basic, advanced) properly mapped to French translations. The obsolete entry preservation with #~ markers is solid for history tracking. Ship it.


884-887: New tutorial option translations look clean.

"Par défaut - Pas de tutoriel" and "Passer la carte de confidentialité" are correctly localized for the new slug-based flow. Consistent with the ProjectPortalEditor changes.

Also applies to: 3070-3072

echo/frontend/src/components/participant/ParticipantOnboardingCards.tsx (3)

45-47: Props simplified nicely, typing is clean.

Removing the initialCards prop in favor of deriving from project is the right call. Single source of truth = less surface area for bugs.


76-77: Default fallback to "none" is consistent.

This matches the normalization logic in ProjectPortalEditor. Consider extracting "none" as a constant if this default appears in more places.


206-210: Fallback to empty array is defensive coding, nice.

If language doesn't match any key, you get []allSlides = []currentCard = undefined → fallback UI at line 221. Solid error boundary pattern.

echo/frontend/src/routes/participant/ParticipantStart.tsx (1)

26-30: Loading state simplified, clean.

Single dependency on isLoadingProject instead of multiple loading flags. Reduced complexity = reduced bugs. 10x approved.

echo/frontend/src/components/project/ProjectPortalEditor.tsx (2)

230-236: Slug normalization is solid defensive coding.

Lowercase conversion + allowlist validation + fallback to "none" handles legacy data and edge cases cleanly. This is the right pattern for migrating schema changes.


529-545: New tutorial options aligned with backend schema.

skip-consent, none, basic, advanced - the full slug matrix. Labels use Lingui t macro correctly. As per coding guidelines, this is using proper localization patterns.

echo/directus/sync/collections/operations.json (1)

17-21: No action needed — all UUID references are internally consistent.

All resolve and reject UUIDs in the file correctly reference valid _syncId values within the same operations.json. The ID re-mapping is normal Directus sync behavior, and there are no dangling flow references or deployment risks.

Likely an incorrect or invalid review comment.

echo/frontend/src/locales/nl-NL.po (2)

536-545: Onboarding button labels + “Ready” copy look tight and consistent.

The new participant onboarding button strings (Terug naar microfoon, Terug, Doorgaan, Volgende, Klaar om te beginnen) read naturally in Dutch and line up with the updated card flow. No changes needed here.

Also applies to: 763-768, 2049-2052, 2706-2709


244-247: Portal editor + tutorial option labels are aligned with the new slug-based onboarding.

The updated labels for tutorial intensity and portal content ("Advanced (Tips and best practices)", "Basis (Alleen essentiële tutorial slides)", "Standaard - Geen tutorial (Alleen privacy verklaringen)", "Concrete onderwerpen", page content/title/help texts, and language/mode labels) all read well and match the intended behavior from the PR description. This should keep the Dutch portal UX in lockstep with the new onboarding configuration.

Also applies to: 553-556, 940-947, 651-652, 710-713, 975-976, 1005-1008, 1790-1793, 1865-1868, 1965-1968, 2001-2004, 2303-2306, 2311-2314

echo/frontend/src/locales/de-DE.po (3)

109-115: Portal editor + onboarding config strings line up with new card behavior

The new PortalEditor strings for tutorial selection, language/mode, concrete topics, reply prompt, portal/thank-you pages, report notifications, and the live preview all hang together with the new onboarding-card logic. Placeholders and grammar look sane, and the generic {0} entry is reused correctly for the new editor context. Nothing blocking from a product/i18n perspective—this will scale fine with the JSON-based onboarding config.

Based on learnings, I’m assuming these were generated via the Lingui flow and not edited by hand.

Also applies to: 229-236, 496-503, 651-656, 703-707, 745-753, 771-779, 877-884, 1003-1006, 1088-1121, 1134-1137, 1459-1463, 1670-1673, 1745-1748, 1859-1862, 2168-2188, 2430-2437, 3435-3446, 3450-3453


478-482: Participant onboarding card buttons look consistent with the new flow

The new ParticipantOnboardingCards keys for back (including mic-check–specific back), continue, next, “ready to begin,” and the mic-check skip all resolve to short, consistent German labels (Zurück, Weiter, Überspringen, etc.). That matches the revamped onboarding-card flow (skip-consent, basic, advanced) without introducing weird phrasing. Looks tight.

Based on learnings, treating these as the Lingui output, not something to tweak directly.

Also applies to: 484-488, 707-711, 1929-1932, 2553-2556, 3056-3059


280-283: At least one untranslated i18n key remains in this file

The key participant.modal.refine.info.available.in (line 3411) still has an English msgstr placeholder. Additionally, dashboard.dembrane.concrete.description (line 1102) has msgstr equal to msgid, indicating no translation. While some strings in the originally cited ranges have since been properly translated, these two keys should be added to the localization backlog to ensure the German UI displays correctly when these features are fully exposed.

⛔ Skipped due to learnings
Learnt from: ussaama
Repo: Dembrane/echo PR: 180
File: echo/frontend/src/locales/es-ES.ts:1-1
Timestamp: 2025-06-06T05:43:44.935Z
Learning: When users indicate they have already addressed translation issues in locale files, verify the current state before assuming the issue persists. Translation files are often updated in bulk and previously flagged English text may have been properly localized.
echo/frontend/src/locales/en-US.po (1)

478-480: LGTM! Locale strings align with the new slug-based onboarding flow.

The new translation entries for tutorial options match the slug values used in the code. Auto-generated tooling handled the reordering and updates correctly.

Also applies to: 531-543, 1164-1167

echo/frontend/src/components/participant/hooks/useOnboardingCards.ts (4)

1-12: LGTM! Icon imports are on point.

All the new lucide-react icons are used in the tutorial slides. Clean import organization.


404-405: Language fallback implementation is clean.

Proper fallback chain: requested language → en-US → empty array. Handles missing translations gracefully.

Also applies to: 923-924


927-1044: Privacy card logic LGTM.

The getPrivacyCard function is solid and correctly handles all supported languages with proper null returns for unsupported locales.


16-51: [Your rewritten review comment text here]
[Exactly ONE classification tag]

Comment on lines +14 to 24
"note": null,
"options": {
"choices": [
{
"text": "Skip Consent",
"value": "skip-consent"
},
{
"text": "None",
"value": "None"
"value": "none"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json

Repository: Dembrane/echo

Length of output: 1498


🏁 Script executed:

rg -i "default_conversation_tutorial_slug" --type ts --type tsx --type js --type jsx -A 5 -B 2

Repository: Dembrane/echo

Length of output: 84


🏁 Script executed:

rg "default_conversation_tutorial_slug" -A 5 -B 2

Repository: Dembrane/echo

Length of output: 14171


🏁 Script executed:

rg "None.*basic.*advanced" --type ts -B 5 -A 5

Repository: Dembrane/echo

Length of output: 1087


🏁 Script executed:

fd "migration" -t f | head -20

Repository: Dembrane/echo

Length of output: 89


🏁 Script executed:

cat echo/docs/database_migrations.md

Repository: Dembrane/echo

Length of output: 632


🏁 Script executed:

head -20 echo/frontend/src/lib/typesDirectus.d.ts

Repository: Dembrane/echo

Length of output: 840


🏁 Script executed:

git log --oneline echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json | head -10

Repository: Dembrane/echo

Length of output: 95


🏁 Script executed:

git log -p --follow -1 echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json | head -100

Repository: Dembrane/echo

Length of output: 39


🏁 Script executed:

grep -A 2 -B 2 '"None"' echo/frontend/src/routes/project/ProjectsHome.tsx

Repository: Dembrane/echo

Length of output: 261


Update TypeScript types and new project defaults for consistency; legacy data is safely handled by frontend.

The lowercase choice values ("skip-consent", "none", "basic", "advanced") and default_value: "none" align well with the frontend's slug normalization logic. However, a few inconsistencies need cleanup:

  • ProjectsHome.tsx still creates new projects with "None" (uppercase), which will be normalized by the frontend's toLowerCase() validation fallback, but should use "none" directly.
  • typesDirectus.d.ts defines the type as "None" | "basic" | "advanced" (missing "skip-consent" and using uppercase "None"); update it to match the schema choices.
  • Existing rows with "None" (uppercase) will appear as unlisted choices in Directus UI, though the frontend safely defaults them to "none". If you want a clean admin experience, backfill "None""none" in the database.
🤖 Prompt for AI Agents
In
echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json
around lines 14-24, the schema choices are lowercase but the code/types still
reference an uppercase "None" and are missing "skip-consent"; update
ProjectsHome.tsx to create new projects with default_value "none" (lowercase),
update typesDirectus.d.ts to include "none" | "skip-consent" | "basic" |
"advanced" (all lowercase) so TypeScript matches the schema, and optionally run
a database backfill to convert existing "None" values to "none" for a consistent
Directus admin UI.

Comment on lines +40 to +41
const privacyCard = getPrivacyCard(lang);
return [...tutorialCards, ...(privacyCard ? [privacyCard] : [])];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Privacy card integration is inconsistent between basic and advanced.

In basic (line 41), privacy card is appended as a separate section:

return [...tutorialCards, ...(privacyCard ? [privacyCard] : [])];

In advanced (lines 477, 579, etc), privacy slides are spread into the existing Privacy section:

...(getPrivacyCard("de-DE")?.slides || [])

This works but creates inconsistency. Consider unifying the approach or document why they differ.

Also applies to: 477-477, 579-579, 680-680, 783-783, 885-885

Comment on lines +53 to 925
const getBasicTutorialCards = (lang: string): LanguageCards[string] => {
const tutorialCards: Record<string, LanguageCards[string]> = {
"de-DE": [
{
section: "Willkommen",
slides: [
{
content:
"Nehmen Sie Ihre Stimme auf, um Fragen zu beantworten und Einfluss zu nehmen.",
cta: "Los geht's!",
extraHelp:
"Dies ist ein Mini-Tutorial. Nutzen Sie die Vor- und Zurück-Tasten zur Navigation. Nach Abschluss gelangen Sie zum Aufnahmeportal.",
icon: PartyPopper,
title: "Willkommen bei Dembrane!",
},
{
content:
"Dembrane hilft Menschen, einfach Input von großen Gruppen zu sammeln.",
cta: "Mehr erfahren",
extraHelp:
"Ob Feedback für eine Kommune, Input im Arbeitsumfeld oder Teilnahme an Forschung – Ihre Stimme zählt!",
icon: Orbit,
title: "Was ist Dembrane?",
},
{
content:
"Beantworten Sie Fragen in Ihrem eigenen Tempo durch Sprechen oder Tippen.",
cta: "Weiter",
extraHelp:
"Spracheingabe ist unser bevorzugter Modus, da sie natürlichere und detailliertere Antworten ermöglicht. Tippen steht immer als Alternative zur Verfügung.",
icon: Speech,
title: "Sagen Sie einfach Ihre Meinung",
},
{
content: "Dembrane macht in Gruppen mehr Spaß!",
cta: "Weiter",
extraHelp:
"Dembrane macht mehr Spaß, wenn Sie jemanden finden, um die Fragen gemeinsam zu besprechen und Ihr Gespräch aufzunehmen. Wir können nicht sagen, wer was gesagt hat, nur welche Ideen geteilt wurden.",
icon: MessagesSquare,
title: "Allein oder in der Gruppe",
},
],
},
{
section: "Wie es funktioniert",
slides: [
{
content:
"Sie erhalten die Fragen, sobald Sie im Aufnahmeportal sind.",
cta: "Verstanden",
extraHelp:
"Die Fragen variieren je nach den Bedürfnissen des Gastgebers. Sie können sich auf kommunale Themen, Arbeitserfahrungen oder Forschungsthemen beziehen. Wenn es keine spezifischen Fragen gibt, können Sie gerne alle Gedanken oder Anliegen teilen.",
icon: HelpCircle,
title: "Fragerunde",
},
],
},
{
section: "Datenschutz",
slides: [
{
content:
"Als Aufnehmer haben Sie die Kontrolle über das, was Sie teilen.",
cta: "Mehr erfahren",
extraHelp:
"Vermeiden Sie die Weitergabe von Details, die Sie dem Gastgeber nicht mitteilen möchten. Seien Sie achtsam und nehmen Sie andere nicht ohne deren Zustimmung auf.",
icon: Lock,
title: "Datenschutz ist wichtig",
},
],
},
],
"en-US": [
{
section: "Welcome",
slides: [
{
content:
"Record your voice to answer questions and make an impact.",
cta: "Let's go!",
extraHelp:
"This is a mini-tutorial. Use the previous and next buttons to navigate. Once completed, you'll enter the recording portal.",
icon: PartyPopper,
title: "Welcome to Dembrane!",
},
{
content:
"Dembrane helps people gather input from large groups easily.",
cta: "Tell me more",
extraHelp:
"Whether it's feedback for a local municipality, input in a work setting, or participation in research, your voice matters!",
icon: Orbit,
title: "What is Dembrane?",
},
{
content:
"Answer questions in your own time by speaking or typing.",
cta: "Next",
extraHelp:
"Voice input is our primary mode, allowing for more natural and detailed responses. Typing is always available as a backup.",
icon: Speech,
title: "Just Speak Your Mind",
},
{
content: "Dembrane is more fun in groups!",
cta: "Next",
extraHelp:
"Dembrane is more fun when you find someone to discuss the questions together and record your conversation. We can't tell who said what, just what ideas were shared.",
icon: MessagesSquare,
title: "Solo or in a Group",
},
],
},
{
section: "How it works",
slides: [
{
content:
"You'll receive the questions once in the recording portal.",
cta: "Got it",
extraHelp:
"Questions vary based on the host's needs. They could be about community issues, work experiences, or research topics. If there are no specific questions, you're free to share any thoughts or concerns.",
icon: HelpCircle,
title: "Question Time",
},
],
},
{
section: "Privacy",
slides: [
{
content: "As the recorder, you are in control of what you share.",
cta: "Tell me more",
extraHelp:
"Avoid sharing details you don't want the host to know. Be mindful and don't record others without their consent.",
icon: Lock,
title: "Privacy Matters",
},
],
},
],
"es-ES": [
{
section: "Bienvenido",
slides: [
{
content:
"Graba tu voz para responder preguntas y generar impacto.",
cta: "¡Vamos!",
extraHelp:
"Este es un mini-tutorial. Usa los botones de anterior y siguiente para navegar. Una vez completado, entrarás al portal de grabación.",
icon: PartyPopper,
title: "¡Bienvenido a Dembrane!",
},
{
content:
"Dembrane ayuda a las personas a recopilar aportaciones de grandes grupos fácilmente.",
cta: "Cuéntame más",
extraHelp:
"Ya sea retroalimentación para una municipalidad local, aportaciones en el trabajo o participación en investigación, ¡tu voz importa!",
icon: Orbit,
title: "¿Qué es Dembrane?",
},
{
content:
"Responde preguntas a tu propio ritmo hablando o escribiendo.",
cta: "Siguiente",
extraHelp:
"La entrada de voz es nuestro modo principal, permitiendo respuestas más naturales y detalladas. Escribir siempre está disponible como respaldo.",
icon: Speech,
title: "Solo Di Lo Que Piensas",
},
{
content: "¡Dembrane es más divertido en grupos!",
cta: "Siguiente",
extraHelp:
"Dembrane es más divertido cuando encuentras a alguien para discutir las preguntas juntos y grabar su conversación. No podemos distinguir quién dijo qué, solo qué ideas se compartieron.",
icon: MessagesSquare,
title: "Solo o en Grupo",
},
],
},
{
section: "Cómo funciona",
slides: [
{
content:
"Recibirás las preguntas una vez en el portal de grabación.",
cta: "Entendido",
extraHelp:
"Las preguntas varían según las necesidades del anfitrión. Pueden ser sobre temas comunitarios, experiencias laborales o temas de investigación. Si no hay preguntas específicas, eres libre de compartir cualquier pensamiento o inquietud.",
icon: HelpCircle,
title: "Hora de Preguntas",
},
],
},
{
section: "Privacidad",
slides: [
{
content: "Como grabador, tú controlas lo que compartes.",
cta: "Cuéntame más",
extraHelp:
"Evita compartir detalles que no quieras que el anfitrión conozca. Sé consciente y no grabes a otros sin su consentimiento.",
icon: Lock,
title: "La Privacidad Importa",
},
],
},
],
"fr-FR": [
{
section: "Bienvenue",
slides: [
{
content:
"Enregistrez votre voix pour répondre aux questions et avoir un impact.",
cta: "C'est parti !",
extraHelp:
"Ceci est un mini-tutoriel. Utilisez les boutons précédent et suivant pour naviguer. Une fois terminé, vous entrerez dans le portail d'enregistrement.",
icon: PartyPopper,
title: "Bienvenue sur Dembrane !",
},
{
content:
"Dembrane aide les gens à recueillir facilement les contributions de grands groupes.",
cta: "Dites-m'en plus",
extraHelp:
"Qu'il s'agisse de commentaires pour une municipalité locale, de contributions dans un cadre professionnel ou de participation à une recherche, votre voix compte !",
icon: Orbit,
title: "Qu'est-ce que Dembrane ?",
},
{
content:
"Répondez aux questions à votre rythme en parlant ou en tapant.",
cta: "Suivant",
extraHelp:
"La saisie vocale est notre mode principal, permettant des réponses plus naturelles et détaillées. La saisie au clavier est toujours disponible en secours.",
icon: Speech,
title: "Dites Simplement Ce Que Vous Pensez",
},
{
content: "Dembrane est plus amusant en groupe !",
cta: "Suivant",
extraHelp:
"Dembrane est plus amusant lorsque vous trouvez quelqu'un pour discuter des questions ensemble et enregistrer votre conversation. Nous ne pouvons pas dire qui a dit quoi, juste quelles idées ont été partagées.",
icon: MessagesSquare,
title: "Seul ou en Groupe",
},
],
},
{
section: "Comment ça marche",
slides: [
{
content:
"Vous recevrez les questions une fois dans le portail d'enregistrement.",
cta: "Compris",
extraHelp:
"Les questions varient en fonction des besoins de l'hôte. Elles peuvent concerner des problèmes communautaires, des expériences professionnelles ou des sujets de recherche. S'il n'y a pas de questions spécifiques, vous êtes libre de partager vos pensées ou préoccupations.",
icon: HelpCircle,
title: "Heure des Questions",
},
],
},
{
section: "Confidentialité",
slides: [
{
content:
"En tant qu'enregistreur, vous contrôlez ce que vous partagez.",
cta: "Dites-m'en plus",
extraHelp:
"Évitez de partager des détails que vous ne voulez pas que l'hôte connaisse. Soyez attentif et n'enregistrez pas les autres sans leur consentement.",
icon: Lock,
title: "La Confidentialité Compte",
},
],
},
],
"nl-NL": [
{
section: "Welkom",
slides: [
{
content:
"Neem je stem op om vragen te beantwoorden en impact te maken.",
cta: "Aan de slag!",
extraHelp:
"Dit is een mini-handleiding. Gebruik de knoppen om te navigeren. Na afloop van de handleiding kom je in de opnameportaal terecht.",
icon: PartyPopper,
title: "Welkom bij Dembrane!",
},
{
content:
"Dembrane helpt mensen gemakkelijk input van grote groepen te verzamelen.",
cta: "Vertel me meer",
extraHelp:
"Of het nu gaat om feedback voor de gemeente, input op het werk, of deelname aan onderzoek, jouw stem telt!",
icon: Orbit,
title: "Wat is Dembrane?",
},
{
content:
"Beantwoord vragen in je eigen tempo door te spreken of te typen.",
cta: "Volgende",
extraHelp:
"Spraak is onze voorkeursmethode, omdat het natuurlijker en gedetailleerder is. Typen kan natuurlijk ook altijd.",
icon: Speech,
title: "Zeg het maar",
},
{
content: "Dembrane is leuker met een groep!",
cta: "Volgende",
extraHelp:
"Dembrane is leuker als je iemand vindt om de vragen samen te bespreken en jullie gesprek op te nemen. We kunnen niet horen wie wat zei, alleen welke ideeën er gedeeld zijn.",
icon: MessagesSquare,
title: "Alleen of in een groep",
},
],
},
{
section: "Hoe het werkt",
slides: [
{
content:
"Je krijgt de vragen te zien zodra je in de opnameportal bent.",
cta: "Begrepen",
extraHelp:
"Vragen variëren afhankelijk van wat de organisator wil weten. Het kan gaan over de buurt, werkervaringen, of onderzoeksonderwerpen. Als er geen specifieke vragen zijn, kun je gewoon je gedachten of zorgen delen.",
icon: HelpCircle,
title: "Vragenronde",
},
],
},
{
section: "Privacy",
slides: [
{
content: "Als opnemer heb je zelf controle over wat je deelt.",
cta: "Vertel me meer",
extraHelp:
"Vermijd het delen van details die je niet met de organisator wilt delen. Wees voorzichtig en neem anderen niet op zonder hun toestemming.",
icon: Lock,
title: "Privacy is belangrijk",
},
],
},
],
};

// Fallback to English if language not found
return tutorialCards[lang] || tutorialCards["en-US"] || [];
};

const getAdvancedTutorialCards = (lang: string): LanguageCards[string] => {
const tutorialCards: Record<string, LanguageCards[string]> = {
"de-DE": [
{
section: "Willkommen",
slides: [
{
content:
"Nehmen Sie Ihre Stimme auf, um Fragen zu beantworten und Einfluss zu nehmen.",
cta: "Los geht's!",
extraHelp:
"Dies ist ein Mini-Tutorial. Nutzen Sie die Vor- und Zurück-Tasten zur Navigation. Nach Abschluss gelangen Sie zum Aufnahmeportal.",
icon: PartyPopper,
title: "Willkommen bei Dembrane!",
},
{
content:
"Dembrane hilft Menschen, einfach Input von großen Gruppen zu sammeln.",
cta: "Mehr erfahren",
extraHelp:
"Ob Feedback für eine Kommune, Input im Arbeitsumfeld oder Teilnahme an Forschung – Ihre Stimme zählt!",
icon: Orbit,
title: "Was ist Dembrane?",
},
{
content:
"Beantworten Sie Fragen in Ihrem eigenen Tempo durch Sprechen oder Tippen.",
cta: "Weiter",
extraHelp:
"Spracheingabe ist unser bevorzugter Modus, da sie natürlichere und detailliertere Antworten ermöglicht. Tippen steht immer als Alternative zur Verfügung.",
icon: Speech,
title: "Sagen Sie einfach Ihre Meinung",
},
{
content: "Dembrane macht in Gruppen mehr Spaß!",
cta: "Weiter",
extraHelp:
"Dembrane macht mehr Spaß, wenn Sie jemanden finden, um die Fragen gemeinsam zu besprechen und Ihr Gespräch aufzunehmen. Wir können nicht sagen, wer was gesagt hat, nur welche Ideen geteilt wurden.",
icon: MessagesSquare,
title: "Allein oder in der Gruppe",
},
],
},
{
section: "Wie es funktioniert",
slides: [
{
content:
"Sie erhalten die Fragen, sobald Sie im Aufnahmeportal sind.",
cta: "Verstanden",
extraHelp:
"Die Fragen variieren je nach den Bedürfnissen des Gastgebers. Sie können sich auf kommunale Themen, Arbeitserfahrungen oder Forschungsthemen beziehen. Wenn es keine spezifischen Fragen gibt, können Sie gerne alle Gedanken oder Anliegen teilen.",
icon: HelpCircle,
title: "Fragerunde",
},
],
},
{
section: "Datenschutz",
slides: [
{
content:
"Als Aufnehmer haben Sie die Kontrolle über das, was Sie teilen.",
cta: "Mehr erfahren",
extraHelp:
"Vermeiden Sie die Weitergabe von Details, die Sie dem Gastgeber nicht mitteilen möchten. Seien Sie achtsam und nehmen Sie andere nicht ohne deren Zustimmung auf.",
icon: Lock,
title: "Datenschutz ist wichtig",
},
...(getPrivacyCard("de-DE")?.slides || []),
],
},
{
section: "Best Practices",
slides: [
{
content:
"Stellen Sie sich vor, Dembrane ist auf Lautsprecher mit Ihnen. Wenn Sie sich selbst hören können, ist alles in Ordnung.",
cta: "Verstanden",
extraHelp:
"Etwas Hintergrundgeräusch ist in Ordnung, solange klar ist, wer spricht.",
icon: Volume2,
title: "Hintergrundgeräusche reduzieren",
},
{
content:
"Stellen Sie eine stabile Verbindung für eine reibungslose Aufnahme sicher.",
cta: "Bereit!",
extraHelp:
"WLAN oder gute mobile Daten funktionieren am besten. Wenn Ihre Verbindung abbricht, keine Sorge. Sie können immer dort weitermachen, wo Sie aufgehört haben.",
icon: Wifi,
title: "Starke Internetverbindung",
},
{
content:
"Vermeiden Sie Unterbrechungen, indem Sie Ihr Gerät entsperrt halten. Wenn es sich sperrt, entsperren Sie es einfach und fahren Sie fort.",
cta: "Okay",
extraHelp:
"Dembrane versucht, Ihr Gerät aktiv zu halten, aber manchmal können Geräte dies überschreiben. Sie können Ihre Geräteeinstellungen anpassen, um länger entsperrt zu bleiben, wenn nötig.",
icon: Smartphone,
title: "Gerät nicht sperren!",
},
],
},
],
"en-US": [
{
section: "Welcome",
slides: [
{
content:
"Record your voice to answer questions and make an impact.",
cta: "Let's go!",
extraHelp:
"This is a mini-tutorial. Use the previous and next buttons to navigate. Once completed, you'll enter the recording portal.",
icon: PartyPopper,
title: "Welcome to Dembrane!",
},
{
content:
"Dembrane helps people gather input from large groups easily.",
cta: "Tell me more",
extraHelp:
"Whether it's feedback for a local municipality, input in a work setting, or participation in research, your voice matters!",
icon: Orbit,
title: "What is Dembrane?",
},
{
content:
"Answer questions in your own time by speaking or typing.",
cta: "Next",
extraHelp:
"Voice input is our primary mode, allowing for more natural and detailed responses. Typing is always available as a backup.",
icon: Speech,
title: "Just Speak Your Mind",
},
{
content: "Dembrane is more fun in groups!",
cta: "Next",
extraHelp:
"Dembrane is more fun when you find someone to discuss the questions together and record your conversation. We can't tell who said what, just what ideas were shared.",
icon: MessagesSquare,
title: "Solo or in a Group",
},
],
},
{
section: "How it works",
slides: [
{
content:
"You'll receive the questions once in the recording portal.",
cta: "Got it",
extraHelp:
"Questions vary based on the host's needs. They could be about community issues, work experiences, or research topics. If there are no specific questions, you're free to share any thoughts or concerns.",
icon: HelpCircle,
title: "Question Time",
},
],
},
{
section: "Privacy",
slides: [
{
content: "As the recorder, you are in control of what you share.",
cta: "Tell me more",
extraHelp:
"Avoid sharing details you don't want the host to know. Be mindful and don't record others without their consent.",
icon: Lock,
title: "Privacy Matters",
},
...(getPrivacyCard("en-US")?.slides || []),
],
},
{
section: "Best Practices",
slides: [
{
content:
"Imagine Dembrane is on speakerphone with you. If you can hear yourself, you're good to go.",
cta: "Noted",
extraHelp:
"Some background noise is okay, as long as who is speaking is clear.",
icon: Volume2,
title: "Reduce Background Noise",
},
{
content: "Ensure a stable connection for smooth recording.",
cta: "Ready!",
extraHelp:
"Wi-Fi or good mobile data works best. If your connection drops, don't worry. You can always restart where you left off.",
icon: Wifi,
title: "Strong Internet Connection",
},
{
content:
"Prevent interruptions by keeping your device unlocked. If it locks, just unlock and continue.",
cta: "Okay",
extraHelp:
"Dembrane tries to keep your device active, but sometimes devices can override it, for example if you have low power mode active. You can adjust your device settings to stay unlocked longer if needed.",
icon: Smartphone,
title: "Don't lock your device!",
},
],
},
],
"es-ES": [
{
section: "Bienvenido",
slides: [
{
content:
"Graba tu voz para responder preguntas y generar impacto.",
cta: "¡Vamos!",
extraHelp:
"Este es un mini-tutorial. Usa los botones de anterior y siguiente para navegar. Una vez completado, entrarás al portal de grabación.",
icon: PartyPopper,
title: "¡Bienvenido a Dembrane!",
},
{
content:
"Dembrane ayuda a las personas a recopilar aportaciones de grandes grupos fácilmente.",
cta: "Cuéntame más",
extraHelp:
"Ya sea retroalimentación para una municipalidad local, aportaciones en el trabajo o participación en investigación, ¡tu voz importa!",
icon: Orbit,
title: "¿Qué es Dembrane?",
},
{
content:
"Responde preguntas a tu propio ritmo hablando o escribiendo.",
cta: "Siguiente",
extraHelp:
"La entrada de voz es nuestro modo principal, permitiendo respuestas más naturales y detalladas. Escribir siempre está disponible como respaldo.",
icon: Speech,
title: "Solo Di Lo Que Piensas",
},
{
content: "¡Dembrane es más divertido en grupos!",
cta: "Siguiente",
extraHelp:
"Dembrane es más divertido cuando encuentras a alguien para discutir las preguntas juntos y grabar su conversación. No podemos distinguir quién dijo qué, solo qué ideas se compartieron.",
icon: MessagesSquare,
title: "Solo o en Grupo",
},
],
},
{
section: "Cómo funciona",
slides: [
{
content:
"Recibirás las preguntas una vez en el portal de grabación.",
cta: "Entendido",
extraHelp:
"Las preguntas varían según las necesidades del anfitrión. Pueden ser sobre temas comunitarios, experiencias laborales o temas de investigación. Si no hay preguntas específicas, eres libre de compartir cualquier pensamiento o inquietud.",
icon: HelpCircle,
title: "Hora de Preguntas",
},
],
},
{
section: "Privacidad",
slides: [
{
content: "Como grabador, tú controlas lo que compartes.",
cta: "Cuéntame más",
extraHelp:
"Evita compartir detalles que no quieras que el anfitrión conozca. Sé consciente y no grabes a otros sin su consentimiento.",
icon: Lock,
title: "La Privacidad Importa",
},
...(getPrivacyCard("es-ES")?.slides || []),
],
},
{
section: "Mejores Prácticas",
slides: [
{
content:
"Imagina que Dembrane está en altavoz contigo. Si puedes escucharte, estás listo.",
cta: "Entendido",
extraHelp:
"Un poco de ruido de fondo está bien, siempre que se entienda quién está hablando.",
icon: Volume2,
title: "Reduce el Ruido de Fondo",
},
{
content:
"Asegura una conexión estable para una grabación fluida.",
cta: "¡Listo!",
extraHelp:
"Wi-Fi o buenos datos móviles funcionan mejor. Si se cae tu conexión, no te preocupes. Siempre puedes reiniciar donde lo dejaste.",
icon: Wifi,
title: "Conexión a Internet Fuerte",
},
{
content:
"Evita interrupciones manteniendo tu dispositivo desbloqueado. Si se bloquea, simplemente desbloquéalo y continúa.",
cta: "De acuerdo",
extraHelp:
"Dembrane intenta mantener tu dispositivo activo, pero a veces los dispositivos pueden anularlo. Puedes ajustar la configuración de tu dispositivo para permanecer desbloqueado más tiempo si es necesario.",
icon: Smartphone,
title: "¡No bloquees tu dispositivo!",
},
],
},
],
"fr-FR": [
{
section: "Bienvenue",
slides: [
{
content:
"Enregistrez votre voix pour répondre aux questions et avoir un impact.",
cta: "C'est parti !",
extraHelp:
"Ceci est un mini-tutoriel. Utilisez les boutons précédent et suivant pour naviguer. Une fois terminé, vous entrerez dans le portail d'enregistrement.",
icon: PartyPopper,
title: "Bienvenue sur Dembrane !",
},
{
content:
"Dembrane aide les gens à recueillir facilement les contributions de grands groupes.",
cta: "Dites-m'en plus",
extraHelp:
"Qu'il s'agisse de commentaires pour une municipalité locale, de contributions dans un cadre professionnel ou de participation à une recherche, votre voix compte !",
icon: Orbit,
title: "Qu'est-ce que Dembrane ?",
},
{
content:
"Répondez aux questions à votre rythme en parlant ou en tapant.",
cta: "Suivant",
extraHelp:
"La saisie vocale est notre mode principal, permettant des réponses plus naturelles et détaillées. La saisie au clavier est toujours disponible en secours.",
icon: Speech,
title: "Dites Simplement Ce Que Vous Pensez",
},
{
content: "Dembrane est plus amusant en groupe !",
cta: "Suivant",
extraHelp:
"Dembrane est plus amusant lorsque vous trouvez quelqu'un pour discuter des questions ensemble et enregistrer votre conversation. Nous ne pouvons pas dire qui a dit quoi, juste quelles idées ont été partagées.",
icon: MessagesSquare,
title: "Seul ou en Groupe",
},
],
},
{
section: "Comment ça marche",
slides: [
{
content:
"Vous recevrez les questions une fois dans le portail d'enregistrement.",
cta: "Compris",
extraHelp:
"Les questions varient en fonction des besoins de l'hôte. Elles peuvent concerner des problèmes communautaires, des expériences professionnelles ou des sujets de recherche. S'il n'y a pas de questions spécifiques, vous êtes libre de partager vos pensées ou préoccupations.",
icon: HelpCircle,
title: "Heure des Questions",
},
],
},
{
section: "Confidentialité",
slides: [
{
content:
"En tant qu'enregistreur, vous contrôlez ce que vous partagez.",
cta: "Dites-m'en plus",
extraHelp:
"Évitez de partager des détails que vous ne voulez pas que l'hôte connaisse. Soyez attentif et n'enregistrez pas les autres sans leur consentement.",
icon: Lock,
title: "La Confidentialité Compte",
},
...(getPrivacyCard("fr-FR")?.slides || []),
],
},
{
section: "Meilleures Pratiques",
slides: [
{
content:
"Imaginez que Dembrane est sur haut-parleur avec vous. Si vous pouvez vous entendre, c'est bon.",
cta: "Noté",
extraHelp:
"Un peu de bruit de fond est acceptable, tant qu'on sait qui parle.",
icon: Volume2,
title: "Réduire le Bruit de Fond",
},
{
content:
"Assurez une connexion stable pour un enregistrement fluide.",
cta: "Prêt !",
extraHelp:
"Le Wi-Fi ou de bonnes données mobiles fonctionnent mieux. Si votre connexion tombe, ne vous inquiétez pas. Vous pouvez toujours reprendre là où vous vous êtes arrêté.",
icon: Wifi,
title: "Connexion Internet Forte",
},
{
content:
"Évitez les interruptions en gardant votre appareil déverrouillé. S'il se verrouille, déverrouillez-le simplement et continuez.",
cta: "D'accord",
extraHelp:
"Dembrane essaie de garder votre appareil actif, mais parfois les appareils peuvent l'annuler. Vous pouvez ajuster les paramètres de votre appareil pour rester déverrouillé plus longtemps si nécessaire.",
icon: Smartphone,
title: "Ne verrouillez pas votre appareil !",
},
],
},
],
"nl-NL": [
{
section: "Welkom",
slides: [
{
content:
"Neem je stem op om vragen te beantwoorden en impact te maken.",
cta: "Aan de slag!",
extraHelp:
"Dit is een mini-handleiding. Gebruik de knoppen om te navigeren. Na afloop van de handleiding kom je in de opnameportaal terecht.",
icon: PartyPopper,
title: "Welkom bij Dembrane!",
},
{
content:
"Dembrane helpt mensen gemakkelijk input van grote groepen te verzamelen.",
cta: "Vertel me meer",
extraHelp:
"Of het nu gaat om feedback voor de gemeente, input op het werk, of deelname aan onderzoek, jouw stem telt!",
icon: Orbit,
title: "Wat is Dembrane?",
},
{
content:
"Beantwoord vragen in je eigen tempo door te spreken of te typen.",
cta: "Volgende",
extraHelp:
"Spraak is onze voorkeursmethode, omdat het natuurlijker en gedetailleerder is. Typen kan natuurlijk ook altijd.",
icon: Speech,
title: "Zeg het maar",
},
{
content: "Dembrane is leuker met een groep!",
cta: "Volgende",
extraHelp:
"Dembrane is leuker als je iemand vindt om de vragen samen te bespreken en jullie gesprek op te nemen. We kunnen niet horen wie wat zei, alleen welke ideeën er gedeeld zijn.",
icon: MessagesSquare,
title: "Alleen of in een groep",
},
],
},
{
section: "Hoe het werkt",
slides: [
{
content:
"Je krijgt de vragen te zien zodra je in de opnameportal bent.",
cta: "Begrepen",
extraHelp:
"Vragen variëren afhankelijk van wat de organisator wil weten. Het kan gaan over de buurt, werkervaringen, of onderzoeksonderwerpen. Als er geen specifieke vragen zijn, kun je gewoon je gedachten of zorgen delen.",
icon: HelpCircle,
title: "Vragenronde",
},
],
},
{
section: "Privacy",
slides: [
{
content: "Als opnemer heb je zelf controle over wat je deelt.",
cta: "Vertel me meer",
extraHelp:
"Vermijd het delen van details die je niet met de organisator wilt delen. Wees voorzichtig en neem anderen niet op zonder hun toestemming.",
icon: Lock,
title: "Privacy is belangrijk",
},
...(getPrivacyCard("nl-NL")?.slides || []),
],
},
{
section: "Tips",
slides: [
{
content:
"Stel je voor dat Dembrane via de luidspreker met je praat. Als je jezelf kunt horen, zit je goed.",
cta: "Begrepen",
extraHelp:
"Een beetje achtergrondgeluid is geen probleem, zolang duidelijk is wie er spreekt.",
icon: Volume2,
title: "Verminder achtergrondgeluid",
},
{
content:
"Zorg voor een stabiele verbinding voor een soepele opname.",
cta: "Klaar!",
extraHelp:
"Wi-Fi of een goede mobiele verbinding werkt het beste. Valt je verbinding weg? Geen zorgen, je kunt altijd opnieuw beginnen waar je gebleven was.",
icon: Wifi,
title: "Goede internetverbinding",
},
{
content:
"Voorkom onderbrekingen door je apparaat ontgrendeld te houden. Als het toch vergrendelt, ontgrendel je het gewoon en ga je verder.",
cta: "Oké",
extraHelp:
"Dembrane probeert je apparaat actief te houden, maar soms kunnen apparaten dit overrulen. Je kunt de instellingen van je apparaat aanpassen om langer ontgrendeld te blijven als dat nodig is.",
icon: Smartphone,
title: "Vergrendel je apparaat niet!",
},
],
},
],
};

// Fallback to English if language not found
return tutorialCards[lang] || tutorialCards["en-US"] || [];
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Refactor opportunity: Massive code duplication between basic and advanced tutorials.

The Welcome, How it works, and initial Privacy sections are 100% identical across basic and advanced for all 5 languages (~900 lines of duplication). This makes maintenance painful and error-prone.

Extract shared sections into a helper:

+const getSharedTutorialSections = (lang: string): LanguageCards[string] => {
+  const sections: Record<string, LanguageCards[string]> = {
+    "en-US": [
+      {
+        section: "Welcome",
+        slides: [/* Welcome slides */]
+      },
+      {
+        section: "How it works", 
+        slides: [/* How it works slides */]
+      }
+    ],
+    // ... other languages
+  };
+  return sections[lang] || sections["en-US"] || [];
+};

 const getBasicTutorialCards = (lang: string): LanguageCards[string] => {
-  const tutorialCards: Record<string, LanguageCards[string]> = {
-    "en-US": [/* full sections */],
-    // ...
-  };
-  return tutorialCards[lang] || tutorialCards["en-US"] || [];
+  const sharedSections = getSharedTutorialSections(lang);
+  const privacyCard = getPrivacyCard(lang);
+  return [...sharedSections, ...(privacyCard ? [privacyCard] : [])];
 };

 const getAdvancedTutorialCards = (lang: string): LanguageCards[string] => {
+  const sharedSections = getSharedTutorialSections(lang);
+  const bestPractices = getBestPracticesSections(lang);
+  // Merge privacy slides into last shared section
+  const privacySlides = getPrivacyCard(lang)?.slides || [];
+  sharedSections[sharedSections.length - 1].slides.push(...privacySlides);
+  return [...sharedSections, bestPractices];
 };

This cuts ~900 lines and makes updates to shared content way easier.

Committable suggestion skipped: line range outside the PR's diff.

project.default_conversation_transcript_prompt ?? "",
default_conversation_tutorial_slug:
project.default_conversation_tutorial_slug ?? "none",
default_conversation_tutorial_slug: normalizedTutorialSlug ?? "none",
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Double fallback ?? "none" is redundant but harmless.

normalizedTutorialSlug is already guaranteed to be a valid string from the ternary above. The ?? "none" is belt-and-suspenders but doesn't hurt.

-			default_conversation_tutorial_slug: normalizedTutorialSlug ?? "none",
+			default_conversation_tutorial_slug: normalizedTutorialSlug,
📝 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
default_conversation_tutorial_slug: normalizedTutorialSlug ?? "none",
default_conversation_tutorial_slug: normalizedTutorialSlug,
🤖 Prompt for AI Agents
In echo/frontend/src/components/project/ProjectPortalEditor.tsx around line 247,
the property default_conversation_tutorial_slug uses a redundant nullish
coalescing fallback (?? "none") even though normalizedTutorialSlug is already
guaranteed to be a valid string by the preceding ternary; remove the extraneous
?? "none" so the assignment becomes default_conversation_tutorial_slug:
normalizedTutorialSlug to eliminate unnecessary duplication.

Comment on lines +229 to +232
#: src/components/project/ProjectPortalEditor.tsx:543
msgid "Advanced (Tips and best practices)"
msgstr "Erweitert (Tipps und best practices)"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the file
find . -name "de-DE.po" -path "*/locales/*" | head -5

Repository: Dembrane/echo

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Read the flagged sections from de-DE.po
# Using sed to read specific line ranges
echo "=== Lines 229-232 ==="
sed -n '229,232p' ./echo/frontend/src/locales/de-DE.po

echo ""
echo "=== Lines 496-499 ==="
sed -n '496,499p' ./echo/frontend/src/locales/de-DE.po

echo ""
echo "=== Lines 531-536 ==="
sed -n '531,536p' ./echo/frontend/src/locales/de-DE.po

echo ""
echo "=== Lines 651-656 ==="
sed -n '651,656p' ./echo/frontend/src/locales/de-DE.po

echo ""
echo "=== Lines 703-707 ==="
sed -n '703,707p' ./echo/frontend/src/locales/de-DE.po

Repository: Dembrane/echo

Length of output: 1275


🏁 Script executed:

#!/bin/bash
# Search for patterns related to slides, cards, and privacy terminology
echo "=== Searching for 'slide' or 'Karte' (card) terminology ==="
rg -i "(slide|karte|privacy|datenschutz)" ./echo/frontend/src/locales/de-DE.po -A 2 -B 2

echo ""
echo "=== Searching for 'best practices' ==="
rg "best practices" ./echo/frontend/src/locales/de-DE.po -A 2 -B 2

Repository: Dembrane/echo

Length of output: 1404


Minor terminology inconsistencies in tutorial and privacy-related labels

The German translations have two opportunities for alignment:

  • "best practices" in line 229 is left untranslated while other UI strings are localized; consider a German equivalent (e.g., "bewährte Praktiken")
  • "data privacy slide" (line 531) uses "Karte", while "tutorial slides" (line 496) uses "Folien"—consider standardizing on one term for consistency

Non-blocking; ideal for a future copy review when focusing on localization polish.

🤖 Prompt for AI Agents
In echo/frontend/src/locales/de-DE.po around lines 229 and referencing lines
~496 and ~531, the German translation leaves "best practices" untranslated and
uses inconsistent terms for tutorial/privacy slides; replace "Erweitert (Tipps
und best practices)" with a fully German string such as "Erweitert (Tipps und
bewährte Praktiken)" and standardize the term for slides across the file (prefer
using "Folien" instead of "Karte") so all tutorial and data-privacy related
entries use the same German word for "slides."

Comment on lines 3217 to +3228
#. js-lingui-explicit-id
#: src/components/participant/ParticipantOnboardingCards.tsx:291
#: src/components/participant/ParticipantOnboardingCards.tsx:281
msgid "participant.mic.check.button.skip"
msgstr "Overslaan"

#~ msgid "Skip"
#~ msgstr "Overslaan"

#: src/components/project/ProjectPortalEditor.tsx:531
msgid "Skip data privacy slide (Host manages consent)"
msgstr "Datenschutzkarte overslaan (Organisatie beheert toestemming)"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix mixed-language translation for “Skip data privacy slide…”.

Right now the msgstr mixes German and Dutch ("Datenschutzkarte overslaan..."), which is going to look off in the UI. I’d tighten it up to native Dutch:

-msgstr "Datenschutzkarte overslaan (Organisatie beheert toestemming)"
+msgstr "Sla de privacydia over (Organisatie beheert toestemming)"
🤖 Prompt for AI Agents
In echo/frontend/src/locales/nl-NL.po around lines 3217-3228, the msgstr for
"Skip data privacy slide (Host manages consent)" mixes German and Dutch; replace
the mixed-language string with a proper Dutch translation such as "Privacy-dia
overslaan (Host beheert toestemming)" (or "Privacy-slide overslaan (Host beheert
toestemming)") so the entire msgstr is native Dutch and consistent with the
msgid.

Comment on lines +3627 to 3630
#: src/components/project/ProjectPortalEditor.tsx:729
msgid "This prompt guides how the AI responds to participants. Customize it to shape the type of feedback or engagement you want to encourage."
msgstr "Deze prompt bepaalt hoe de AI reageert op deelnemers. Deze prompt stuurt aan hoe de AI reageert"

Copy link
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 the “This prompt guides…” translation; current msgstr is truncated.

The Dutch copy for the reply prompt helper stops mid-thought and repeats itself, so it doesn’t explain what the prompt actually does. Something like this would be closer to the English intent:

-msgstr "Deze prompt bepaalt hoe de AI reageert op deelnemers. Deze prompt stuurt aan hoe de AI reageert"
+msgstr "Deze prompt bepaalt hoe de AI reageert op deelnemers. Pas hem aan om het type feedback of betrokkenheid te sturen dat je wilt stimuleren."

This keeps the tone and gives hosts a clear mental model of what changing the prompt does.

📝 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
#: src/components/project/ProjectPortalEditor.tsx:729
msgid "This prompt guides how the AI responds to participants. Customize it to shape the type of feedback or engagement you want to encourage."
msgstr "Deze prompt bepaalt hoe de AI reageert op deelnemers. Deze prompt stuurt aan hoe de AI reageert"
#: src/components/project/ProjectPortalEditor.tsx:729
msgid "This prompt guides how the AI responds to participants. Customize it to shape the type of feedback or engagement you want to encourage."
msgstr "Deze prompt bepaalt hoe de AI reageert op deelnemers. Pas hem aan om het type feedback of betrokkenheid te sturen dat je wilt stimuleren."
🤖 Prompt for AI Agents
In echo/frontend/src/locales/nl-NL.po around lines 3627 to 3630, the msgstr for
the msgid starting "This prompt guides how the AI responds to participants..."
is truncated; replace the current incomplete Dutch string with a full, complete
translation that matches the English intent — for example: "Deze prompt bepaalt
hoe de AI reageert op deelnemers. Pas deze prompt aan om te sturen welk soort
feedback of betrokkenheid je wilt aanmoedigen." — update the msgstr value to
this full sentence so the helper explains clearly what changing the prompt does.

Comment on lines +44 to 48
{isLoadingProject ? (
<DembraneLoadingSpinner isLoading />
) : (
<ParticipantOnboardingCards
initialCards={transformCard(tutorialCard as EchoPortalTutorial)}
project={project as Project}
/>
<ParticipantOnboardingCards project={project as Project} />
)}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 9, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type assertion project as Project needs a guard.

If isLoadingProject is false but project is undefined (edge case on fetch failure without error), you'll pass undefined to ParticipantOnboardingCards. The error check at line 32 only fires when projectError is truthy.

Consider adding a null check:

 return (
   <div className="h-full grow">
     {isLoadingProject ? (
       <DembraneLoadingSpinner isLoading />
+    ) : !project ? (
+      <div className="flex flex-col items-center justify-center">
+        <Alert color="info">
+          {t`An error occurred while loading the Portal. Please contact the support team.`}
+        </Alert>
+      </div>
     ) : (
       <ParticipantOnboardingCards project={project as Project} />
     )}
   </div>
 );
📝 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
{isLoadingProject ? (
<DembraneLoadingSpinner isLoading />
) : (
<ParticipantOnboardingCards
initialCards={transformCard(tutorialCard as EchoPortalTutorial)}
project={project as Project}
/>
<ParticipantOnboardingCards project={project as Project} />
)}
return (
<div className="h-full grow">
{isLoadingProject ? (
<DembraneLoadingSpinner isLoading />
) : !project ? (
<div className="flex flex-col items-center justify-center">
<Alert color="info">
{t`An error occurred while loading the Portal. Please contact the support team.`}
</Alert>
</div>
) : (
<ParticipantOnboardingCards project={project as Project} />
)}
</div>
);
🤖 Prompt for AI Agents
In echo/frontend/src/routes/participant/ParticipantStart.tsx around lines 44-48,
the code force-casts project with "project as Project" even when
isLoadingProject is false, which can pass undefined into
ParticipantOnboardingCards; update the conditional to guard against a missing
project (e.g., change the ternary to check both isLoadingProject and project
existence, or render an error/fallback UI when project is undefined) and remove
the unsafe type assertion so ParticipantOnboardingCards only receives a valid
Project instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we already have this guard: if (loadingFinished && projectError) {
return (



{tAn error occurred while loading the Portal. Please contact the support team.}


);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@ussaama Ah yeah, I see the guard! But there's a subtle timing issue – loadingFinished is set in the useEffect, so on the first render after the query errors out, you have:

  • isLoadingProject: false
  • loadingFinished: still false (hasn't updated yet)
  • projectError: truthy

So the guard if (loadingFinished && projectError) doesn't fire, and you fall through to rendering project as Project while it's undefined.

Quick fix: just drop the loadingFinished check:

if (projectError) {
    return (
        <div className="flex flex-col items-center justify-center">
            <Alert color="info">
                {t`An error occurred while loading the Portal. Please contact the support team.`}
            </Alert>
        </div>
    );
}

Or if you need loadingFinished for the session storage flag, add a null check in the render ternary. LGTM otherwise! 🚀


🧠 Learnings used
Learnt from: ussaama
Repo: Dembrane/echo PR: 259
File: echo/frontend/src/components/layout/ParticipantLayout.tsx:33-33
Timestamp: 2025-08-08T10:39:31.114Z
Learning: In echo/frontend/src/components/layout/ParticipantLayout.tsx, prefer using simple pathname.includes("start") and pathname.includes("finish") to control the settings button visibility. No need to switch to segment-based matching or add a useEffect to auto-close the modal for these routes, per ussaama’s preference in PR #259.

Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/routes/auth/**/*.tsx : Auth hero uses `/public/video/auth-hero.mp4` with `/public/video/auth-hero-poster.jpg` as poster; keep the bright blur overlay consistent when iterating on onboarding screens

Learnt from: CR
Repo: Dembrane/echo PR: 0
File: echo/frontend/AGENTS.md:0-0
Timestamp: 2025-12-05T00:33:33.442Z
Learning: Applies to echo/frontend/**/src/routes/auth/**/*.tsx : Gentle login/logout flows use `useTransitionCurtain().runTransition()` before navigation—animations expect Directus session mutations to await that promise

Learnt from: ussaama
Repo: Dembrane/echo PR: 169
File: echo/frontend/src/components/project/ProjectPortalEditor.tsx:409-464
Timestamp: 2025-05-30T15:38:44.413Z
Learning: Badge-based selectors in ProjectPortalEditor.tsx: Keyboard navigation enhancements for accessibility are considered optional improvements rather than critical issues. The user acknowledges these suggestions but doesn't prioritize them as blockers.

Learnt from: ussaama
Repo: Dembrane/echo PR: 266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses regular useQuery (not useSuspenseQuery), which means conversationsCountQuery.data can be undefined during loading states. When using Number(conversationsCountQuery.data) ?? 0, this creates NaN because Number(undefined) = NaN and NaN is not nullish, so the fallback doesn't apply. The correct pattern is Number(conversationsCountQuery.data ?? 0) to ensure the fallback happens before type conversion.

Learnt from: ussaama
Repo: Dembrane/echo PR: 349
File: echo/frontend/src/components/report/hooks/index.ts:51-71
Timestamp: 2025-10-24T08:08:23.128Z
Learning: In echo/frontend/src/components/report/hooks/index.ts, the useGetProjectParticipants hook uses Directus aggregate which always returns count as a string (e.g., "0", "1", "2"). The pattern `Number.parseInt(result[0]?.count ?? "0", 10) || 0` is the correct way to handle this, with the string fallback "0" ensuring parseInt receives a valid string input.
<!--

Learnt from: ussaama
Repo: Dembrane/echo PR: 205
File: echo/frontend/src/lib/query.ts:1444-1506
Timestamp: 2025-07-10T12:48:20.683Z
Learning: ussaama prefers string concatenation over template literals for simple cases where readability is clearer, even when linting tools suggest template literals. Human readability takes precedence over strict linting rules in straightforward concatenation scenarios.

@ussaama ussaama requested a review from spashii December 9, 2025 13:33
@spashii spashii added this pull request to the merge queue Dec 9, 2025
Merged via the queue into main with commit ab0308d Dec 9, 2025
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants