import { useCollection, useIdentity, useAI } from '@deplixo/sdk'; /* @component-map * App — Main container, tab navigation between Cards and Game [app.jsx] * WordCards — Browse, add, edit vocabulary cards with upload [components/WordCards.jsx] * MatchingGame — Matching game mode pairing words with definitions [components/MatchingGame.jsx] * @end-component-map */ // DEPLOY_CONFIG: {"cron": [{"name": "daily-word-of-the-day-notification", "schedule": "0 9 * * *", "action": "event", "config": {"event_type": "daily_word_of_the_day"}}]} import { useEffect, useMemo, useState } from 'react'; import { WordCards } from './components/WordCards.jsx'; import { MatchingGame } from './components/MatchingGame.jsx'; function App() { const { user } = useIdentity(); const { generate: generateAI, loading: aiLoading, error: aiError } = useAI(); const [activeTab, setActiveTab] = useState('cards'); const [cardsViewMode, setCardsViewMode] = useState('cards'); const { items: childSelectionItems } = useCollection('wordwonder_child_selection', { personal: true }); const selectedChildId = useMemo(() => childSelectionItems.find(i => i.key === 'wordwonder:selectedChildId')?.value || 'default-child', [childSelectionItems]); const selectedChildName = useMemo(() => childSelectionItems.find(i => i.key === 'wordwonder:selectedChildName')?.value || '', [childSelectionItems]); const [childDraftName, setChildDraftName] = useState(selectedChildName); const [readingLevel, setReadingLevel] = useState(''); const [childInterests, setChildInterests] = useState(''); const [generatedIdeas, setGeneratedIdeas] = useState([]); const [generationStatus, setGenerationStatus] = useState(''); const { items: childProfiles, loading: childrenLoading, add: addChild, update: updateChild } = useCollection('wordwonder_children'); const { items: generatedCardDrafts, add: addGeneratedCard, update: updateGeneratedCard, remove: removeGeneratedCard } = useCollection('wordwonder_ai_card_drafts', { personal: true }); const { items: learningEvents, loading: learningHistoryLoading } = useCollection('wordwonder_learning_history', { personal: true }); const currentChild = childProfiles.find((child) => child.childId === selectedChildId) || null; const persistSelectedChild = (childId, childName, { add, update }) => { setChildDraftName(childName); const childIdItem = childSelectionItems.find(i => i.key === 'wordwonder:selectedChildId'); if (childIdItem) { update(childIdItem.id, { key: 'wordwonder:selectedChildId', value: childId }); } else { add({ key: 'wordwonder:selectedChildId', value: childId }); } const childNameItem = childSelectionItems.find(i => i.key === 'wordwonder:selectedChildName'); if (childNameItem) { update(childNameItem.id, { key: 'wordwonder:selectedChildName', value: childName }); } else { add({ key: 'wordwonder:selectedChildName', value: childName }); } }; const handleCreateOrSelectChild = async () => { const trimmedName = (childDraftName || '').trim(); if (!trimmedName) return; let existing = childProfiles.find((child) => (child.name || '').toLowerCase() === trimmedName.toLowerCase()) || null; if (!existing) { const newChild = await addChild({ childId: `child_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, name: trimmedName, stars: 0, rewards: [], completedActivities: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), createdBy: user?.id || null, }); existing = newChild || null; } if (existing) { const { add, update } = useCollection('wordwonder_child_selection', { personal: true }); persistSelectedChild(existing.childId, existing.name || trimmedName, { add, update }); } }; const handleQuickSelect = async (child) => { if (!child) return; const { add, update } = useCollection('wordwonder_child_selection', { personal: true }); persistSelectedChild(child.childId, child.name || '', { add, update }); if (updateChild && child.id) { await updateChild(child.id, { ...child, updatedAt: new Date().toISOString(), lastSelectedAt: new Date().toISOString(), }); } }; const parseAIList = (text) => { const raw = String(text || '').trim(); if (!raw) return []; try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; if (parsed && Array.isArray(parsed.cards)) return parsed.cards; } catch (e) {} return raw .split('\n') .map((line) => line.replace(/^[-*\d.\s]+/, '').trim()) .filter(Boolean); }; const normalizeSuggestion = (item, idx) => { if (typeof item === 'string') { const word = item.trim(); return { word, definition: '', example: '', imageUrl: '', readingLevel: readingLevel.trim(), interests: childInterests.trim(), source: 'ai', status: 'draft', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), order: idx, }; } return { word: item.word || item.term || item.vocab || item.title || `Word ${idx + 1}`, definition: item.definition || item.meaning || '', example: item.example || item.sentence || '', imageUrl: item.imageUrl || item.image || '', readingLevel: item.readingLevel || readingLevel.trim(), interests: item.interests || childInterests.trim(), source: 'ai', status: 'draft', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), order: idx, }; }; const handleGenerateVocabulary = async () => { const level = readingLevel.trim(); const interests = childInterests.trim(); if (!level || !interests) { setGenerationStatus('Please add both the reading level and interests.'); return; } setGenerationStatus('Generating vocabulary ideas...'); const prompt = `Generate 10 child-friendly vocabulary words for a child reading at ${level}. Interests: ${interests}. Return ONLY JSON as an array of objects with keys word, definition, example, and optionally imageUrl. Keep words age-appropriate, motivating, and easy to review by a parent.`; try { const result = await generateAI(prompt); const text = typeof result === 'string' ? result : (result?.text || result?.content || JSON.stringify(result || '')); const list = parseAIList(text).map(normalizeSuggestion).filter((item) => item.word); setGeneratedIdeas(list); setGenerationStatus(list.length ? `Generated ${list.length} vocabulary ideas.` : 'No ideas returned. Try again with more detail.'); for (const item of list) { await addGeneratedCard({ ...item, childId: selectedChildId, childName: selectedChildName, reviewed: false, }); } } catch (e) { setGenerationStatus('AI generation failed. You can still add cards manually.'); } }; const handleAddDraftToCards = async (draft) => { if (!draft) return; await addGeneratedCard({ ...draft, childId: selectedChildId, childName: selectedChildName, reviewed: true, status: 'approved', updatedAt: new Date().toISOString(), }); }; const handleApproveDraft = async (draft) => { if (!draft) return; if (updateGeneratedCard && draft.id) { await updateGeneratedCard(draft.id, { ...draft, reviewed: true, status: 'approved', updatedAt: new Date().toISOString(), }); } }; const handleRejectDraft = async (draft) => { if (!draft) return; if (removeGeneratedCard && draft.id) { await removeGeneratedCard(draft.id); } }; const draftsForChild = useMemo( () => generatedCardDrafts.filter((draft) => !selectedChildId || draft.childId === selectedChildId), [generatedCardDrafts, selectedChildId] ); const wordsLearnedChart = useMemo(() => { const childFilteredEvents = learningEvents.filter((event) => !selectedChildId || event.childId === selectedChildId); const completedEvents = childFilteredEvents.filter((event) => event && (event.action === 'learned' || event.action === 'completed' || event.type === 'word_learned' || event.learned === true)); const bucketed = new Map(); for (const event of completedEvents) { const createdAt = event.createdAt || event.timestamp || event.updatedAt; const date = createdAt ? new Date(createdAt) : new Date(); if (Number.isNaN(date.getTime())) continue; const key = date.toISOString().slice(0, 10); bucketed.set(key, (bucketed.get(key) || 0) + 1); } const series = [...bucketed.entries()] .sort((a, b) => a[0].localeCompare(b[0])) .map(([date, count]) => ({ date, count })); const total = series.reduce((sum, item) => sum + item.count, 0); const maxCount = Math.max(1, ...series.map((item) => item.count)); return { series, total, maxCount, weeklyMode: series.length > 14, }; }, [learningEvents, selectedChildId]); const chartDisplaySeries = useMemo(() => { if (!wordsLearnedChart.weeklyMode) return wordsLearnedChart.series; const weekly = new Map(); for (const item of wordsLearnedChart.series) { const date = new Date(`${item.date}T00:00:00`); if (Number.isNaN(date.getTime())) continue; const start = new Date(date); const day = start.getDay(); const diff = (day + 6) % 7; start.setDate(start.getDate() - diff); const key = start.toISOString().slice(0, 10); weekly.set(key, (weekly.get(key) || 0) + item.count); } return [...weekly.entries()] .sort((a, b) => a[0].localeCompare(b[0])) .map(([date, count]) => ({ date, count })); }, [wordsLearnedChart.series, wordsLearnedChart.weeklyMode]); const chartMax = Math.max(1, ...chartDisplaySeries.map((item) => item.count)); return (
📚

Word Wonder

Learn new words every day!

Child profile

{currentChild ? `Working in progress for ${currentChild.name}` : 'Create or select a child to save stars and completion separately.'}

{currentChild && (
Active {currentChild.name}
)}
setChildDraftName(e.target.value)} placeholder="Enter child's name" />
{childProfiles.length === 0 ? (

No child profiles yet. Add one above to keep progress separate.

) : ( childProfiles.map((child) => ( )) )}
{activeTab === 'cards' && (
{cardsViewMode === 'progress' ? (

Words learned over time

{wordsLearnedChart.weeklyMode ? 'Weekly view based on your child’s learning history.' : 'Daily view based on your child’s learning history.'}

Total learned: {wordsLearnedChart.total} Periods shown: {chartDisplaySeries.length} History records: {learningHistoryLoading ? 'Loading...' : learningEvents.length}
{chartDisplaySeries.length === 0 ? (

No learning history yet. As cards are completed or marked learned, the chart will appear here.

) : (
{chartDisplaySeries.map((item) => { const height = Math.max(14, Math.round((item.count / chartMax) * 100)); const label = wordsLearnedChart.weeklyMode ? new Date(`${item.date}T00:00:00`).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : new Date(`${item.date}T00:00:00`).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); return (
{item.count}
{label}
); })}
)}
) : ( <>

AI vocabulary helper

Enter the child’s reading level and interests to generate a parent-reviewed starter list.

{aiLoading ? 'Thinking...' : generationStatus || 'Ready to generate'}
setReadingLevel(e.target.value)} placeholder="e.g. early reader, grade 2, fluent reader" />