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 [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [mounted, setMounted] = useState(false);
const [dims, setDims] = useState({ w: 0, h: 0 });
const updateLines = useCallback(() => {
if (!containerRef.current || !avatarRef.current) return;
@ -25,13 +26,17 @@ const Index = () => {
const avatarRect = avatarRef.current.getBoundingClientRect();
const cx = avatarRect.left + avatarRect.width / 2 - containerRect.left;
const cy = avatarRect.top + avatarRect.height / 2 - containerRect.top;
setDims({ w: containerRect.width, h: containerRect.height });
const newLines = cardRefs.current.map((card) => {
if (!card) return { x1: cx, y1: cy, x2: cx, y2: cy };
const cardRect = card.getBoundingClientRect();
const cardCx = cardRect.left + cardRect.width / 2 - containerRect.left;
const cardCy = cardRect.top + cardRect.height / 2 - containerRect.top;
return { x1: cx, y1: cy, x2: cardCx, y2: cardCy };
return {
x1: cx,
y1: cy,
x2: cardRect.left + cardRect.width / 2 - containerRect.left,
y2: cardRect.top + cardRect.height / 2 - containerRect.top,
};
});
setLines(newLines);
}, []);
@ -40,29 +45,30 @@ const Index = () => {
updateLines();
const timer = setTimeout(() => setMounted(true), 100);
window.addEventListener('resize', updateLines);
return () => {
window.removeEventListener('resize', updateLines);
clearTimeout(timer);
};
return () => { window.removeEventListener('resize', updateLines); clearTimeout(timer); };
}, [updateLines]);
useEffect(() => {
// Re-calc after fonts/images load
const timer = setTimeout(updateLines, 500);
return () => clearTimeout(timer);
const t = setTimeout(updateLines, 500);
return () => clearTimeout(t);
}, [updateLines]);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
setMousePos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
setMousePos({ x: e.clientX, y: e.clientY });
}, []);
// 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 (
<div
ref={containerRef}
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 */}
<div
@ -97,100 +103,85 @@ const Index = () => {
</defs>
{lines.map((line, i) => (
<g key={i}>
{/* Glow line */}
<line
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"
filter="url(#glow)"
/>
{/* Main line */}
<line
x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2}
stroke="url(#lineGrad)"
strokeWidth="1.5"
strokeDasharray="6 4"
/>
{/* End dot on card */}
<circle cx={line.x2} cy={line.y2} r="3" fill="hsl(25 95% 55% / 0.4)" />
</g>
))}
{/* Center dot */}
{lines.length > 0 && (
<circle cx={lines[0]?.x1} cy={lines[0]?.y1} r="5" fill="hsl(25 95% 55% / 0.6)" />
)}
</svg>
{/* Center Avatar Hub */}
<div ref={avatarRef} className="relative z-10 mb-8 md:mb-12 flex flex-col items-center">
<div className="glass-card rounded-full p-1.5 ring-2 ring-primary/20">
<img
src={avatar}
alt="Mohamad"
className="w-28 h-28 md:w-36 md:h-36 rounded-full"
/>
{/* Hub container - centered */}
<div className="relative z-10" style={{ width: radius * 2 + (isMobile ? 120 : 160), height: radius * 2 + (isMobile ? 120 : 160) }}>
{/* Center Avatar */}
<div
ref={avatarRef}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center"
>
<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 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 */}
<div className="relative z-10 w-full max-w-3xl">
{/* Top row - 3 cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 md:gap-4 mb-3 md:mb-4">
{items.slice(0, 3).map((item, i) => (
{/* Orbiting cards */}
{items.map((item, i) => {
const angle = ((360 / count) * i + angleOffset) * (Math.PI / 180);
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
const cardW = isMobile ? 110 : 130;
const cardH = isMobile ? 80 : 90;
return (
<a
key={item.label}
ref={(el) => { cardRefs.current[i] = el; }}
href={item.href}
target="_blank"
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">
<item.icon className="w-5 h-5 text-primary mb-2" />
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
<item.icon className="w-4 h-4 text-primary mb-1.5" />
<ExternalLink className="w-3 h-3 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>
<p className="font-heading font-semibold text-foreground text-xs md:text-sm leading-tight">{item.label}</p>
<p className="text-[10px] text-muted-foreground mt-0.5 leading-tight">{item.desc}</p>
</GlassCard>
</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>
{/* 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
</p>
</div>