- 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>
55 lines
2.3 KiB
TypeScript
55 lines
2.3 KiB
TypeScript
'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>
|
|
);
|
|
});
|