Hub hub with center avatar
Implemented Wii U–style hub layout with center avatar connected to orbiting cards via SVG lines; added dynamic line drawing, mouse interaction glow, and central avatar integration. X-Lovable-Edit-ID: edt-d3d4bd10-ecd6-492a-8214-7108a8a6d5f5
This commit is contained in:
commit
7c3bc30e04
1 changed files with 164 additions and 78 deletions
|
|
@ -1,101 +1,188 @@
|
||||||
import { useRef } from 'react';
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
||||||
import { GlassCard } from '@/components/GlassCard';
|
import { GlassCard } from '@/components/GlassCard';
|
||||||
import { useMousePosition } from '@/hooks/useMousePosition';
|
|
||||||
import avatar from '@/assets/avatar.png';
|
import avatar from '@/assets/avatar.png';
|
||||||
import { Home, MessageCircle, Rss, GitBranch, Search, ExternalLink } from 'lucide-react';
|
import { Home, MessageCircle, Rss, GitBranch, Search, ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
const links = [
|
const items = [
|
||||||
{ icon: Home, label: 'Home', href: 'https://damaj.tech/', desc: 'Main page' },
|
{ icon: Home, label: 'Home', href: 'https://damaj.tech/', desc: 'Main page' },
|
||||||
{ icon: MessageCircle, label: 'Who Am I?', href: 'https://damaj.tech/site/whoami', desc: 'About me' },
|
{ icon: MessageCircle, label: 'Who Am I?', href: 'https://damaj.tech/site/whoami', desc: 'About me' },
|
||||||
{ icon: Rss, label: 'Blog', href: 'https://damaj.tech/site/blog', desc: 'My writings' },
|
{ icon: Rss, label: 'Blog', href: 'https://damaj.tech/site/blog', desc: 'My writings' },
|
||||||
];
|
|
||||||
|
|
||||||
const services = [
|
|
||||||
{ icon: GitBranch, label: 'Git Server', href: 'https://git.damaj.tech/', desc: 'Self-hosted Forgejo' },
|
{ icon: GitBranch, label: 'Git Server', href: 'https://git.damaj.tech/', desc: 'Self-hosted Forgejo' },
|
||||||
{ icon: Search, label: 'SearXNG', href: 'https://searxng.damaj.tech/', desc: 'Private search engine' },
|
{ icon: Search, label: 'SearXNG', href: 'https://searxng.damaj.tech/', desc: 'Private search' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const mouse = useMousePosition(containerRef);
|
const avatarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const cardRefs = useRef<(HTMLAnchorElement | null)[]>([]);
|
||||||
|
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 updateLines = useCallback(() => {
|
||||||
|
if (!containerRef.current || !avatarRef.current) return;
|
||||||
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
|
const avatarRect = avatarRef.current.getBoundingClientRect();
|
||||||
|
const cx = avatarRect.left + avatarRect.width / 2 - containerRect.left;
|
||||||
|
const cy = avatarRect.top + avatarRect.height / 2 - containerRect.top;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
setLines(newLines);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateLines();
|
||||||
|
const timer = setTimeout(() => setMounted(true), 100);
|
||||||
|
window.addEventListener('resize', updateLines);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateLines);
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [updateLines]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Re-calc after fonts/images load
|
||||||
|
const timer = setTimeout(updateLines, 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [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 });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="min-h-screen bg-background relative overflow-hidden"
|
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"
|
||||||
>
|
>
|
||||||
{/* Global mouse glow */}
|
{/* Global mouse glow */}
|
||||||
<div
|
<div
|
||||||
className="fixed pointer-events-none z-0"
|
className="fixed pointer-events-none z-0"
|
||||||
style={{
|
style={{
|
||||||
left: mouse.x,
|
left: mousePos.x,
|
||||||
top: mouse.y,
|
top: mousePos.y,
|
||||||
width: 600,
|
width: 500,
|
||||||
height: 600,
|
height: 500,
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
background: 'radial-gradient(circle, hsl(25 95% 55% / 0.06) 0%, transparent 70%)',
|
background: 'radial-gradient(circle, hsl(25 95% 55% / 0.06) 0%, transparent 70%)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative z-10 max-w-2xl mx-auto px-4 py-12 md:py-20 flex flex-col gap-5">
|
{/* SVG Lines */}
|
||||||
{/* Avatar + Intro */}
|
<svg
|
||||||
<GlassCard className="p-6 md:p-8">
|
className="absolute inset-0 w-full h-full pointer-events-none z-[1]"
|
||||||
<div className="flex items-center gap-5">
|
style={{ opacity: mounted ? 1 : 0, transition: 'opacity 0.8s ease' }}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="lineGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="hsl(25 95% 55% / 0.5)" />
|
||||||
|
<stop offset="100%" stopColor="hsl(25 95% 55% / 0.15)" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</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)"
|
||||||
|
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
|
<img
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt="Mohamad's avatar"
|
alt="Mohamad"
|
||||||
className="w-20 h-20 md:w-24 md:h-24 rounded-full ring-2 ring-primary/30"
|
className="w-28 h-28 md:w-36 md:h-36 rounded-full"
|
||||||
/>
|
/>
|
||||||
<div>
|
</div>
|
||||||
|
<div className="mt-4 text-center">
|
||||||
<h1 className="text-2xl md:text-3xl font-heading font-bold text-foreground">
|
<h1 className="text-2xl md:text-3xl font-heading font-bold text-foreground">
|
||||||
👋 Hey There!
|
👋 Hey There!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1 font-body">
|
<p className="text-muted-foreground text-sm mt-1">
|
||||||
My name is <span className="text-gradient font-semibold">Mohamad</span>
|
I'm <span className="text-gradient font-semibold">Mohamad</span> — welcome to my space.
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Welcome to my space. Feel free to look around.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
|
||||||
|
|
||||||
{/* Website Content */}
|
{/* Cards arranged around */}
|
||||||
<div>
|
<div className="relative z-10 w-full max-w-3xl">
|
||||||
<h2 className="text-xs font-mono uppercase tracking-widest text-muted-foreground mb-3 px-1">
|
{/* Top row - 3 cards */}
|
||||||
Website Content
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 md:gap-4 mb-3 md:mb-4">
|
||||||
</h2>
|
{items.slice(0, 3).map((item, i) => (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
<a
|
||||||
{links.map((link) => (
|
key={item.label}
|
||||||
<a key={link.label} href={link.href} target="_blank" rel="noopener noreferrer">
|
ref={(el) => { cardRefs.current[i] = el; }}
|
||||||
<GlassCard className="p-5 h-full link-arrow-parent cursor-pointer">
|
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">
|
<div className="flex items-start justify-between">
|
||||||
<link.icon className="w-5 h-5 text-primary mb-3" />
|
<item.icon className="w-5 h-5 text-primary mb-2" />
|
||||||
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
|
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
|
||||||
</div>
|
</div>
|
||||||
<p className="font-heading font-semibold text-foreground">{link.label}</p>
|
<p className="font-heading font-semibold text-foreground text-sm md:text-base">{item.label}</p>
|
||||||
<p className="text-xs text-muted-foreground mt-1">{link.desc}</p>
|
<p className="text-xs text-muted-foreground mt-0.5">{item.desc}</p>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Self-hosted Services */}
|
{/* Bottom row - 2 cards centered */}
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-3 md:gap-4 max-w-md mx-auto">
|
||||||
<h2 className="text-xs font-mono uppercase tracking-widest text-muted-foreground mb-3 px-1">
|
{items.slice(3).map((item, i) => (
|
||||||
Self-hosted Services
|
<a
|
||||||
</h2>
|
key={item.label}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
ref={(el) => { cardRefs.current[i + 3] = el; }}
|
||||||
{services.map((svc) => (
|
href={item.href}
|
||||||
<a key={svc.label} href={svc.href} target="_blank" rel="noopener noreferrer">
|
target="_blank"
|
||||||
<GlassCard className="p-5 h-full link-arrow-parent cursor-pointer">
|
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">
|
<div className="flex items-start justify-between">
|
||||||
<svc.icon className="w-5 h-5 text-primary mb-3" />
|
<item.icon className="w-5 h-5 text-primary mb-2" />
|
||||||
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
|
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
|
||||||
</div>
|
</div>
|
||||||
<p className="font-heading font-semibold text-foreground">{svc.label}</p>
|
<p className="font-heading font-semibold text-foreground text-sm md:text-base">{item.label}</p>
|
||||||
<p className="text-xs text-muted-foreground mt-1">{svc.desc}</p>
|
<p className="text-xs text-muted-foreground mt-0.5">{item.desc}</p>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|
@ -103,11 +190,10 @@ const Index = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<p className="text-center text-xs text-muted-foreground font-mono mt-4">
|
<p className="relative z-10 text-center text-xs text-muted-foreground font-mono mt-10">
|
||||||
damaj.tech
|
damaj.tech
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue