Polish hero, projects, modal, and tech marquee

UI polish and refactor across home components: HeroSection now uses a fixed particle seed and staggered motion variants to avoid hydration/layout shifts, updates background/video/gradient overlays, refines typewriter timing and CTA styles, and improves accessibility/visuals. DemoModal is portalled to document.body, z-index/backdrop/animation timing adjusted, and body-scroll/escape handling preserved. ProjectsSection adds a new "Client Websites" grid with two project cards (davo1.cz, Epinger) including badges, previews and visit links. TechMarquee was refactored to a seamless repeating track (four copies) with a CSS marquee animation, hover-to-pause and item hover effects; also minor theme/color tweaks and general style improvements.
This commit is contained in:
2026-06-07 22:08:59 +02:00
parent ad1f6a90b6
commit 3766b033bc
4 changed files with 358 additions and 208 deletions

View File

@@ -5,190 +5,186 @@ import { FaChevronDown } from "react-icons/fa";
const ROLES = ["Web Developer", "Drone Pilot", "Systems Architect", "Real-Time Engineer"];
const PARTICLES = Array.from({ length: 10 }, (_, i) => ({
id: i,
left: `${10 + Math.random() * 80}%`,
top: `${10 + Math.random() * 75}%`,
size: `${4 + Math.random() * 8}px`,
delay: `${Math.random() * 4}s`,
duration: `${4 + Math.random() * 4}s`,
}));
// Fixed seed so no hydration mismatch & no layout shift on re-render
const PARTICLES = [
{ id: 0, left: "12%", top: "18%", size: 5, delay: 0, dur: 5.2 },
{ id: 1, left: "82%", top: "14%", size: 7, delay: 0.8, dur: 6.1 },
{ id: 2, left: "25%", top: "72%", size: 4, delay: 1.5, dur: 4.8 },
{ id: 3, left: "68%", top: "55%", size: 9, delay: 0.3, dur: 7.0 },
{ id: 4, left: "90%", top: "40%", size: 5, delay: 2.1, dur: 5.5 },
{ id: 5, left: "44%", top: "85%", size: 6, delay: 0.6, dur: 6.3 },
{ id: 6, left: "8%", top: "60%", size: 4, delay: 1.9, dur: 4.5 },
{ id: 7, left: "57%", top: "22%", size: 8, delay: 0.2, dur: 5.8 },
{ id: 8, left: "75%", top: "78%", size: 5, delay: 1.2, dur: 6.6 },
{ id: 9, left: "35%", top: "45%", size: 6, delay: 2.4, dur: 5.0 },
];
const fade = (delay = 0) => ({
initial: { opacity: 0, y: 30 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.7, ease: "easeOut", delay },
});
const stagger = {
container: {
animate: { transition: { staggerChildren: 0.18 } },
},
item: {
initial: { opacity: 0, y: 28 },
animate: { opacity: 1, y: 0, transition: { duration: 0.65, ease: "easeOut" } },
},
};
export default function HeroSection() {
const [roleIdx, setRoleIdx] = useState(0);
const [roleIdx, setRoleIdx] = useState(0);
const [displayed, setDisplayed] = useState("");
const [typing, setTyping] = useState(true);
const [typing, setTyping] = useState(true);
useEffect(() => {
const target = ROLES[roleIdx];
let i = typing ? 0 : target.length;
const speed = typing ? 55 : 30;
const timer = setInterval(() => {
if (typing) {
i++;
setDisplayed(target.slice(0, i));
if (i >= target.length) {
clearInterval(timer);
setTimeout(() => setTyping(false), 1800);
}
if (i >= target.length) { clearInterval(timer); setTimeout(() => setTyping(false), 1800); }
} else {
i--;
setDisplayed(target.slice(0, i));
if (i <= 0) {
clearInterval(timer);
setRoleIdx((prev) => (prev + 1) % ROLES.length);
setTyping(true);
}
if (i <= 0) { clearInterval(timer); setRoleIdx(p => (p + 1) % ROLES.length); setTyping(true); }
}
}, speed);
}, typing ? 55 : 28);
return () => clearInterval(timer);
}, [roleIdx, typing]);
return (
<section
style={{ minHeight: "100svh", position: "relative", display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden" }}
>
{/* Video background */}
<video
autoPlay
muted
loop
playsInline
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", zIndex: 0, opacity: 0.35 }}
<section style={{ minHeight: "100svh", position: "relative", display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden" }}>
{/* Video background — shows when file is present */}
<video autoPlay muted loop playsInline
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", zIndex: 0, opacity: 0.3 }}
src="/assets/hero-drone.mp4"
/>
{/* Gradient overlay */}
{/* Animated mesh gradient fallback / overlay */}
<div style={{
position: "absolute", inset: 0, zIndex: 1,
background: "linear-gradient(to bottom, rgba(3,29,68,0.55) 0%, rgba(3,29,68,0.85) 60%, var(--c-background) 100%)",
background: `
radial-gradient(ellipse 80% 60% at 20% 30%, color-mix(in hsl, var(--c-boxes), transparent 60%) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 70%, color-mix(in hsl, var(--c-other), transparent 75%) 0%, transparent 55%),
radial-gradient(ellipse 100% 80% at 50% 50%, color-mix(in hsl, var(--c-background-light), transparent 20%) 0%, var(--c-background) 80%)
`,
}} />
{/* Subtle grid lines */}
<div style={{
position: "absolute", inset: 0, zIndex: 1, opacity: 0.04,
backgroundImage: "linear-gradient(var(--c-lines) 1px, transparent 1px), linear-gradient(90deg, var(--c-lines) 1px, transparent 1px)",
backgroundSize: "60px 60px",
}} />
{/* Floating particles */}
{PARTICLES.map((p) => (
<div
key={p.id}
className="animate-float"
style={{
position: "absolute",
left: p.left,
top: p.top,
width: p.size,
height: p.size,
borderRadius: "50%",
background: "var(--c-text)",
opacity: 0.18,
filter: "blur(2px)",
zIndex: 1,
animationDelay: p.delay,
animationDuration: p.duration,
}}
/>
{PARTICLES.map(p => (
<div key={p.id} className="animate-float" style={{
position: "absolute", left: p.left, top: p.top,
width: p.size, height: p.size, borderRadius: "50%",
background: "var(--c-lines)", opacity: 0.22, filter: "blur(1.5px)",
zIndex: 1, animationDelay: `${p.delay}s`, animationDuration: `${p.dur}s`,
}} />
))}
{/* Content */}
<div style={{ position: "relative", zIndex: 2, textAlign: "center", padding: "2rem 1.5rem", maxWidth: "820px" }}>
<motion.div
variants={stagger.container}
initial="initial"
animate="animate"
style={{ position: "relative", zIndex: 2, textAlign: "center", padding: "2rem 1.5rem", maxWidth: "860px", width: "100%" }}
>
{/* Badge */}
<motion.div {...fade(0.1)}>
<motion.div variants={stagger.item} style={{ marginBottom: "1.5rem" }}>
<span style={{
display: "inline-block",
padding: "0.35em 1em",
borderRadius: "9999px",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 40%)",
background: "color-mix(in hsl, var(--c-other), transparent 80%)",
color: "var(--c-other)",
fontSize: "0.82rem",
fontWeight: 600,
letterSpacing: "0.06em",
textTransform: "uppercase",
marginBottom: "1.2rem",
display: "inline-flex", alignItems: "center", gap: "0.4em",
padding: "0.4em 1.1em", borderRadius: "9999px",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 45%)",
background: "color-mix(in hsl, var(--c-other), transparent 82%)",
color: "var(--c-other)", fontSize: "0.8rem", fontWeight: 600,
letterSpacing: "0.07em", textTransform: "uppercase",
}}>
Available for projects
<span style={{ fontSize: "0.6em" }}></span> Available for projects
</span>
</motion.div>
{/* Name */}
<motion.h1 {...fade(0.25)} style={{ fontSize: "clamp(2.8rem, 7vw, 5rem)", fontWeight: 900, marginBottom: "0.3em", lineHeight: 1.1 }}>
<span className="text-rainbow animate-gradient" style={{ background: "linear-gradient(90deg, var(--c-other), var(--c-lines), var(--c-text), var(--c-other))" }}>
{/* Name — gradient text via inline WebkitTextFillColor so no class conflict */}
<motion.h1
variants={stagger.item}
style={{ fontSize: "clamp(3rem, 8vw, 5.5rem)", fontWeight: 900, margin: "0 0 0.4em", lineHeight: 1.05, letterSpacing: "-0.02em" }}
>
<span style={{
display: "inline-block",
background: "linear-gradient(135deg, var(--c-other) 0%, var(--c-lines) 45%, var(--c-text) 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}>
Bruno Novotný
</span>
</motion.h1>
{/* Typewriter */}
<motion.div {...fade(0.4)} style={{ fontSize: "clamp(1.2rem, 3vw, 1.9rem)", fontWeight: 500, color: "var(--c-text)", marginBottom: "1.5rem", minHeight: "2.5rem" }}>
<span style={{ color: "color-mix(in hsl, var(--c-lines), transparent 30%)" }}>I build as a </span>
<span style={{ color: "var(--c-text)", fontWeight: 700 }}>{displayed}</span>
<span style={{ color: "var(--c-other)", animation: "bounce-y 0.7s ease-in-out infinite" }}>|</span>
{/* Typewriter role */}
<motion.div
variants={stagger.item}
style={{ fontSize: "clamp(1.1rem, 2.8vw, 1.7rem)", fontWeight: 400, marginBottom: "1.6rem", minHeight: "2.2rem", color: "color-mix(in hsl, var(--c-text), transparent 20%)" }}
>
I am a{" "}
<span style={{ fontWeight: 700, color: "var(--c-text)" }}>{displayed}</span>
<span style={{ color: "var(--c-other)", fontWeight: 300 }}>|</span>
</motion.div>
{/* Sub */}
<motion.p {...fade(0.55)} style={{ color: "color-mix(in hsl, var(--c-text), transparent 30%)", fontSize: "1.05rem", maxWidth: "560px", margin: "0 auto 2.2rem", lineHeight: 1.7 }}>
From stunning drone footage to complex web platforms I craft digital experiences that work at scale.
{/* Subtitle */}
<motion.p
variants={stagger.item}
style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", fontSize: "1rem", maxWidth: "540px", margin: "0 auto 2.4rem", lineHeight: 1.75 }}
>
From cinematic drone footage to complex real-time web platforms I craft digital experiences that work at scale.
</motion.p>
{/* CTAs */}
<motion.div {...fade(0.7)} style={{ display: "flex", gap: "1rem", justifyContent: "center", flexWrap: "wrap" }}>
<motion.div variants={stagger.item} style={{ display: "flex", gap: "1rem", justifyContent: "center", flexWrap: "wrap" }}>
<a
href="#projects"
className="animate-pulse-glow"
style={{
padding: "0.8em 2em",
borderRadius: "9999px",
background: "linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 40%))",
color: "#031D44",
fontWeight: 700,
fontSize: "0.95rem",
textDecoration: "none",
border: "none",
cursor: "pointer",
transition: "transform 0.2s ease",
padding: "0.8em 2.2em", borderRadius: "9999px", cursor: "pointer",
background: "linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 45%))",
color: "#031D44", fontWeight: 700, fontSize: "0.95rem",
textDecoration: "none", border: "none",
boxShadow: "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = "scale(1.05)")}
onMouseLeave={(e) => (e.currentTarget.style.transform = "scale(1)")}
onMouseEnter={e => { e.currentTarget.style.transform = "scale(1.06)"; e.currentTarget.style.boxShadow = "0 0 2rem color-mix(in hsl, var(--c-other), transparent 35%)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)"; }}
>
Explore My Work
</a>
<Link
to="/contact"
style={{
padding: "0.8em 2em",
borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-background-light), transparent 30%)",
backdropFilter: "blur(10px)",
color: "var(--c-text)",
fontWeight: 600,
fontSize: "0.95rem",
padding: "0.8em 2.2em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-background-light), transparent 25%)",
backdropFilter: "blur(12px)",
color: "var(--c-text)", fontWeight: 600, fontSize: "0.95rem",
textDecoration: "none",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 55%)",
transition: "transform 0.2s ease, border-color 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "scale(1.05)";
e.currentTarget.style.borderColor = "var(--c-other)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "scale(1)";
e.currentTarget.style.borderColor = "color-mix(in hsl, var(--c-lines), transparent 55%)";
transition: "transform 0.2s ease, border-color 0.2s ease, background 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "scale(1.06)"; e.currentTarget.style.borderColor = "var(--c-other)"; e.currentTarget.style.background = "color-mix(in hsl, var(--c-background-light), transparent 5%)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.borderColor = "color-mix(in hsl, var(--c-lines), transparent 55%)"; e.currentTarget.style.background = "color-mix(in hsl, var(--c-background-light), transparent 25%)"; }}
>
Let's Talk
</Link>
</motion.div>
</div>
</motion.div>
{/* Scroll indicator */}
<div
className="animate-bounce-y"
style={{ position: "absolute", bottom: "2.5rem", left: "50%", transform: "translateX(-50%)", zIndex: 2, color: "color-mix(in hsl, var(--c-text), transparent 50%)", fontSize: "1.4rem" }}
>
<div className="animate-bounce-y" style={{
position: "absolute", bottom: "2rem", left: "50%", transform: "translateX(-50%)",
zIndex: 2, color: "color-mix(in hsl, var(--c-text), transparent 55%)", fontSize: "1.2rem",
}}>
<FaChevronDown />
</div>
</section>

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
import { FaCopy, FaCheck, FaExternalLinkAlt, FaTimes } from "react-icons/fa";
@@ -53,7 +54,6 @@ function CredRow({ label, value }: { label: string; value: string }) {
}
export default function DemoModal({ opened, onClose }: Props) {
// Close on Escape
useEffect(() => {
if (!opened) return;
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
@@ -61,13 +61,12 @@ export default function DemoModal({ opened, onClose }: Props) {
return () => window.removeEventListener("keydown", handler);
}, [opened, onClose]);
// Prevent body scroll when open
useEffect(() => {
document.body.style.overflow = opened ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [opened]);
return (
return createPortal(
<AnimatePresence>
{opened && (
<>
@@ -80,8 +79,8 @@ export default function DemoModal({ opened, onClose }: Props) {
transition={{ duration: 0.2 }}
onClick={onClose}
style={{
position: "fixed", inset: 0, zIndex: 1000,
background: "rgba(3,29,68,0.75)",
position: "fixed", inset: 0, zIndex: 9000,
background: "rgba(3,29,68,0.78)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
}}
@@ -90,19 +89,20 @@ export default function DemoModal({ opened, onClose }: Props) {
{/* Panel */}
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
initial={{ opacity: 0, scale: 0.9, y: 24 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.92, y: 10 }}
transition={{ duration: 0.25, ease: "easeOut" }}
style={{
position: "fixed", top: "50%", left: "50%",
position: "fixed",
top: "50%", left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1001,
zIndex: 9001,
width: "min(92vw, 420px)",
background: "var(--c-background-light)",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 60%)",
borderRadius: "1.25rem",
boxShadow: "0 24px 64px rgba(0,0,0,0.5)",
boxShadow: "0 24px 64px rgba(0,0,0,0.6)",
overflow: "hidden",
}}
>
@@ -177,6 +177,7 @@ export default function DemoModal({ opened, onClose }: Props) {
</motion.div>
</>
)}
</AnimatePresence>
</AnimatePresence>,
document.body,
);
}

View File

@@ -230,6 +230,154 @@ export default function ProjectsSection() {
</button>
</motion.div>
</motion.div>
{/* Client websites divider */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
style={{ marginTop: "3rem", marginBottom: "1.5rem", display: "flex", alignItems: "center", gap: "1rem" }}
>
<div style={{ flex: 1, height: "1px", background: "linear-gradient(90deg, transparent, color-mix(in hsl, var(--c-lines), transparent 65%))" }} />
<span style={{ fontSize: "0.78rem", fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "color-mix(in hsl, var(--c-text), transparent 50%)", whiteSpace: "nowrap" }}>
Client Websites
</span>
<div style={{ flex: 1, height: "1px", background: "linear-gradient(90deg, color-mix(in hsl, var(--c-lines), transparent 65%), transparent)" }} />
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "1.5rem" }}
>
{/* davo1.cz — advertising firm, white+gold logo → dark bg */}
<motion.div
variants={cardVariants}
whileHover={{ y: -8 }}
transition={{ type: "spring", stiffness: 250, damping: 20 }}
className="glass"
style={{ display: "flex", flexDirection: "column", position: "relative", overflow: "hidden" }}
>
<div style={{ position: "absolute", top: 0, left: 0, right: 0, height: "3px", background: "linear-gradient(90deg, #C9A84C, #8B6914)" }} />
{/* Logo preview — dark so white+gold shows */}
<div style={{
background: "#0f0b05",
display: "flex", alignItems: "center", justifyContent: "center",
padding: "1.5rem 2rem", height: "160px", flexShrink: 0,
}}>
<img
src="/portfolio/davo1.png"
alt="davo1.cz logo"
style={{ maxHeight: "80px", maxWidth: "100%", objectFit: "contain" }}
/>
</div>
<div style={{ padding: "1.5rem 2rem 2rem", display: "flex", flexDirection: "column", gap: "1rem", flex: 1 }}>
<div>
<h3 style={{ margin: "0 0 0.2rem", fontSize: "1.1rem", fontWeight: 800 }}>davo1.cz</h3>
<span style={{ fontSize: "0.8rem", color: "color-mix(in hsl, var(--c-text), transparent 45%)" }}>Advertising agency</span>
</div>
<p style={{ margin: 0, color: "color-mix(in hsl, var(--c-text), transparent 30%)", fontSize: "0.9rem", lineHeight: 1.65 }}>
Presentation website for an advertising firm clean design, custom graphics, and service showcase built to attract business clients.
</p>
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
<TechBadge label="Static HTML/CSS" color="#C9A84C" />
<TechBadge label="Custom Design" color="#C9A84C" />
<TechBadge label="Branding" />
</div>
<a
href="https://davo1.cz"
target="_blank"
rel="noopener noreferrer"
style={{
display: "inline-flex", alignItems: "center", gap: "0.5rem",
padding: "0.65em 1.4em", borderRadius: "9999px",
background: "color-mix(in hsl, #C9A84C, transparent 78%)",
border: "1px solid color-mix(in hsl, #C9A84C, transparent 50%)",
color: "#C9A84C", fontWeight: 600, fontSize: "0.88rem",
textDecoration: "none", marginTop: "auto",
transition: "background 0.2s ease, transform 0.2s ease",
}}
onMouseEnter={(e) => { e.currentTarget.style.background = "color-mix(in hsl, #C9A84C, transparent 55%)"; e.currentTarget.style.transform = "scale(1.03)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "color-mix(in hsl, #C9A84C, transparent 78%)"; e.currentTarget.style.transform = "scale(1)"; }}
>
Visit Site <FaArrowRight style={{ fontSize: "0.75rem" }} />
</a>
</div>
</motion.div>
{/* Epinger — fences & gates, black+orange logo → white bg */}
<motion.div
variants={cardVariants}
whileHover={{ y: -8 }}
transition={{ type: "spring", stiffness: 250, damping: 20 }}
className="glass"
style={{ display: "flex", flexDirection: "column", position: "relative", overflow: "hidden" }}
>
<div style={{ position: "absolute", top: 0, left: 0, right: 0, height: "3px", background: "linear-gradient(90deg, #ff7b00, #ffb347)" }} />
{/* Logo preview — white so black text shows */}
<div style={{
background: "#ffffff",
display: "flex", alignItems: "center", justifyContent: "center",
padding: "0.5rem 1rem", height: "160px", flexShrink: 0,
}}>
<img
src="/portfolio/epinger.png"
alt="Epinger logo"
style={{ width: "90%", maxWidth: "360px", objectFit: "contain" }}
/>
</div>
<div style={{ padding: "1.5rem 2rem 2rem", display: "flex", flexDirection: "column", gap: "1rem", flex: 1 }}>
<div>
<h3 style={{ margin: "0 0 0.2rem", fontSize: "1.1rem", fontWeight: 800 }}>Epinger Vstupní systémy</h3>
<span style={{ fontSize: "0.8rem", color: "color-mix(in hsl, var(--c-text), transparent 45%)" }}>Construction & access systems</span>
</div>
<p style={{ margin: 0, color: "color-mix(in hsl, var(--c-text), transparent 30%)", fontSize: "0.9rem", lineHeight: 1.65 }}>
Business website for a company specializing in electronic entry systems, automated gates, and classic fencing product catalog with inquiry forms.
</p>
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
<TechBadge label="Custom Design" color="#ff7b00" />
<TechBadge label="Product Catalog" color="#ff7b00" />
<TechBadge label="Contact Forms" />
</div>
<div style={{ marginTop: "auto", display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<a
href="http://epinger.cz"
target="_blank"
rel="noopener noreferrer"
style={{
display: "inline-flex", alignItems: "center", gap: "0.5rem",
padding: "0.65em 1.4em", borderRadius: "9999px",
background: "color-mix(in hsl, #ff7b00, transparent 82%)",
border: "1px solid color-mix(in hsl, #ff7b00, transparent 55%)",
color: "#ff9a40", fontWeight: 600, fontSize: "0.88rem",
textDecoration: "none",
transition: "background 0.2s ease, transform 0.2s ease",
}}
onMouseEnter={(e) => { e.currentTarget.style.background = "color-mix(in hsl, #ff7b00, transparent 60%)"; e.currentTarget.style.transform = "scale(1.03)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "color-mix(in hsl, #ff7b00, transparent 82%)"; e.currentTarget.style.transform = "scale(1)"; }}
>
Visit Site <FaArrowRight style={{ fontSize: "0.75rem" }} />
</a>
<span style={{ fontSize: "0.72rem", color: "color-mix(in hsl, var(--c-text), transparent 55%)", display: "flex", alignItems: "center", gap: "0.3rem" }}>
<span style={{ color: "#ff7b00" }}></span> HTTP only could be migrated to HTTPS on my hosting in a day
</span>
</div>
</div>
</motion.div>
</motion.div>
</div>
<DemoModal opened={demoOpen} onClose={() => setDemoOpen(false)} />

View File

@@ -6,68 +6,27 @@ import {
import { FaBrain } from "react-icons/fa";
const TECHS = [
{ icon: <SiDocker />, label: "Docker", color: "#2496ed" },
{ icon: <SiNginx />, label: "Nginx", color: "#009900" },
{ icon: <SiPython />, label: "Python", color: "#3776ab" },
{ icon: <SiDjango />, label: "Django", color: "#09d3ac" },
{ icon: <SiReact />, label: "React", color: "#61dafb" },
{ icon: <SiDebian />, label: "Debian", color: "#a80030" },
{ icon: <SiDocker />, label: "Docker", color: "#2496ed" },
{ icon: <SiNginx />, label: "Nginx", color: "#009900" },
{ icon: <SiPython />, label: "Python", color: "#3776ab" },
{ icon: <SiDjango />, label: "Django", color: "#09d3ac" },
{ icon: <SiReact />, label: "React", color: "#61dafb" },
{ icon: <SiDebian />, label: "Debian", color: "#d70a53" },
{ icon: <SiPostgresql />, label: "PostgreSQL", color: "#336791" },
{ icon: <SiRedis />, label: "Redis", color: "#ff4438" },
{ icon: <SiCelery />, label: "Celery", color: "#37b24d" },
{ icon: null, label: "Gorse.io", color: "var(--c-other)" },
{ icon: <FaBrain />, label: "Ollama", color: "var(--c-lines)", experimental: true },
{ icon: <SiRedis />, label: "Redis", color: "#ff4438" },
{ icon: <SiCelery />, label: "Celery", color: "#37b24d" },
{ icon: null, label: "Gorse.io", color: "var(--c-other)" },
{ icon: <FaBrain />, label: "Ollama", color: "var(--c-lines)", experimental: true },
];
function TechItem({ icon, label, color, experimental }: { icon: React.ReactNode; label: string; color: string; experimental?: boolean }) {
return (
<motion.div
whileHover={{ scale: 1.2, y: -4 }}
transition={{ type: "spring", stiffness: 300, damping: 18 }}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.5rem",
padding: "0.75rem 1.25rem",
borderRadius: "1rem",
cursor: "default",
position: "relative",
}}
>
{experimental && (
<span style={{
position: "absolute",
top: 0,
right: 0,
fontSize: "0.6rem",
fontWeight: 700,
padding: "0.15em 0.4em",
borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-lines), transparent 70%)",
color: "var(--c-lines)",
letterSpacing: "0.04em",
lineHeight: 1.4,
}}>
exp
</span>
)}
<div style={{ fontSize: "2.5rem", color, filter: `drop-shadow(0 0 0.4rem color-mix(in hsl, ${color}, transparent 65%))` }}>
{icon ?? <span style={{ fontSize: "1rem", fontWeight: 800, color }}>{label.split(".")[0]}</span>}
</div>
<span style={{ fontSize: "0.78rem", fontWeight: 600, color: "color-mix(in hsl, var(--c-text), transparent 35%)", whiteSpace: "nowrap" }}>
{label}
</span>
</motion.div>
);
}
// Four copies so the loop is seamless at any viewport width
const TRACK = [...TECHS, ...TECHS, ...TECHS, ...TECHS];
export default function TechMarquee() {
const doubled = [...TECHS, ...TECHS];
return (
<section className="section" style={{ background: "var(--c-background)", overflow: "hidden" }}>
<div className="container" style={{ marginBottom: "2rem" }}>
<section style={{ background: "var(--c-background)", padding: "4rem 0", overflow: "hidden" }}>
{/* Header */}
<div className="container" style={{ marginBottom: "2.5rem" }}>
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -96,41 +55,87 @@ export default function TechMarquee() {
</motion.div>
</div>
{/* Fade edges */}
{/* Marquee */}
<div style={{ position: "relative" }}>
<div style={{
position: "absolute", left: 0, top: 0, bottom: 0, width: "8rem", zIndex: 2,
background: "linear-gradient(to right, var(--c-background), transparent)",
pointerEvents: "none",
}} />
<div style={{
position: "absolute", right: 0, top: 0, bottom: 0, width: "8rem", zIndex: 2,
background: "linear-gradient(to left, var(--c-background), transparent)",
pointerEvents: "none",
}} />
{/* Fade edges */}
<div style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: "10rem", zIndex: 2, background: "linear-gradient(to right, var(--c-background), transparent)", pointerEvents: "none" }} />
<div style={{ position: "absolute", right: 0, top: 0, bottom: 0, width: "10rem", zIndex: 2, background: "linear-gradient(to left, var(--c-background), transparent)", pointerEvents: "none" }} />
{/* Marquee track */}
{/* Track wrapper — pause on hover */}
<div
style={{ overflow: "hidden" }}
onMouseEnter={(e) => {
const el = e.currentTarget.firstElementChild as HTMLElement;
if (el) el.style.animationPlayState = "paused";
}}
onMouseLeave={(e) => {
const el = e.currentTarget.firstElementChild as HTMLElement;
if (el) el.style.animationPlayState = "running";
}}
className="marquee-track-wrapper"
style={{ overflow: "hidden", width: "100%" }}
>
{/* Scrolling track — CSS animation, no Framer Motion here */}
<div
className="animate-marquee"
style={{ display: "flex", width: "max-content" }}
className="marquee-track"
style={{
display: "flex",
width: "max-content",
animation: "marquee 35s linear infinite",
}}
>
{doubled.map((tech, i) => (
<TechItem key={`${tech.label}-${i}`} {...tech} />
{TRACK.map((tech, i) => (
<div
key={i}
className="tech-item"
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.6rem",
padding: "0.75rem 1.5rem",
position: "relative",
cursor: "default",
transition: "transform 0.2s ease",
}}
>
{tech.experimental && (
<span style={{
position: "absolute", top: 0, right: "0.5rem",
fontSize: "0.55rem", fontWeight: 700,
padding: "0.12em 0.4em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-lines), transparent 70%)",
color: "var(--c-lines)", letterSpacing: "0.05em",
}}>
exp
</span>
)}
<div style={{
fontSize: "2.8rem",
color: tech.color,
filter: `drop-shadow(0 0 0.5rem color-mix(in hsl, ${tech.color}, transparent 60%))`,
lineHeight: 1,
transition: "filter 0.2s ease, transform 0.2s ease",
}}>
{tech.icon ?? (
<span style={{ fontSize: "1.1rem", fontWeight: 900, color: tech.color, letterSpacing: "-0.02em" }}>
{tech.label.split(".")[0]}
</span>
)}
</div>
<span style={{
fontSize: "0.78rem", fontWeight: 600,
color: "color-mix(in hsl, var(--c-text), transparent 35%)",
whiteSpace: "nowrap",
}}>
{tech.label}
</span>
</div>
))}
</div>
</div>
</div>
{/* Inline styles for hover pause + item hover */}
<style>{`
.marquee-track-wrapper:hover .marquee-track {
animation-play-state: paused;
}
.tech-item:hover {
transform: translateY(-6px) scale(1.15);
}
`}</style>
</section>
);
}