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 (
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 (
);
})}
)}
) : (
<>
AI vocabulary helper
Enter the child’s reading level and interests to generate a parent-reviewed starter list.
{aiLoading ? 'Thinking...' : generationStatus || 'Ready to generate'}
Review generated cards
{draftsForChild.length === 0 ? (
Generated cards will appear here for review before adding them to the deck.
) : (
draftsForChild.map((draft) => (
{draft.word}
{draft.definition || 'No definition yet. You can edit this card in My Cards.'}
{draft.example &&
Example: {draft.example}
}
))
)}
>
)}
)}
{activeTab === 'game' && }
);
}
export { App };
if (typeof ReactDOM !== 'undefined') {
ReactDOM.createRoot(document.getElementById('root')).render();
}