first commit

This commit is contained in:
2026-08-17 19:45:29 +05:30
commit 3c27da77a8
510 changed files with 137749 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
export default function EcosystemWheel({ layers, onLayerClick, selectedLayer, onLayerHover }) {
const [hoveredIndex, setHoveredIndex] = useState(null);
const centerX = 300;
const centerY = 300;
// Icon positions - precisely positioned in center of each puzzle piece
const iconPositions = [
{ x: 385, y: 160 }, // 30° - Buyer (top-right)
{ x: 460, y: 300 }, // 60° - Enterprises (top-right)
{ x: 380, y: 440 }, // 120° - Sectors (bottom-right)
{ x: 220, y: 440 }, // 180° - Partner (bottom-center)
{ x: 140, y: 300}, // 240° - Approved Vendor (bottom-left)
{ x: 220, y: 160} // 300° - Workforce (top-left)
];
// Create interlocking puzzle pieces
const createOuterPuzzle = (index, total) => {
const startAngle = index * 360 / total - 90;
const endAngle = (index + 1) * 360 / total - 90;
const midAngle = (startAngle + endAngle) / 2;
const innerRadius = 110;
const outerRadius = 210;
const tabSize = 18;
const toRad = (deg) => deg * Math.PI / 180;
const startInner = {
x: centerX + innerRadius * Math.cos(toRad(startAngle)),
y: centerY + innerRadius * Math.sin(toRad(startAngle))
};
const endInner = {
x: centerX + innerRadius * Math.cos(toRad(endAngle)),
y: centerY + innerRadius * Math.sin(toRad(endAngle))
};
const startOuter = {
x: centerX + outerRadius * Math.cos(toRad(startAngle)),
y: centerY + outerRadius * Math.sin(toRad(startAngle))
};
const endOuter = {
x: centerX + outerRadius * Math.cos(toRad(endAngle)),
y: centerY + outerRadius * Math.sin(toRad(endAngle))
};
const innerTabAngle = startAngle + (endAngle - startAngle) * 0.5;
const outerTabAngle = startAngle + (endAngle - startAngle) * 0.5;
const innerTabOut = {
x: centerX + (innerRadius - tabSize) * Math.cos(toRad(innerTabAngle)),
y: centerY + (innerRadius - tabSize) * Math.sin(toRad(innerTabAngle))
};
const outerTabOut = {
x: centerX + (outerRadius + tabSize) * Math.cos(toRad(outerTabAngle)),
y: centerY + (outerRadius + tabSize) * Math.sin(toRad(outerTabAngle))
};
let path = `M ${startInner.x} ${startInner.y}`;
const innerArc1End = startAngle + (endAngle - startAngle) * 0.4;
const innerArc2Start = startAngle + (endAngle - startAngle) * 0.6;
path += ` A ${innerRadius} ${innerRadius} 0 0 1 ${centerX + innerRadius * Math.cos(toRad(innerArc1End))} ${centerY + innerRadius * Math.sin(toRad(innerArc1End))}`;
path += ` Q ${innerTabOut.x} ${innerTabOut.y} ${centerX + innerRadius * Math.cos(toRad(innerArc2Start))} ${centerY + innerRadius * Math.sin(toRad(innerArc2Start))}`;
path += ` A ${innerRadius} ${innerRadius} 0 0 1 ${endInner.x} ${endInner.y}`;
path += ` L ${endOuter.x} ${endOuter.y}`;
const outerArc1End = endAngle - (endAngle - startAngle) * 0.4;
const outerArc2Start = endAngle - (endAngle - startAngle) * 0.6;
path += ` A ${outerRadius} ${outerRadius} 0 0 0 ${centerX + outerRadius * Math.cos(toRad(outerArc1End))} ${centerY + outerRadius * Math.sin(toRad(outerArc1End))}`;
path += ` Q ${outerTabOut.x} ${outerTabOut.y} ${centerX + outerRadius * Math.cos(toRad(outerArc2Start))} ${centerY + outerRadius * Math.sin(toRad(outerArc2Start))}`;
path += ` A ${outerRadius} ${outerRadius} 0 0 0 ${startOuter.x} ${startOuter.y}`;
path += ` L ${startInner.x} ${startInner.y}`;
path += " Z";
return path;
};
const createCenterPuzzle = () => {
const radius = 90;
const numTabs = layers.length;
let path = "";
for (let i = 0; i < numTabs; i++) {
const angle1 = i * 360 / numTabs - 90;
const angle2 = (i + 1) * 360 / numTabs - 90;
const midAngle = (angle1 + angle2) / 2;
const tabSize = 18;
const toRad = (deg) => deg * Math.PI / 180;
const start = {
x: centerX + radius * Math.cos(toRad(angle1)),
y: centerY + radius * Math.sin(toRad(angle1))
};
const end = {
x: centerX + radius * Math.cos(toRad(angle2)),
y: centerY + radius * Math.sin(toRad(angle2))
};
const tabIn = {
x: centerX + (radius + tabSize) * Math.cos(toRad(midAngle)),
y: centerY + (radius + tabSize) * Math.sin(toRad(midAngle))
};
if (i === 0) {
path += `M ${start.x} ${start.y}`;
}
const arc1End = angle1 + (angle2 - angle1) * 0.4;
const arc2Start = angle1 + (angle2 - angle1) * 0.6;
path += ` A ${radius} ${radius} 0 0 1 ${centerX + radius * Math.cos(toRad(arc1End))} ${centerY + radius * Math.sin(toRad(arc1End))}`;
path += ` Q ${tabIn.x} ${tabIn.y} ${centerX + radius * Math.cos(toRad(arc2Start))} ${centerY + radius * Math.sin(toRad(arc2Start))}`;
path += ` A ${radius} ${radius} 0 0 1 ${end.x} ${end.y}`;
}
path += " Z";
return path;
};
return (
<div className="relative w-full flex flex-col items-center py-8" style={{ minHeight: '700px' }}>
<div className="text-center mb-6">
<h3 className="text-2xl font-bold text-[#1C323E] mb-2">KROW Ecosystem</h3>
<p className="text-slate-600 text-sm">Hover over each piece to see details • Click to explore</p>
</div>
<div className="relative">
<svg width="620" height="600" viewBox="0 0 620 600" className="mx-auto drop-shadow-xl">
<defs>
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="4" />
<feOffset dx="0" dy="3" result="offsetblur" />
<feComponentTransfer>
<feFuncA type="linear" slope="0.3" />
</feComponentTransfer>
<feMerge>
<feMergeNode />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* Outer Puzzle Pieces */}
{layers.map((layer, index) => {
const angle = index * 360 / layers.length - 90;
const rad = angle * Math.PI / 180;
// Use exact icon position from array
const iconX = iconPositions[index].x;
const iconY = iconPositions[index].y;
// Label position - Outside puzzle
const labelRadius = 240;
const labelX = centerX + labelRadius * Math.cos(rad);
const labelY = centerY + labelRadius * Math.sin(rad);
const isHovered = hoveredIndex === index;
const Icon = layer.icon;
return (
<g key={index}>
{/* Puzzle Piece */}
<motion.path
d={createOuterPuzzle(index, layers.length)}
fill={isHovered ? "#F8FAFC" : "#FFFFFF"}
stroke={isHovered ? "#0A39DF" : "#CBD5E1"}
strokeWidth={isHovered ? "3" : "2"}
filter="url(#shadow)"
className="cursor-pointer transition-all duration-300"
onMouseEnter={() => {
setHoveredIndex(index);
onLayerHover?.(layer);
}}
onMouseLeave={() => {
setHoveredIndex(null);
onLayerHover?.(null);
}}
onClick={() => onLayerClick?.(layer)}
animate={{
scale: isHovered ? 1.05 : 1
}}
transition={{ duration: 0.3 }}
style={{
transformOrigin: `${centerX}px ${centerY}px`
}} />
{/* Icon - Exact positioned in puzzle piece */}
<circle
cx={iconX}
cy={iconY}
r="24"
fill="#1C323E"
className="pointer-events-none transition-all duration-300" />
<foreignObject
x={iconX - 24}
y={iconY - 24}
width="48"
height="48"
className="pointer-events-none">
<div className="w-full h-full flex items-center justify-center">
<Icon className="w-7 h-7 text-white" />
</div>
</foreignObject>
{/* Label - Outside puzzle */}
<foreignObject
x={labelX - 90}
y={labelY - 25}
width="180"
height="50"
className="pointer-events-none">
<div className="w-full h-full flex items-center justify-center">
<div className="bg-white px-5 py-2.5 rounded-xl shadow-lg border border-slate-200">
<p className={`font-bold text-sm text-center leading-tight whitespace-nowrap transition-colors ${
isHovered ? 'text-[#0A39DF]' : 'text-[#1C323E]'}`
}>
{layer.name}
</p>
</div>
</div>
</foreignObject>
</g>);
})}
{/* Center Puzzle Piece */}
<motion.path
d={createCenterPuzzle()}
fill="#FFFFFF"
stroke="#CBD5E1"
strokeWidth="2"
filter="url(#shadow)"
animate={{
scale: hoveredIndex !== null ? 1.03 : 1
}}
transition={{ duration: 0.3 }}
style={{
transformOrigin: `${centerX}px ${centerY}px`
}} />
{/* Center Logo */}
<foreignObject
x={centerX - 40}
y={centerY - 40}
width="80"
height="80"
className="pointer-events-none">
<div className="w-full h-full flex items-center justify-center">
<img
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/3ba390829_KROWlogo.png"
alt="KROW"
className="w-16 h-16 object-contain" />
</div>
</foreignObject>
</svg>
{/* Centered Metrics Display */}
<AnimatePresence>
{hoveredIndex !== null &&
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none z-50">
<div className="bg-white rounded-2xl shadow-2xl border-2 border-[#0A39DF] p-6 min-w-[300px]">
<div className="flex items-center justify-center gap-3 mb-5 pb-4 border-b border-slate-200">
<div className="w-12 h-12 rounded-xl bg-[#0A39DF] flex items-center justify-center shadow-md">
{React.createElement(layers[hoveredIndex].icon, { className: "w-6 h-6 text-white" })}
</div>
<h4 className="font-bold text-[#1C323E] text-lg">{layers[hoveredIndex].name}</h4>
</div>
<div className="space-y-3">
{Object.entries(layers[hoveredIndex].metrics).map(([key, value]) =>
<div key={key} className="flex justify-between items-center gap-6">
<span className="text-slate-600 text-sm capitalize font-medium">
{key.replace(/([A-Z])/g, ' $1').trim()}
</span>
<span className="font-bold text-lg text-[#0A39DF]">
{value}
</span>
</div>
)}
</div>
<div className="mt-5 pt-4 border-t border-slate-200 text-center">
<p className="text-xs text-slate-500">Click to view full dashboard</p>
</div>
</div>
</motion.div>
}
</AnimatePresence>
</div>
</div>);
}