Center avatar with orbit links

Implemented a Wii U–style hub layout placing the PFP at the center with orbiting circular arrangement of navigation cards. Added dynamic SVG lines connecting the center avatar to each card, and responsive adjustments for mobile.

X-Lovable-Edit-ID: edt-eb52c3ed-7c6d-40ef-b672-b799fc8a3f60
This commit is contained in:
gpt-engineer-app[bot] 2026-03-07 21:36:45 +00:00
commit ed812b8be3

View file

@ -18,6 +18,7 @@ const Index = () => {
const [lines, setLines] = useState<{ x1: number; y1: number; x2: number; y2: number }[]>([]); const [lines, setLines] = useState<{ x1: number; y1: number; x2: number; y2: number }[]>([]);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [dims, setDims] = useState({ w: 0, h: 0 });
const updateLines = useCallback(() => { const updateLines = useCallback(() => {
if (!containerRef.current || !avatarRef.current) return; if (!containerRef.current || !avatarRef.current) return;
@ -25,13 +26,17 @@ const Index = () => {
const avatarRect = avatarRef.current.getBoundingClientRect(); const avatarRect = avatarRef.current.getBoundingClientRect();
const cx = avatarRect.left + avatarRect.width / 2 - containerRect.left; const cx = avatarRect.left + avatarRect.width / 2 - containerRect.left;
const cy = avatarRect.top + avatarRect.height / 2 - containerRect.top; const cy = avatarRect.top + avatarRect.height / 2 - containerRect.top;
setDims({ w: containerRect.width, h: containerRect.height });
const newLines = cardRefs.current.map((card) => { const newLines = cardRefs.current.map((card) => {
if (!card) return { x1: cx, y1: cy, x2: cx, y2: cy }; if (!card) return { x1: cx, y1: cy, x2: cx, y2: cy };
const cardRect = card.getBoundingClientRect(); const cardRect = card.getBoundingClientRect();
const cardCx = cardRect.left + cardRect.width / 2 - containerRect.left; return {
const cardCy = cardRect.top + cardRect.height / 2 - containerRect.top; x1: cx,
return { x1: cx, y1: cy, x2: cardCx, y2: cardCy }; y1: cy,
x2: cardRect.left + cardRect.width / 2 - containerRect.left,
y2: cardRect.top + cardRect.height / 2 - containerRect.top,
};
}); });
setLines(newLines); setLines(newLines);
}, []); }, []);
@ -40,29 +45,30 @@ const Index = () => {
updateLines(); updateLines();
const timer = setTimeout(() => setMounted(true), 100); const timer = setTimeout(() => setMounted(true), 100);
window.addEventListener('resize', updateLines); window.addEventListener('resize', updateLines);
return () => { return () => { window.removeEventListener('resize', updateLines); clearTimeout(timer); };
window.removeEventListener('resize', updateLines);
clearTimeout(timer);
};
}, [updateLines]); }, [updateLines]);
useEffect(() => { useEffect(() => {
// Re-calc after fonts/images load const t = setTimeout(updateLines, 500);
const timer = setTimeout(updateLines, 500); return () => clearTimeout(t);
return () => clearTimeout(timer);
}, [updateLines]); }, [updateLines]);
const handleMouseMove = useCallback((e: React.MouseEvent) => { const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!containerRef.current) return; setMousePos({ x: e.clientX, y: e.clientY });
const rect = containerRef.current.getBoundingClientRect();
setMousePos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
}, []); }, []);
// On mobile, use a smaller radius
const isMobile = dims.w < 640;
const radius = isMobile ? 140 : 220;
const count = items.length;
// Start from top (-90deg) and spread evenly
const angleOffset = -90;
return ( return (
<div <div
ref={containerRef} ref={containerRef}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
className="min-h-screen bg-background relative overflow-hidden flex flex-col items-center justify-start py-10 md:py-16 px-4" className="min-h-screen bg-background relative overflow-hidden flex items-center justify-center"
> >
{/* Global mouse glow */} {/* Global mouse glow */}
<div <div
@ -97,100 +103,85 @@ const Index = () => {
</defs> </defs>
{lines.map((line, i) => ( {lines.map((line, i) => (
<g key={i}> <g key={i}>
{/* Glow line */}
<line <line
x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2} x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2}
stroke="hsl(25 95% 55% / 0.1)" stroke="hsl(25 95% 55% / 0.08)"
strokeWidth="4" strokeWidth="4"
filter="url(#glow)" filter="url(#glow)"
/> />
{/* Main line */}
<line <line
x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2} x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2}
stroke="url(#lineGrad)" stroke="url(#lineGrad)"
strokeWidth="1.5" strokeWidth="1.5"
strokeDasharray="6 4" strokeDasharray="6 4"
/> />
{/* End dot on card */}
<circle cx={line.x2} cy={line.y2} r="3" fill="hsl(25 95% 55% / 0.4)" /> <circle cx={line.x2} cy={line.y2} r="3" fill="hsl(25 95% 55% / 0.4)" />
</g> </g>
))} ))}
{/* Center dot */}
{lines.length > 0 && ( {lines.length > 0 && (
<circle cx={lines[0]?.x1} cy={lines[0]?.y1} r="5" fill="hsl(25 95% 55% / 0.6)" /> <circle cx={lines[0]?.x1} cy={lines[0]?.y1} r="5" fill="hsl(25 95% 55% / 0.6)" />
)} )}
</svg> </svg>
{/* Center Avatar Hub */} {/* Hub container - centered */}
<div ref={avatarRef} className="relative z-10 mb-8 md:mb-12 flex flex-col items-center"> <div className="relative z-10" style={{ width: radius * 2 + (isMobile ? 120 : 160), height: radius * 2 + (isMobile ? 120 : 160) }}>
<div className="glass-card rounded-full p-1.5 ring-2 ring-primary/20"> {/* Center Avatar */}
<img <div
src={avatar} ref={avatarRef}
alt="Mohamad" className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center"
className="w-28 h-28 md:w-36 md:h-36 rounded-full" >
/> <div className="glass-card rounded-full p-1.5 ring-2 ring-primary/20">
<img
src={avatar}
alt="Mohamad"
className="w-24 h-24 md:w-32 md:h-32 rounded-full"
/>
</div>
<div className="mt-3 text-center whitespace-nowrap">
<h1 className="text-lg md:text-xl font-heading font-bold text-foreground">👋 Hey!</h1>
<p className="text-xs text-muted-foreground">
I'm <span className="text-gradient font-semibold">Mohamad</span>
</p>
</div>
</div> </div>
<div className="mt-4 text-center">
<h1 className="text-2xl md:text-3xl font-heading font-bold text-foreground">
👋 Hey There!
</h1>
<p className="text-muted-foreground text-sm mt-1">
I'm <span className="text-gradient font-semibold">Mohamad</span> welcome to my space.
</p>
</div>
</div>
{/* Cards arranged around */} {/* Orbiting cards */}
<div className="relative z-10 w-full max-w-3xl"> {items.map((item, i) => {
{/* Top row - 3 cards */} const angle = ((360 / count) * i + angleOffset) * (Math.PI / 180);
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 md:gap-4 mb-3 md:mb-4"> const x = Math.cos(angle) * radius;
{items.slice(0, 3).map((item, i) => ( const y = Math.sin(angle) * radius;
const cardW = isMobile ? 110 : 130;
const cardH = isMobile ? 80 : 90;
return (
<a <a
key={item.label} key={item.label}
ref={(el) => { cardRefs.current[i] = el; }} ref={(el) => { cardRefs.current[i] = el; }}
href={item.href} href={item.href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block" className="absolute"
style={{
left: `calc(50% + ${x}px - ${cardW / 2}px)`,
top: `calc(50% + ${y}px - ${cardH / 2}px)`,
width: cardW,
}}
> >
<GlassCard className="p-4 md:p-5 h-full link-arrow-parent cursor-pointer"> <GlassCard className="p-3 md:p-4 link-arrow-parent cursor-pointer">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<item.icon className="w-5 h-5 text-primary mb-2" /> <item.icon className="w-4 h-4 text-primary mb-1.5" />
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" /> <ExternalLink className="w-3 h-3 text-muted-foreground link-arrow" />
</div> </div>
<p className="font-heading font-semibold text-foreground text-sm md:text-base">{item.label}</p> <p className="font-heading font-semibold text-foreground text-xs md:text-sm leading-tight">{item.label}</p>
<p className="text-xs text-muted-foreground mt-0.5">{item.desc}</p> <p className="text-[10px] text-muted-foreground mt-0.5 leading-tight">{item.desc}</p>
</GlassCard> </GlassCard>
</a> </a>
))} );
</div> })}
{/* Bottom row - 2 cards centered */}
<div className="grid grid-cols-2 gap-3 md:gap-4 max-w-md mx-auto">
{items.slice(3).map((item, i) => (
<a
key={item.label}
ref={(el) => { cardRefs.current[i + 3] = el; }}
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="block"
>
<GlassCard className="p-4 md:p-5 h-full link-arrow-parent cursor-pointer">
<div className="flex items-start justify-between">
<item.icon className="w-5 h-5 text-primary mb-2" />
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
</div>
<p className="font-heading font-semibold text-foreground text-sm md:text-base">{item.label}</p>
<p className="text-xs text-muted-foreground mt-0.5">{item.desc}</p>
</GlassCard>
</a>
))}
</div>
</div> </div>
{/* Footer */} {/* Footer */}
<p className="relative z-10 text-center text-xs text-muted-foreground font-mono mt-10"> <p className="absolute bottom-4 left-1/2 -translate-x-1/2 text-xs text-muted-foreground font-mono z-10">
damaj.tech damaj.tech
</p> </p>
</div> </div>