From ad5be01b70cdfeff70090ddbcb27d45b45220fa7 Mon Sep 17 00:00:00 2001 From: RaymondVerhoef Date: Wed, 27 May 2026 21:18:17 +0200 Subject: [PATCH] feat: enhance ConceptExplainer to handle content extraction and display; add normalization for PocketBase records --- .../micro_learning/ConceptExplainer.jsx | 35 +++++++++++++++---- src/lib/microLearningService.js | 30 ++++++++++++++-- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/components/micro_learning/ConceptExplainer.jsx b/src/components/micro_learning/ConceptExplainer.jsx index a504fb9..f533bd0 100644 --- a/src/components/micro_learning/ConceptExplainer.jsx +++ b/src/components/micro_learning/ConceptExplainer.jsx @@ -1,19 +1,42 @@ import Card from '../ui/Card'; import Button from '../ui/Button'; +/** + * Safely extract sections from a content object that may be: + * - a properly parsed object: { sections: [...] } + * - a JSON string: '{"sections":[...]}' + * - null / undefined + * + * Returns an array in every case so .map() never throws. + */ +function getSections(content) { + let obj = content; + if (typeof obj === 'string') { + try { obj = JSON.parse(obj); } catch { return []; } + } + const sections = obj?.sections; + return Array.isArray(sections) ? sections : []; +} + export default function ConceptExplainer({ content, onComplete }) { + const sections = getSections(content); + return (

Concept Explainer

- {content?.sections?.map((section, i) => ( -
-

{section.title}

-
-
- ))} + {sections.length === 0 ? ( +

No content available for this topic yet.

+ ) : ( + sections.map((section, i) => ( +
+

{section.title}

+
+
+ )) + )}
diff --git a/src/lib/microLearningService.js b/src/lib/microLearningService.js index 5c30588..5db8930 100644 --- a/src/lib/microLearningService.js +++ b/src/lib/microLearningService.js @@ -94,7 +94,8 @@ export async function getOrGenerateMicroLearning(topicId, type) { }); console.log(`[MicroLearning] Stored: ${record.id}`); - return record; + // Normalise in case PB returns the JSON field as a string on this PB version. + return normalizeRecord(record); } /** @@ -154,12 +155,37 @@ export async function deleteAllForTopic(topicId) { // ── Internal helpers ────────────────────────────────────────────────────────── +/** + * Ensure the PocketBase record's `content` field is a parsed JS object. + * + * Older PocketBase versions (or misconfigured instances) may return a JSON + * field as a raw string instead of a parsed object. The PB JS SDK normally + * handles this, but we have seen production deployments where `r.content` is + * the serialised JSON string — which causes `content.sections?.map` to throw + * "is not a function" because strings don't have a `.map` method. + * + * This guard is defensive and idempotent: if `content` is already a plain + * object it is returned as-is; only strings are re-parsed. + */ +function normalizeRecord(record) { + if (!record) return record; + if (typeof record.content === 'string') { + try { + record.content = JSON.parse(record.content); + } catch { + // Malformed JSON — replace with an empty object so the UI can handle it. + record.content = {}; + } + } + return record; +} + async function findExisting(topicId, type) { try { const records = await pb.collection('micro_learnings').getFullList({ filter: `topic_id = "${topicId}" && type = "${type}" && status = "published"`, }); - return records.length > 0 ? records[0] : null; + return records.length > 0 ? normalizeRecord(records[0]) : null; } catch { return null; }