feat: implement MathTree MVP — skill tree, workbench, and progress system

- Interactive skill tree with dagre auto-layout and React Flow (15 math+logic nodes)
- Workbench with math input (LTR/RTL toggle), multiple choice, keyboard shortcuts
- Challenge verification engine with retry-based flow (no answer reveal until 3 failures)
- Scratchpad canvas with freehand drawing, text boxes, eraser, colors, and stroke sizes
- "Aprende primero" collapsible explanations on introductory challenges
- XP/level system, daily streaks, 7 achievements, progress persistence via Zustand
- Profile page with stats and achievement gallery
- Sidebar navigation with XP bar and streak badge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jose Luis Montañes
2026-03-26 01:53:45 +01:00
parent a2846420a3
commit f9f74d3f19
32 changed files with 3031 additions and 67 deletions

View File

@@ -0,0 +1,54 @@
'use client';
import { memo } from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import { Lock } from 'lucide-react';
import type { SkillNode, NodeStatus } from '@/types/skill-tree';
type SkillNodeData = SkillNode & { status: NodeStatus } & Record<string, unknown>;
type SkillNodeType = Node<SkillNodeData>;
const statusStyles: Record<NodeStatus, string> = {
locked: 'bg-muted/50 border-muted-foreground/20 opacity-50 cursor-not-allowed',
available: 'bg-card border-primary/50 hover:border-primary hover:shadow-lg hover:shadow-primary/10 cursor-pointer',
'in-progress': 'bg-card border-amber-500/50 shadow-md shadow-amber-500/10 cursor-pointer',
completed: 'bg-card border-green-500/50 shadow-md shadow-green-500/10 cursor-pointer',
mastered: 'bg-card border-purple-500/50 shadow-md shadow-purple-500/10 cursor-pointer',
};
const statusBadge: Record<NodeStatus, { label: string; className: string } | null> = {
locked: null,
available: { label: 'Disponible', className: 'bg-primary/20 text-primary' },
'in-progress': { label: 'En progreso', className: 'bg-amber-500/20 text-amber-500' },
completed: { label: 'Completado', className: 'bg-green-500/20 text-green-500' },
mastered: { label: 'Dominado', className: 'bg-purple-500/20 text-purple-500' },
};
export const SkillNodeComponent = memo(function SkillNodeComponent({
data,
}: NodeProps<SkillNodeType>) {
const status = data.status;
const badge = statusBadge[status];
return (
<div
className={`px-4 py-3 rounded-xl border-2 transition-all duration-200 min-w-[140px] max-w-[180px] ${statusStyles[status]}`}
>
<Handle type="target" position={Position.Top} className="!bg-muted-foreground/30 !w-2 !h-2 !border-0" />
<div className="flex items-center gap-2 mb-1">
<span className="text-lg">{status === 'locked' ? '' : data.icon}</span>
{status === 'locked' && <Lock className="w-4 h-4 text-muted-foreground" />}
<span className="text-sm font-semibold truncate">{data.title}</span>
</div>
{badge && (
<div className={`text-[10px] font-medium px-2 py-0.5 rounded-full w-fit ${badge.className}`}>
{badge.label}
</div>
)}
<Handle type="source" position={Position.Bottom} className="!bg-muted-foreground/30 !w-2 !h-2 !border-0" />
</div>
);
});