feat: add multi-language support and hero section component to portfolio website

This commit is contained in:
Yolando
2026-03-28 20:26:44 +07:00
parent 39f8567519
commit 53da46def1
3 changed files with 167 additions and 79 deletions

View File

@@ -6,6 +6,8 @@
"contact": "Contact" "contact": "Contact"
}, },
"Hero": { "Hero": {
"greeting": "Hi, I'm",
"name": "Yolando.",
"badge": "Available for opportunities", "badge": "Available for opportunities",
"titlePart1": "Building", "titlePart1": "Building",
"titleHighlight": "Secure, Scalable", "titleHighlight": "Secure, Scalable",

View File

@@ -6,6 +6,8 @@
"contact": "Kontak" "contact": "Kontak"
}, },
"Hero": { "Hero": {
"greeting": "Hai, saya",
"name": "Yolando.",
"badge": "Tersedia untuk peluang baru", "badge": "Tersedia untuk peluang baru",
"titlePart1": "Membangun Sistem", "titlePart1": "Membangun Sistem",
"titleHighlight": "Aman & Skalabel", "titleHighlight": "Aman & Skalabel",

View File

@@ -1,106 +1,190 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { useState } from "react";
import { ArrowDown, FileText, Send } from "lucide-react"; import { motion, PanInfo } from "framer-motion";
import { ArrowDown, FileText, Send, Hand } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
const PROFILE_IMAGES = [
"https://images.unsplash.com/photo-1556157382-97eda2d62296?w=600&auto=format&fit=crop&q=80",
"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=600&auto=format&fit=crop&q=80",
"https://images.unsplash.com/photo-1605379399642-870262d3d051?w=600&auto=format&fit=crop&q=80",
];
export function HeroSection() { export function HeroSection() {
const t = useTranslations("Hero"); const t = useTranslations("Hero");
const [cards, setCards] = useState(PROFILE_IMAGES);
const handleDragEnd = (event: any, info: PanInfo) => {
// If the card is dragged beyond a certain threshold on X-axis (left or right)
if (Math.abs(info.offset.x) > 100) {
setCards((prevCards) => {
const newCards = [...prevCards];
const topCard = newCards.shift();
if (topCard) newCards.push(topCard);
return newCards;
});
}
};
return ( return (
<section <section
id="hero" id="hero"
className="relative min-h-screen flex items-center justify-center overflow-hidden" className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20"
> >
<div className="absolute inset-0 grid-pattern opacity-30" /> <div className="absolute inset-0 grid-pattern opacity-30" />
{/* Decorative Blur Backgrounds */}
<div className="absolute top-1/4 -left-32 w-96 h-96 rounded-full bg-accent/20 blur-[120px] animate-pulse-glow" /> <div className="absolute top-1/4 -left-32 w-96 h-96 rounded-full bg-accent/20 blur-[120px] animate-pulse-glow" />
<div className="absolute bottom-1/4 -right-32 w-96 h-96 rounded-full bg-purple-500/15 blur-[120px] animate-pulse-glow" style={{ animationDelay: "2s" }} /> <div
className="absolute bottom-1/4 -right-32 w-96 h-96 rounded-full bg-purple-500/15 blur-[120px] animate-pulse-glow"
style={{ animationDelay: "2s" }}
/>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full bg-indigo-500/5 blur-[100px]" /> <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full bg-indigo-500/5 blur-[100px]" />
<div className="relative z-10 max-w-5xl mx-auto px-6 text-center"> <div className="relative z-10 w-full max-w-6xl mx-auto px-6">
<motion.div <div className="grid lg:grid-cols-2 gap-12 lg:gap-8 items-center">
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<span className="inline-flex items-center gap-2 px-4 py-2 rounded-full text-xs font-mono font-medium bg-success/10 text-success border border-success/20 mb-8">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-success" />
</span>
{t("badge")}
</span>
</motion.div>
<motion.h1 {/* LEFT: Text Content */}
initial={{ opacity: 0, y: 30 }} <div className="text-center lg:text-left flex flex-col items-center lg:items-start order-2 lg:order-1">
animate={{ opacity: 1, y: 0 }} <motion.div
transition={{ duration: 0.7, delay: 0.3 }} initial={{ opacity: 0, y: 20 }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight leading-[1.1] mb-6" animate={{ opacity: 1, y: 0 }}
> transition={{ duration: 0.6, delay: 0.2 }}
{t("titlePart1")}{" "} className="flex items-center gap-3 mb-4"
<span className="gradient-text">{t("titleHighlight")}</span>
<br />
{t("titlePart2")}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.5 }}
className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-10 leading-relaxed"
>
<span className="font-mono text-accent font-semibold">{t("yearsExp")}</span>{" "}
{t("subtitle")}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.7 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4"
>
<a
href="#contact"
className="group inline-flex items-center gap-2 px-7 py-3.5 rounded-xl bg-gradient-to-r from-accent to-purple-500 text-white font-semibold text-sm shadow-lg shadow-accent/25 hover:shadow-accent/40 hover:scale-105 transition-all duration-300"
>
<Send size={16} />
{t("ctaContact")}
</a>
<a
href="#projects"
className="inline-flex items-center gap-2 px-7 py-3.5 rounded-xl font-semibold text-sm border border-border hover:bg-muted/50 transition-all duration-300 hover:scale-105"
>
<FileText size={16} />
{t("ctaProjects")}
</a>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 1 }}
className="mt-16 flex flex-wrap items-center justify-center gap-3"
>
{["Java", "Spring Boot", "Kafka", "PostgreSQL", "Docker", "Kubernetes"].map((tech) => (
<span
key={tech}
className="px-3 py-1.5 rounded-lg text-xs font-mono text-muted-foreground bg-muted/50 border border-border/50 hover:border-accent/30 hover:text-accent transition-all duration-300"
> >
{tech} <span className="inline-flex items-center gap-2 px-4 py-2 rounded-full text-xs font-mono font-medium bg-success/10 text-success border border-success/20">
</span> <span className="relative flex h-2 w-2">
))} <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75" />
</motion.div> <span className="relative inline-flex rounded-full h-2 w-2 bg-success" />
</span>
{t("badge")}
</span>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.3 }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight leading-[1.1] mb-6"
>
<span className="block text-2xl sm:text-3xl lg:text-4xl text-muted-foreground font-medium mb-2">
{t("greeting")}{" "}
<span className="text-foreground gradient-text inline-block">{t("name")}</span>
</span>
{t("titlePart1")}{" "}
<span className="gradient-text whitespace-nowrap">{t("titleHighlight")}</span>
<br />
{t("titlePart2")}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.5 }}
className="text-lg text-muted-foreground mb-10 leading-relaxed max-w-xl"
>
<span className="font-mono text-accent font-semibold">
{t("yearsExp")}
</span>{" "}
{t("subtitle")}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.7 }}
className="flex flex-col sm:flex-row items-center gap-4 w-full sm:w-auto"
>
<a
href="#contact"
className="w-full sm:w-auto group inline-flex items-center justify-center gap-2 px-7 py-3.5 rounded-xl bg-gradient-to-r from-accent to-purple-500 text-white font-semibold text-sm shadow-lg shadow-accent/25 hover:shadow-accent/40 hover:scale-105 transition-all duration-300"
>
<Send size={16} />
{t("ctaContact")}
</a>
<a
href="#projects"
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-7 py-3.5 rounded-xl font-semibold text-sm border border-border hover:bg-muted/50 transition-all duration-300 hover:scale-105"
>
<FileText size={16} />
{t("ctaProjects")}
</a>
</motion.div>
</div>
{/* RIGHT: Swipeable Card Deck */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="order-1 lg:order-2 flex justify-center lg:justify-end relative w-full perspective-1000 h-[380px] sm:h-[450px]"
>
<div className="relative w-full max-w-[280px] sm:max-w-[320px] h-full mx-auto lg:mx-0 lg:mr-8 xl:mr-16">
{cards.map((imgUrl, index) => {
const isTop = index === 0;
return (
<motion.div
key={imgUrl}
className="absolute inset-0 rounded-3xl overflow-hidden shadow-2xl border border-white/10 glass cursor-grab active:cursor-grabbing bg-card"
style={{
transformOrigin: "bottom center",
zIndex: cards.length - index,
}}
initial={false}
animate={{
x: 0,
scale: 1 - index * 0.06,
y: index * 20,
rotateZ: isTop ? 0 : index % 2 === 0 ? 3 : -3,
opacity: 1 - index * 0.2,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 20,
}}
drag={isTop ? "x" : false}
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.8}
onDragEnd={isTop ? handleDragEnd : undefined}
whileHover={isTop ? { scale: 1.02, rotateZ: -1 } : {}}
whileTap={isTop ? { scale: 1.05 } : {}}
>
<img
src={imgUrl}
alt="Yolando Showcase"
className="w-full h-full object-cover pointer-events-none"
/>
{/* Instructional Overlay only on top card */}
{isTop && (
<div className="absolute top-4 right-4 bg-background/60 backdrop-blur-md px-3 py-1.5 rounded-full border border-white/20 shadow-sm flex items-center gap-1.5 pointer-events-none">
<span className="text-[10px] font-bold tracking-widest uppercase truncate animate-pulse">
Swipe
</span>
</div>
)}
</motion.div>
);
})}
</div>
</motion.div>
</div>
</div> </div>
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 1.5 }} transition={{ duration: 1, delay: 1.5 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2" className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20"
> >
<a href="#experience" className="flex flex-col items-center gap-2 text-muted-foreground hover:text-accent transition-colors"> <a
href="#experience"
className="flex flex-col items-center gap-2 text-muted-foreground hover:text-accent transition-colors"
>
<span className="text-xs font-mono">{t("scroll")}</span> <span className="text-xs font-mono">{t("scroll")}</span>
<motion.div <motion.div
animate={{ y: [0, 8, 0] }} animate={{ y: [0, 8, 0] }}