{"CACHEDAT":"2026-04-14 05:11:07","SLUG":"v2-Ywa0JpmwUr","MARKDOWN":"\n\n \n\n```javascript\nimport React, { useState } from 'react';\r\nimport { ChevronRight, Plus, Minus, Download } from 'lucide-react';\r\n\r\nexport default function CompetenceBuilder() {\r\n const [step, setStep] = useState(1);\r\n const [config, setConfig] = useState({\r\n numDomains: 4,\r\n domainArrangement: 'radial',\r\n separatorColor: '#ffffff',\r\n separatorWidth: 3,\r\n useGoalShading: true,\r\n outcomeDisplay: 'radial', // 'radial' or 'concentric'\r\n outcomeRingMode: 'uniform', // 'uniform' (gleiche Höhe) or 'divided' (aufgeteilt)\r\n domains: [\r\n { name: 'Domain 1', color: '#3498DB', goals: [] },\r\n { name: 'Domain 2', color: '#2ECC71', goals: [] },\r\n { name: 'Domain 3', color: '#F39C12', goals: [] },\r\n { name: 'Domain 4', color: '#E74C3C', goals: [] }\r\n ]\r\n });\r\n\r\n const updateDomainCount = (count) => {\r\n const colors = ['#3498DB', '#2ECC71', '#F39C12', '#E74C3C', '#9B59B6', '#1ABC9C', '#E67E22', '#34495E'];\r\n const newDomains = Array.from({ length: count }, (_, i) => \r\n config.domains[i] || { \r\n name: `Domain ${i + 1}`, \r\n color: colors[i % colors.length],\r\n goals: []\r\n }\r\n );\r\n setConfig({ ...config, numDomains: count, domains: newDomains });\r\n };\r\n\r\n const updateDomainName = (index, name) => {\r\n const newDomains = [...config.domains];\r\n newDomains[index].name = name;\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const updateDomainColor = (index, color) => {\r\n const newDomains = [...config.domains];\r\n newDomains[index].color = color;\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const addGoal = (domainIndex) => {\r\n const newDomains = [...config.domains];\r\n newDomains[domainIndex].goals.push({\r\n name: `Goal ${newDomains[domainIndex].goals.length + 1}`,\r\n outcomes: []\r\n });\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const updateGoalName = (domainIndex, goalIndex, name) => {\r\n const newDomains = [...config.domains];\r\n newDomains[domainIndex].goals[goalIndex].name = name;\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const removeGoal = (domainIndex, goalIndex) => {\r\n const newDomains = [...config.domains];\r\n newDomains[domainIndex].goals.splice(goalIndex, 1);\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const addOutcome = (domainIndex, goalIndex) => {\r\n const newDomains = [...config.domains];\r\n newDomains[domainIndex].goals[goalIndex].outcomes.push(\r\n `Outcome ${newDomains[domainIndex].goals[goalIndex].outcomes.length + 1}`\r\n );\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const updateOutcomeName = (domainIndex, goalIndex, outcomeIndex, name) => {\r\n const newDomains = [...config.domains];\r\n newDomains[domainIndex].goals[goalIndex].outcomes[outcomeIndex] = name;\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const removeOutcome = (domainIndex, goalIndex, outcomeIndex) => {\r\n const newDomains = [...config.domains];\r\n newDomains[domainIndex].goals[goalIndex].outcomes.splice(outcomeIndex, 1);\r\n setConfig({ ...config, domains: newDomains });\r\n };\r\n\r\n const generateVisualization = () => {\r\n setStep(4);\r\n };\r\n\r\n const downloadSVG = () => {\r\n const svgElement = document.getElementById('competence-visualization');\r\n if (!svgElement) return;\r\n \r\n const svgData = new XMLSerializer().serializeToString(svgElement);\r\n const blob = new Blob([svgData], { type: 'image/svg+xml' });\r\n const url = URL.createObjectURL(blob);\r\n const link = document.createElement('a');\r\n link.href = url;\r\n link.download = 'kompetenzmodell.svg';\r\n link.click();\r\n URL.revokeObjectURL(url);\r\n };\r\n\r\n const hexToRgb = (hex) => {\r\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\r\n return result ? {\r\n r: parseInt(result[1], 16),\r\n g: parseInt(result[2], 16),\r\n b: parseInt(result[3], 16)\r\n } : null;\r\n };\r\n\r\n const adjustColorBrightness = (hex, percent) => {\r\n const rgb = hexToRgb(hex);\r\n if (!rgb) return hex;\r\n \r\n const adjust = (val) => Math.min(255, Math.max(0, Math.round(val * (1 + percent))));\r\n \r\n return `#${adjust(rgb.r).toString(16).padStart(2, '0')}${adjust(rgb.g).toString(16).padStart(2, '0')}${adjust(rgb.b).toString(16).padStart(2, '0')}`;\r\n };\r\n\r\n const RadialVisualization = () => {\r\n const centerX = 500;\r\n const centerY = 500;\r\n const innerRadius = 80;\r\n const outerRadius = 450;\r\n \r\n const angleStep = (2 * Math.PI) / config.domains.length;\r\n\r\n // Für uniform mode: maximale Anzahl von Outcomes finden\r\n const maxOutcomes = config.outcomeDisplay === 'concentric' && config.outcomeRingMode === 'uniform'\r\n ? Math.max(...config.domains.flatMap(d => d.goals.map(g => g.outcomes.length)), 1)\r\n : 0;\r\n\r\n return (\r\n \r\n );\r\n };\r\n\r\n const ConcentricVisualization = () => {\r\n const centerX = 500;\r\n const centerY = 500;\r\n const innerRadius = 80;\r\n const ringWidth = 100;\r\n \r\n return (\r\n \r\n );\r\n };\r\n\r\n const describeArc = (x, y, innerRadius, outerRadius, startAngle, endAngle) => {\r\n const innerStart = polarToCartesian(x, y, innerRadius, endAngle);\r\n const innerEnd = polarToCartesian(x, y, innerRadius, startAngle);\r\n const outerStart = polarToCartesian(x, y, outerRadius, endAngle);\r\n const outerEnd = polarToCartesian(x, y, outerRadius, startAngle);\r\n\r\n const largeArcFlag = endAngle - startAngle <= Math.PI ? \"0\" : \"1\";\r\n\r\n return [\r\n \"M\", outerStart.x, outerStart.y,\r\n \"A\", outerRadius, outerRadius, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,\r\n \"L\", innerEnd.x, innerEnd.y,\r\n \"A\", innerRadius, innerRadius, 0, largeArcFlag, 1, innerStart.x, innerStart.y,\r\n \"Z\"\r\n ].join(\" \");\r\n };\r\n\r\n const polarToCartesian = (centerX, centerY, radius, angleInRadians) => {\r\n return {\r\n x: centerX + (radius * Math.cos(angleInRadians)),\r\n y: centerY + (radius * Math.sin(angleInRadians))\r\n };\r\n };\r\n\r\n return (\r\n
\r\n Wenn aktiviert, wird jedes Goal innerhalb einer Domain mit einer leicht dunkleren Schattierung dargestellt\r\n
\r\nNoch keine Goals definiert
\r\n )}\r\nimport React, { useState } from 'react';
import { ChevronRight, Plus, Minus, Download } from 'lucide-react';
export default function CompetenceBuilder() {
const [step, setStep] = useState(1);
const [config, setConfig] = useState({
numDomains: 4,
domainArrangement: 'radial',
separatorColor: '#ffffff',
separatorWidth: 3,
useGoalShading: true,
outcomeDisplay: 'radial', // 'radial' or 'concentric'
outcomeRingMode: 'uniform', // 'uniform' (gleiche Höhe) or 'divided' (aufgeteilt)
domains: [
{ name: 'Domain 1', color: '#3498DB', goals: [] },
{ name: 'Domain 2', color: '#2ECC71', goals: [] },
{ name: 'Domain 3', color: '#F39C12', goals: [] },
{ name: 'Domain 4', color: '#E74C3C', goals: [] }
]
});
const updateDomainCount = (count) => {
const colors = ['#3498DB', '#2ECC71', '#F39C12', '#E74C3C', '#9B59B6', '#1ABC9C', '#E67E22', '#34495E'];
const newDomains = Array.from({ length: count }, (_, i) =>
config.domains[i] || {
name: `Domain ${i + 1}`,
color: colors[i % colors.length],
goals: []
}
);
setConfig({ ...config, numDomains: count, domains: newDomains });
};
const updateDomainName = (index, name) => {
const newDomains = [...config.domains];
newDomains[index].name = name;
setConfig({ ...config, domains: newDomains });
};
const updateDomainColor = (index, color) => {
const newDomains = [...config.domains];
newDomains[index].color = color;
setConfig({ ...config, domains: newDomains });
};
const addGoal = (domainIndex) => {
const newDomains = [...config.domains];
newDomains[domainIndex].goals.push({
name: `Goal ${newDomains[domainIndex].goals.length + 1}`,
outcomes: []
});
setConfig({ ...config, domains: newDomains });
};
const updateGoalName = (domainIndex, goalIndex, name) => {
const newDomains = [...config.domains];
newDomains[domainIndex].goals[goalIndex].name = name;
setConfig({ ...config, domains: newDomains });
};
const removeGoal = (domainIndex, goalIndex) => {
const newDomains = [...config.domains];
newDomains[domainIndex].goals.splice(goalIndex, 1);
setConfig({ ...config, domains: newDomains });
};
const addOutcome = (domainIndex, goalIndex) => {
const newDomains = [...config.domains];
newDomains[domainIndex].goals[goalIndex].outcomes.push(
`Outcome ${newDomains[domainIndex].goals[goalIndex].outcomes.length + 1}`
);
setConfig({ ...config, domains: newDomains });
};
const updateOutcomeName = (domainIndex, goalIndex, outcomeIndex, name) => {
const newDomains = [...config.domains];
newDomains[domainIndex].goals[goalIndex].outcomes[outcomeIndex] = name;
setConfig({ ...config, domains: newDomains });
};
const removeOutcome = (domainIndex, goalIndex, outcomeIndex) => {
const newDomains = [...config.domains];
newDomains[domainIndex].goals[goalIndex].outcomes.splice(outcomeIndex, 1);
setConfig({ ...config, domains: newDomains });
};
const generateVisualization = () => {
setStep(4);
};
const downloadSVG = () => {
const svgElement = document.getElementById('competence-visualization');
if (!svgElement) return;
const svgData = new XMLSerializer().serializeToString(svgElement);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'kompetenzmodell.svg';
link.click();
URL.revokeObjectURL(url);
};
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
const adjustColorBrightness = (hex, percent) => {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
const adjust = (val) => Math.min(255, Math.max(0, Math.round(val * (1 + percent))));
return `#${adjust(rgb.r).toString(16).padStart(2, '0')}${adjust(rgb.g).toString(16).padStart(2, '0')}${adjust(rgb.b).toString(16).padStart(2, '0')}`;
};
const RadialVisualization = () => {
const centerX = 500;
const centerY = 500;
const innerRadius = 80;
const outerRadius = 450;
const angleStep = (2 * Math.PI) / config.domains.length;
// Für uniform mode: maximale Anzahl von Outcomes finden
const maxOutcomes = config.outcomeDisplay === 'concentric' && config.outcomeRingMode === 'uniform'
? Math.max(...config.domains.flatMap(d => d.goals.map(g => g.outcomes.length)), 1)
: 0;
return (
<svg id="competence-visualization" width="1000" height="1000" viewBox="0 0 1000 1000">
{/* Center Circle */}
<circle cx={centerX} cy={centerY} r={innerRadius} fill="#f8f9fa" stroke="#dee2e6" strokeWidth="2" />
<text x={centerX} y={centerY} textAnchor="middle" dominantBaseline="middle" fontSize="16" fontWeight="bold" fill="#495057">
Kompetenz-
</text>
<text x={centerX} y={centerY + 20} textAnchor="middle" dominantBaseline="middle" fontSize="16" fontWeight="bold" fill="#495057">
modell
</text>
{config.domains.map((domain, domainIdx) => {
const startAngle = domainIdx * angleStep - Math.PI / 2;
const endAngle = (domainIdx + 1) * angleStep - Math.PI / 2;
const midAngle = (startAngle + endAngle) / 2;
const totalGoals = domain.goals.length || 1;
const goalAngleStep = (endAngle - startAngle) / totalGoals;
return (
<g key={domainIdx}>
{/* Goals als radiale Segmente */}
{domain.goals.length > 0 ? domain.goals.map((goal, goalIdx) => {
const goalStartAngle = startAngle + goalIdx * goalAngleStep;
const goalEndAngle = startAngle + (goalIdx + 1) * goalAngleStep;
const goalMidAngle = (goalStartAngle + goalEndAngle) / 2;
// Farbschattierung berechnen
const goalColor = config.useGoalShading
? adjustColorBrightness(domain.color, -0.15 * goalIdx)
: domain.color;
// Radiale Darstellung der Outcomes
if (config.outcomeDisplay === 'radial') {
const totalOutcomes = goal.outcomes.length || 1;
const outcomeAngleStep = (goalEndAngle - goalStartAngle) / totalOutcomes;
return (
<g key={goalIdx}>
{goal.outcomes.length > 0 ? goal.outcomes.map((outcome, outcomeIdx) => {
const outcomeStartAngle = goalStartAngle + outcomeIdx * outcomeAngleStep;
const outcomeEndAngle = goalStartAngle + (outcomeIdx + 1) * outcomeAngleStep;
const outcomeMidAngle = (outcomeStartAngle + outcomeEndAngle) / 2;
const outcomeColor = config.useGoalShading
? adjustColorBrightness(goalColor, -0.1 * outcomeIdx)
: goalColor;
const outcomePath = describeArc(centerX, centerY, innerRadius, outerRadius, outcomeStartAngle, outcomeEndAngle);
return (
<g key={outcomeIdx}>
<path
d={outcomePath}
fill={outcomeColor}
stroke={config.separatorColor}
strokeWidth={config.separatorWidth}
opacity="0.85"
/>
{/* Outcome Label */}
<text
x={centerX + Math.cos(outcomeMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.75)}
y={centerY + Math.sin(outcomeMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.75)}
textAnchor="middle"
dominantBaseline="middle"
fontSize="9"
fill="white"
fontWeight="500"
transform={`rotate(${(outcomeMidAngle * 180 / Math.PI + 90)}, ${centerX + Math.cos(outcomeMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.75)}, ${centerY + Math.sin(outcomeMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.75)})`}
>
{outcome.length > 12 ? outcome.substring(0, 12) + '...' : outcome}
</text>
</g>
);
}) : (
<path
d={describeArc(centerX, centerY, innerRadius, outerRadius, goalStartAngle, goalEndAngle)}
fill={goalColor}
stroke={config.separatorColor}
strokeWidth={config.separatorWidth}
opacity="0.85"
/>
)}
{/* Goal Label */}
<text
x={centerX + Math.cos(goalMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.4)}
y={centerY + Math.sin(goalMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.4)}
textAnchor="middle"
dominantBaseline="middle"
fontSize="12"
fontWeight="bold"
fill="white"
transform={`rotate(${(goalMidAngle * 180 / Math.PI + 90)}, ${centerX + Math.cos(goalMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.4)}, ${centerY + Math.sin(goalMidAngle) * (innerRadius + (outerRadius - innerRadius) * 0.4)})`}
>
{goal.name.length > 15 ? goal.name.substring(0, 15) + '...' : goal.name}
</text>
</g>
);
}
// Konzentrische Darstellung der Outcomes
else {
const availableRadius = outerRadius - innerRadius;
if (config.outcomeRingMode === 'uniform') {
// Uniform: alle Ringe haben die gleiche Höhe
const ringHeight = availableRadius / maxOutcomes;
return (
<g key={goalIdx}>
{goal.outcomes.length > 0 ? goal.outcomes.map((outcome, outcomeIdx) => {
const ringInner = innerRadius + outcomeIdx * ringHeight;
const ringOuter = innerRadius + (outcomeIdx + 1) * ringHeight;
const ringMid = (ringInner + ringOuter) / 2;
const outcomeColor = config.useGoalShading
? adjustColorBrightness(goalColor, -0.1 * outcomeIdx)
: goalColor;
const ringPath = describeArc(centerX, centerY, ringInner, ringOuter, goalStartAngle, goalEndAngle);
return (
<g key={outcomeIdx}>
<path
d={ringPath}
fill={outcomeColor}
stroke={config.separatorColor}
strokeWidth={config.separatorWidth}
opacity="0.85"
/>
{/* Outcome Label */}
<text
x={centerX + Math.cos(goalMidAngle) * ringMid}
y={centerY + Math.sin(goalMidAngle) * ringMid}
textAnchor="middle"
dominantBaseline="middle"
fontSize="9"
fill="white"
fontWeight="500"
transform={`rotate(${(goalMidAngle * 180 / Math.PI + 90)}, ${centerX + Math.cos(goalMidAngle) * ringMid}, ${centerY + Math.sin(goalMidAngle) * ringMid})`}
>
{outcome.length > 12 ? outcome.substring(0, 12) + '...' : outcome}
</text>
</g>
);
}) : null}
{/* Ausgegrauer Bereich wenn weniger Outcomes als maxOutcomes */}
{goal.outcomes.length < maxOutcomes && (
<path
d={describeArc(centerX, centerY, innerRadius + goal.outcomes.length * ringHeight, outerRadius, goalStartAngle, goalEndAngle)}
fill="#d3d3d3"
stroke={config.separatorColor}
strokeWidth={config.separatorWidth}
opacity="0.3"
/>
)}
{/* Goal Label */}
<text
x={centerX + Math.cos(goalMidAngle) * (innerRadius + ringHeight * 0.5)}
y={centerY + Math.sin(goalMidAngle) * (innerRadius + ringHeight * 0.5)}
textAnchor="middle"
dominantBaseline="middle"
fontSize="12"
fontWeight="bold"
fill="white"
transform={`rotate(${(goalMidAngle * 180 / Math.PI + 90)}, ${centerX + Math.cos(goalMidAngle) * (innerRadius + ringHeight * 0.5)}, ${centerY + Math.sin(goalMidAngle) * (innerRadius + ringHeight * 0.5)})`}
>
{goal.name.length > 15 ? goal.name.substring(0, 15) + '...' : goal.name}
</text>
</g>
);
} else {
// Divided: Goal-Raum wird gleichmäßig auf Outcomes aufgeteilt
const numOutcomes = goal.outcomes.length || 1;
const ringHeight = availableRadius / numOutcomes;
return (
<g key={goalIdx}>
{goal.outcomes.length > 0 ? goal.outcomes.map((outcome, outcomeIdx) => {
const ringInner = innerRadius + outcomeIdx * ringHeight;
const ringOuter = innerRadius + (outcomeIdx + 1) * ringHeight;
const ringMid = (ringInner + ringOuter) / 2;
const outcomeColor = config.useGoalShading
? adjustColorBrightness(goalColor, -0.1 * outcomeIdx)
: goalColor;
const ringPath = describeArc(centerX, centerY, ringInner, ringOuter, goalStartAngle, goalEndAngle);
return (
<g key={outcomeIdx}>
<path
d={ringPath}
fill={outcomeColor}
stroke={config.separatorColor}
strokeWidth={config.separatorWidth}
opacity="0.85"
/>
{/* Outcome Label */}
<text
x={centerX + Math.cos(goalMidAngle) * ringMid}
y={centerY + Math.sin(goalMidAngle) * ringMid}
textAnchor="middle"
dominantBaseline="middle"
fontSize="9"
fill="white"
fontWeight="500"
transform={`rotate(${(goalMidAngle * 180 / Math.PI + 90)}, ${centerX + Math.cos(goalMidAngle) * ringMid}, ${centerY + Math.sin(goalMidAngle) * ringMid})`}
>
{outcome.length > 12 ? outcome.substring(0, 12) + '...' : outcome}
</text>
</g>
);
}) : (
<path
d={describeArc(centerX, centerY, innerRadius, outerRadius, goalStartAngle, goalEndAngle)}
fill={goalColor}
stroke={config.separatorColor}
strokeWidth={config.separatorWidth}
opacity="0.85"
/>
)}
{/* Goal Label */}
<text
x={centerX + Math.cos(goalMidAngle) * (innerRadius + ringHeight * 0.5)}
y={centerY + Math.sin(goalMidAngle) * (innerRadius + ringHeight * 0.5)}
textAnchor="middle"
dominantBaseline="middle"
fontSize="12"
fontWeight="bold"
fill="white"
transform={`rotate(${(goalMidAngle * 180 / Math.PI + 90)}, ${centerX + Math.cos(goalMidAngle) * (innerRadius + ringHeight * 0.5)}, ${centerY + Math.sin(goalMidAngle) * (innerRadius + ringHeight * 0.5)})`}
>
{goal.name.length > 15 ? goal.name.substring(0, 15) + '...' : goal.name}
</text>
</g>
);
}
}
}) : (
// Wenn keine Goals vorhanden, gesamte Domain als ein Segment
<path
d={describeArc(centerX, centerY, innerRadius, outerRadius, startAngle, endAngle)}
fill={domain.color}
stroke={config.separatorColor}
strokeWidth={config.separatorWidth}
opacity="0.85"
/>
)}
{/* Domain Label am äußeren Rand */}
<text
x={centerX + Math.cos(midAngle) * (outerRadius + 30)}
y={centerY + Math.sin(midAngle) * (outerRadius + 30)}
textAnchor="middle"
dominantBaseline="middle"
fontSize="16"
fontWeight="bold"
fill={domain.color}
>
{domain.name}
</text>
{/* Trennlinie zwischen Domains */}
<line
x1={centerX + Math.cos(endAngle) * innerRadius}
y1={centerY + Math.sin(endAngle) * innerRadius}
x2={centerX + Math.cos(endAngle) * outerRadius}
y2={centerY + Math.sin(endAngle) * outerRadius}
stroke={config.separatorColor}
strokeWidth={config.separatorWidth * 1.5}
/>
</g>
);
})}
</svg>
);
};
const ConcentricVisualization = () => {
const centerX = 500;
const centerY = 500;
const innerRadius = 80;
const ringWidth = 100;
return (
<svg id="competence-visualization" width="1000" height="1000" viewBox="0 0 1000 1000">
{/* Center Circle */}
<circle cx={centerX} cy={centerY} r={innerRadius} fill="#f8f9fa" stroke="#dee2e6" strokeWidth="2" />
<text x={centerX} y={centerY} textAnchor="middle" dominantBaseline="middle" fontSize="16" fontWeight="bold" fill="#495057">
Kompetenz-
</text>
<text x={centerX} y={centerY + 20} textAnchor="middle" dominantBaseline="middle" fontSize="16" fontWeight="bold" fill="#495057">
modell
</text>
{config.domains.map((domain, domainIdx) => {
const domainRadius = innerRadius + (domainIdx + 1) * ringWidth;
const angleStep = (2 * Math.PI) / (domain.goals.length || 8);
return (
<g key={domainIdx}>
{/* Domain ring */}
<circle
cx={centerX}
cy={centerY}
r={domainRadius}
fill="none"
stroke={domain.color}
strokeWidth={ringWidth - 10}
opacity="0.3"
/>
{/* Domain label */}
<text
x={centerX}
y={centerY - domainRadius - 10}
textAnchor="middle"
fontSize="14"
fontWeight="bold"
fill={domain.color}
>
{domain.name}
</text>
{/* Goals as segments */}
{domain.goals.length > 0 ? domain.goals.map((goal, goalIdx) => {
const angle = goalIdx * angleStep - Math.PI / 2;
const x = centerX + Math.cos(angle) * domainRadius;
const y = centerY + Math.sin(angle) * domainRadius;
return (
<g key={goalIdx}>
{/* Goal point */}
<circle cx={x} cy={y} r="8" fill={domain.color} stroke="white" strokeWidth="2" />
{/* Goal label */}
<text
x={x + Math.cos(angle) * 30}
y={y + Math.sin(angle) * 30}
textAnchor="middle"
dominantBaseline="middle"
fontSize="10"
fill={domain.color}
fontWeight="600"
>
{goal.name.length > 20 ? goal.name.substring(0, 20) + '...' : goal.name}
</text>
{/* Outcomes as smaller points */}
{goal.outcomes.map((outcome, outcomeIdx) => {
const outcomeAngleOffset = (outcomeIdx - (goal.outcomes.length - 1) / 2) * 0.15;
const outcomeAngle = angle + outcomeAngleOffset;
const outcomeRadius = domainRadius + 25;
const ox = centerX + Math.cos(outcomeAngle) * outcomeRadius;
const oy = centerY + Math.sin(outcomeAngle) * outcomeRadius;
return (
<g key={outcomeIdx}>
<line x1={x} y1={y} x2={ox} y2={oy} stroke={domain.color} strokeWidth="1" opacity="0.5" />
<circle cx={ox} cy={oy} r="4" fill={domain.color} opacity="0.7" stroke="white" strokeWidth="1" />
</g>
);
})}
</g>
);
}) : null}
</g>
);
})}
</svg>
);
};
const describeArc = (x, y, innerRadius, outerRadius, startAngle, endAngle) => {
const innerStart = polarToCartesian(x, y, innerRadius, endAngle);
const innerEnd = polarToCartesian(x, y, innerRadius, startAngle);
const outerStart = polarToCartesian(x, y, outerRadius, endAngle);
const outerEnd = polarToCartesian(x, y, outerRadius, startAngle);
const largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1";
return [
"M", outerStart.x, outerStart.y,
"A", outerRadius, outerRadius, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
"L", innerEnd.x, innerEnd.y,
"A", innerRadius, innerRadius, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
"Z"
].join(" ");
};
const polarToCartesian = (centerX, centerY, radius, angleInRadians) => {
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
};
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-6xl mx-auto">
<h1 className="text-3xl font-bold text-gray-800 mb-8">Kompetenzmodell Builder</h1>
{/* Progress Steps */}
<div className="flex items-center justify-center mb-8 space-x-4">
{['Struktur', 'Domains', 'Goals & Outcomes', 'Visualisierung'].map((label, idx) => (
<div key={idx} className="flex items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold ${
step > idx + 1 ? 'bg-green-500 text-white' :
step === idx + 1 ? 'bg-blue-500 text-white' :
'bg-gray-300 text-gray-600'
}`}>
{idx + 1}
</div>
<span className="ml-2 text-sm font-medium text-gray-700">{label}</span>
{idx < 3 && <ChevronRight className="ml-4 text-gray-400" size={20} />}
</div>
))}
</div>
{/* Step 1: Grundstruktur */}
{step === 1 && (
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Schritt 1: Grundstruktur</h2>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Anzahl der Domains (Ebene 1)
</label>
<input
type="number"
min="2"
max="8"
value={config.numDomains}
onChange={(e) => updateDomainCount(parseInt(e.target.value))}
className="w-32 px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Anordnung der Domains
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
name="arrangement"
value="radial"
checked={config.domainArrangement === 'radial'}
onChange={(e) => setConfig({ ...config, domainArrangement: e.target.value })}
className="mr-2"
/>
<span>Radial (Kuchenstücke)</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="arrangement"
value="concentric"
checked={config.domainArrangement === 'concentric'}
onChange={(e) => setConfig({ ...config, domainArrangement: e.target.value })}
className="mr-2"
/>
<span>Konzentrisch (Ringe)</span>
</label>
</div>
</div>
{config.domainArrangement === 'radial' && (
<div className="border-t pt-6 space-y-6">
<h3 className="text-lg font-semibold text-gray-800">Visualisierungsoptionen</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Farbe der Trennlinien
</label>
<div className="flex items-center space-x-4">
<input
type="color"
value={config.separatorColor}
onChange={(e) => setConfig({ ...config, separatorColor: e.target.value })}
className="w-16 h-10 rounded cursor-pointer"
/>
<span className="text-sm text-gray-600">{config.separatorColor}</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Stärke der Trennlinien: {config.separatorWidth === 0 ? 'Keine' : `${config.separatorWidth}px`}
</label>
<input
type="range"
min="0"
max="10"
value={config.separatorWidth}
onChange={(e) => setConfig({ ...config, separatorWidth: parseInt(e.target.value) })}
className="w-full"
/>
</div>
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={config.useGoalShading}
onChange={(e) => setConfig({ ...config, useGoalShading: e.target.checked })}
className="mr-2"
/>
<span className="text-sm font-medium text-gray-700">
Goals durch Farbschattierungen unterscheiden
</span>
</label>
<p className="text-xs text-gray-500 ml-6 mt-1">
Wenn aktiviert, wird jedes Goal innerhalb einer Domain mit einer leicht dunkleren Schattierung dargestellt
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Darstellung der Outcomes
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
name="outcomeDisplay"
value="radial"
checked={config.outcomeDisplay === 'radial'}
onChange={(e) => setConfig({ ...config, outcomeDisplay: e.target.value })}
className="mr-2"
/>
<span className="text-sm">Radial (Untersegmente)</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="outcomeDisplay"
value="concentric"
checked={config.outcomeDisplay === 'concentric'}
onChange={(e) => setConfig({ ...config, outcomeDisplay: e.target.value })}
className="mr-2"
/>
<span className="text-sm">Konzentrisch (Teilkreise)</span>
</label>
</div>
</div>
{config.outcomeDisplay === 'concentric' && (
<div className="ml-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Höhe der Outcome-Ringe
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
name="outcomeRingMode"
value="uniform"
checked={config.outcomeRingMode === 'uniform'}
onChange={(e) => setConfig({ ...config, outcomeRingMode: e.target.value })}
className="mr-2"
/>
<span className="text-sm">Einheitliche Höhe (ausgegraut bei weniger Outcomes)</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="outcomeRingMode"
value="divided"
checked={config.outcomeRingMode === 'divided'}
onChange={(e) => setConfig({ ...config, outcomeRingMode: e.target.value })}
className="mr-2"
/>
<span className="text-sm">Gleichmäßig aufgeteilt (variable Höhe pro Goal)</span>
</label>
</div>
</div>
)}
</div>
)}
</div>
<button
onClick={() => setStep(2)}
className="mt-8 px-6 py-3 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-600 transition"
>
Weiter zu Domains
</button>
</div>
)}
{/* Step 2: Domains konfigurieren */}
{step === 2 && (
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Schritt 2: Domains benennen</h2>
<div className="space-y-4">
{config.domains.map((domain, idx) => (
<div key={idx} className="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
<div className="flex-1">
<input
type="text"
value={domain.name}
onChange={(e) => updateDomainName(idx, e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
placeholder="Domain Name"
/>
</div>
<input
type="color"
value={domain.color}
onChange={(e) => updateDomainColor(idx, e.target.value)}
className="w-16 h-10 rounded cursor-pointer"
/>
</div>
))}
</div>
<div className="flex space-x-4 mt-8">
<button
onClick={() => setStep(1)}
className="px-6 py-3 bg-gray-300 text-gray-700 font-semibold rounded-lg hover:bg-gray-400 transition"
>
Zurück
</button>
<button
onClick={() => setStep(3)}
className="px-6 py-3 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-600 transition"
>
Weiter zu Goals & Outcomes
</button>
</div>
</div>
)}
{/* Step 3: Goals und Outcomes */}
{step === 3 && (
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Schritt 3: Goals & Outcomes definieren</h2>
<div className="space-y-6">
{config.domains.map((domain, domainIdx) => (
<div key={domainIdx} className="border-2 rounded-lg p-6" style={{ borderColor: domain.color }}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold" style={{ color: domain.color }}>
{domain.name}
</h3>
<button
onClick={() => addGoal(domainIdx)}
className="flex items-center px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition"
>
<Plus size={16} className="mr-2" />
Goal hinzufügen
</button>
</div>
<div className="space-y-4">
{domain.goals.map((goal, goalIdx) => (
<div key={goalIdx} className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center space-x-2 mb-3">
<input
type="text"
value={goal.name}
onChange={(e) => updateGoalName(domainIdx, goalIdx, e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md"
placeholder="Goal Name"
/>
<button
onClick={() => removeGoal(domainIdx, goalIdx)}
className="p-2 bg-red-500 text-white rounded hover:bg-red-600"
>
<Minus size={16} />
</button>
</div>
<div className="ml-4 space-y-2">
<button
onClick={() => addOutcome(domainIdx, goalIdx)}
className="text-sm px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
>
+ Outcome hinzufügen
</button>
{goal.outcomes.map((outcome, outcomeIdx) => (
<div key={outcomeIdx} className="flex items-center space-x-2">
<input
type="text"
value={outcome}
onChange={(e) => updateOutcomeName(domainIdx, goalIdx, outcomeIdx, e.target.value)}
className="flex-1 px-3 py-1 text-sm border border-gray-300 rounded"
placeholder="Outcome Name"
/>
<button
onClick={() => removeOutcome(domainIdx, goalIdx, outcomeIdx)}
className="p-1 text-red-500 hover:bg-red-50 rounded"
>
<Minus size={14} />
</button>
</div>
))}
</div>
</div>
))}
</div>
{domain.goals.length === 0 && (
<p className="text-gray-500 text-sm italic">Noch keine Goals definiert</p>
)}
</div>
))}
</div>
<div className="flex space-x-4 mt-8">
<button
onClick={() => setStep(2)}
className="px-6 py-3 bg-gray-300 text-gray-700 font-semibold rounded-lg hover:bg-gray-400 transition"
>
Zurück
</button>
<button
onClick={generateVisualization}
className="px-6 py-3 bg-green-500 text-white font-semibold rounded-lg hover:bg-green-600 transition"
>
Visualisierung generieren
</button>
</div>
</div>
)}
{/* Step 4: Visualisierung */}
{step === 4 && (
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Schritt 4: Visualisierung</h2>
<div className="flex justify-center mb-6">
{config.domainArrangement === 'radial' ? <RadialVisualization /> : <ConcentricVisualization />}
</div>
<div className="flex justify-center space-x-4 mt-8">
<button
onClick={() => setStep(3)}
className="px-6 py-3 bg-gray-300 text-gray-700 font-semibold rounded-lg hover:bg-gray-400 transition"
>
Zurück zur Konfiguration
</button>
<button
onClick={downloadSVG}
className="flex items-center px-6 py-3 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-600 transition"
>
<Download size={16} className="mr-2" />
SVG herunterladen
</button>
</div>
</div>
)}
</div>
</div>
);
}","UPDATEDAT":"2025-10-16T14:02:35.786Z","ID":"db4602c7-f0bc-46d5-889e-f5edad1a3e98","TITLE":"v2"}