feat: add multi-language support and hero section component to portfolio website
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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] }}
|
||||||
|
|||||||
Reference in New Issue
Block a user