feat: implement internationalization with next-intl and add core portfolio sections

This commit is contained in:
Yolando
2026-03-28 19:59:39 +07:00
parent 5b0254d71b
commit 39f8567519
17 changed files with 1304 additions and 253 deletions

View File

@@ -1,7 +1,11 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "@/shared/components/theme-provider";
import "./globals.css";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
import "../globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -35,19 +39,34 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
// Ensure that the incoming `locale` is valid
if (!routing.locales.includes(locale as any)) {
notFound();
}
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<html
lang="en"
lang={locale}
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">
<ThemeProvider>{children}</ThemeProvider>
<NextIntlClientProvider messages={messages}>
<ThemeProvider>{children}</ThemeProvider>
</NextIntlClientProvider>
</body>
</html>
);

View File

@@ -3,67 +3,58 @@
import { AnimatedSection } from "@/shared/components/animated-section";
import { SectionHeading } from "@/shared/components/section-heading";
import { Briefcase, Award, Rocket, Code2, Building2 } from "lucide-react";
interface TimelineItem {
year: string;
title: string;
company: string;
description: string;
achievements: string[];
icon: React.ReactNode;
}
const timelineData: TimelineItem[] = [
{
year: "2024 — Present",
title: "Senior Backend Developer",
company: "Enterprise Banking Corp",
description:
"Leading microservices architecture design and implementation for core banking platform.",
achievements: [
"Architected event-driven system processing 500K+ transactions/day",
"Reduced API response time by 40% through caching optimization",
"Mentored team of 4 junior developers",
],
icon: <Rocket size={20} />,
},
{
year: "2023 — 2024",
title: "Backend Developer",
company: "Digital Banking Solutions",
description:
"Developed and maintained Spring Boot microservices for payment processing and customer management.",
achievements: [
"Built real-time notification service with Apache Kafka",
"Implemented OAuth2 + JWT authentication system",
"Achieved 99.9% uptime on production services",
],
icon: <Code2 size={20} />,
},
{
year: "2021 — 2023",
title: "Junior Backend Developer",
company: "FinTech Startup",
description:
"Started career building REST APIs and database management for financial applications.",
achievements: [
"Designed PostgreSQL schema handling 1M+ records",
"Created automated CI/CD pipeline with Jenkins",
"Developed unit & integration test coverage to 85%",
],
icon: <Building2 size={20} />,
},
];
import { useTranslations } from "next-intl";
export function ExperienceSection() {
const t = useTranslations("Experience");
const timelineData = [
{
year: t("jobs.enterprise.year"),
title: t("jobs.enterprise.title"),
company: t("jobs.enterprise.company"),
description: t("jobs.enterprise.description"),
achievements: [
t("jobs.enterprise.achievements.0"),
t("jobs.enterprise.achievements.1"),
t("jobs.enterprise.achievements.2"),
],
icon: <Rocket size={20} />,
},
{
year: t("jobs.digital.year"),
title: t("jobs.digital.title"),
company: t("jobs.digital.company"),
description: t("jobs.digital.description"),
achievements: [
t("jobs.digital.achievements.0"),
t("jobs.digital.achievements.1"),
t("jobs.digital.achievements.2"),
],
icon: <Code2 size={20} />,
},
{
year: t("jobs.fintech.year"),
title: t("jobs.fintech.title"),
company: t("jobs.fintech.company"),
description: t("jobs.fintech.description"),
achievements: [
t("jobs.fintech.achievements.0"),
t("jobs.fintech.achievements.1"),
t("jobs.fintech.achievements.2"),
],
icon: <Building2 size={20} />,
},
];
return (
<section id="experience" className="section-padding relative">
<div className="max-w-5xl mx-auto px-6">
<AnimatedSection>
<SectionHeading
badge="Career Journey"
title="Experience & Evolution"
subtitle="A timeline of building enterprise-grade systems in the banking technology industry."
badge={t("badge")}
title={t("title")}
subtitle={t("subtitle")}
/>
</AnimatedSection>

View File

@@ -2,23 +2,23 @@
import { motion } from "framer-motion";
import { ArrowDown, FileText, Send } from "lucide-react";
import { useTranslations } from "next-intl";
export function HeroSection() {
const t = useTranslations("Hero");
return (
<section
id="hero"
className="relative min-h-screen flex items-center justify-center overflow-hidden"
>
{/* Background effects */}
<div className="absolute inset-0 grid-pattern opacity-30" />
{/* Gradient orbs */}
<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 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">
{/* Status badge */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@@ -29,37 +29,32 @@ export function HeroSection() {
<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>
Available for opportunities
{t("badge")}
</span>
</motion.div>
{/* Main heading */}
<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"
>
Building{" "}
<span className="gradient-text">Secure, Scalable</span>
{t("titlePart1")}{" "}
<span className="gradient-text">{t("titleHighlight")}</span>
<br />
Enterprise-Grade Systems
{t("titlePart2")}
</motion.h1>
{/* Subtitle */}
<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">3+ Years</span>{" "}
in Banking Technology.{" "}
<span className="text-foreground">Backend Developer</span> specializing in
Java Spring Boot, Microservices Architecture, and Enterprise Security.
<span className="font-mono text-accent font-semibold">{t("yearsExp")}</span>{" "}
{t("subtitle")}
</motion.p>
{/* CTA buttons */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
@@ -71,18 +66,17 @@ export function HeroSection() {
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} />
Get in Touch
{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} />
View Projects
{t("ctaProjects")}
</a>
</motion.div>
{/* Tech badges */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -100,7 +94,6 @@ export function HeroSection() {
</motion.div>
</div>
{/* Scroll indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -108,7 +101,7 @@ export function HeroSection() {
className="absolute bottom-8 left-1/2 -translate-x-1/2"
>
<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">Scroll</span>
<span className="text-xs font-mono">{t("scroll")}</span>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}

View File

@@ -4,8 +4,10 @@ import { useState } from "react";
import { AnimatedSection } from "@/shared/components/animated-section";
import { SectionHeading } from "@/shared/components/section-heading";
import { Send, Mail, User, MessageSquare, CheckCircle, Loader2 } from "lucide-react";
import { useTranslations } from "next-intl";
export function ContactSection() {
const t = useTranslations("Contact");
const [formState, setFormState] = useState<"idle" | "loading" | "success">("idle");
const [formData, setFormData] = useState({
name: "",
@@ -34,9 +36,9 @@ export function ContactSection() {
<div className="relative max-w-4xl mx-auto px-6">
<AnimatedSection>
<SectionHeading
badge="Let's Connect"
title="Get in Touch"
subtitle="Interested in working together? Whether you're a recruiter, hiring manager, or potential collaborator — I'd love to hear from you."
badge={t("badge")}
title={t("title")}
subtitle={t("subtitle")}
/>
</AnimatedSection>
@@ -49,9 +51,9 @@ export function ContactSection() {
<div className="w-16 h-16 rounded-full bg-success/10 flex items-center justify-center mx-auto mb-4">
<CheckCircle size={32} className="text-success" />
</div>
<h3 className="text-xl font-bold mb-2">Message Sent!</h3>
<h3 className="text-xl font-bold mb-2">{t("form.successTitle")}</h3>
<p className="text-muted-foreground text-sm">
Thank you for reaching out. I&apos;ll get back to you soon.
{t("form.successDesc")}
</p>
</div>
</div>
@@ -66,7 +68,7 @@ export function ContactSection() {
className="flex items-center gap-2 text-sm font-medium"
>
<User size={14} className="text-accent" />
Full Name
{t("form.nameLabel")}
</label>
<input
id="contact-name"
@@ -76,7 +78,7 @@ export function ContactSection() {
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
placeholder="John Doe"
placeholder={t("form.namePlaceholder")}
className="w-full px-4 py-3 rounded-xl bg-muted/50 border border-border/50 text-sm placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent/50 transition-all"
/>
</div>
@@ -88,7 +90,7 @@ export function ContactSection() {
className="flex items-center gap-2 text-sm font-medium"
>
<Mail size={14} className="text-accent" />
Email Address
{t("form.emailLabel")}
</label>
<input
id="contact-email"
@@ -98,7 +100,7 @@ export function ContactSection() {
onChange={(e) =>
setFormData((prev) => ({ ...prev, email: e.target.value }))
}
placeholder="john@company.com"
placeholder={t("form.emailPlaceholder")}
className="w-full px-4 py-3 rounded-xl bg-muted/50 border border-border/50 text-sm placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent/50 transition-all"
/>
</div>
@@ -111,7 +113,7 @@ export function ContactSection() {
className="flex items-center gap-2 text-sm font-medium"
>
<MessageSquare size={14} className="text-accent" />
Message
{t("form.messageLabel")}
</label>
<textarea
id="contact-message"
@@ -121,7 +123,7 @@ export function ContactSection() {
onChange={(e) =>
setFormData((prev) => ({ ...prev, message: e.target.value }))
}
placeholder="Tell me about the opportunity or project you have in mind..."
placeholder={t("form.messagePlaceholder")}
className="w-full px-4 py-3 rounded-xl bg-muted/50 border border-border/50 text-sm placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent/50 transition-all resize-none"
/>
</div>
@@ -135,12 +137,12 @@ export function ContactSection() {
{formState === "loading" ? (
<>
<Loader2 size={16} className="animate-spin" />
Sending...
{t("form.submitting")}
</>
) : (
<>
<Send size={16} />
Send Message
{t("form.submit")}
</>
)}
</button>

View File

@@ -13,85 +13,70 @@ import {
Globe,
Layers,
} from "lucide-react";
interface Project {
id: string;
title: string;
description: string;
category: "backend" | "frontend" | "mobile";
tags: string[];
metrics?: string;
repoUrl?: string;
liveUrl?: string;
}
const projects: Project[] = [
{
id: "1",
title: "Core Banking API Gateway",
description:
"High-performance API Gateway handling 500K+ daily transactions with rate limiting, circuit breaker pattern, and distributed tracing across 12 microservices.",
category: "backend",
tags: ["Spring Boot", "Kafka", "Redis", "Docker"],
metrics: "Increased throughput by 40%, 99.9% uptime",
},
{
id: "2",
title: "Payment Processing Engine",
description:
"Event-driven payment system with Saga pattern for distributed transactions, supporting real-time transfers, bill payments, and batch processing.",
category: "backend",
tags: ["Java", "Kafka", "PostgreSQL", "gRPC"],
metrics: "Processing 200K+ payments/day",
},
{
id: "3",
title: "Customer Onboarding Portal",
description:
"Modern onboarding portal with multi-step KYC verification, document upload, and real-time status tracking for banking customers.",
category: "frontend",
tags: ["React", "Next.js", "TypeScript", "Tailwind"],
repoUrl: "#",
liveUrl: "#",
},
{
id: "4",
title: "Internal Dashboard",
description:
"Admin dashboard for monitoring API performance, user analytics, and system health with real-time WebSocket updates.",
category: "frontend",
tags: ["React", "Chart.js", "WebSocket", "REST"],
repoUrl: "#",
},
{
id: "5",
title: "Mobile Banking App",
description:
"Cross-platform mobile banking application with biometric authentication, push notifications, and offline transaction history.",
category: "mobile",
tags: ["React Native", "TypeScript", "Redux"],
},
{
id: "6",
title: "Authentication Microservice",
description:
"Centralized auth service with OAuth2, JWT, MFA, and session management. Supports SSO across 8 internal applications.",
category: "backend",
tags: ["Spring Security", "OAuth2", "JWT", "Redis"],
metrics: "Securing 50K+ active users",
},
];
const filters = [
{ value: "all", label: "All Projects", icon: <Layers size={16} /> },
{ value: "backend", label: "Backend", icon: <Server size={16} /> },
{ value: "frontend", label: "Frontend", icon: <Globe size={16} /> },
{ value: "mobile", label: "Mobile", icon: <Smartphone size={16} /> },
];
import { useTranslations } from "next-intl";
export function ProjectsSection() {
const t = useTranslations("Projects");
const [activeFilter, setActiveFilter] = useState("all");
const projects = [
{
id: "1",
title: t("items.apiGateway.title"),
description: t("items.apiGateway.description"),
category: "backend",
tags: ["Spring Boot", "Kafka", "Redis", "Docker"],
metrics: t.has("items.apiGateway.metrics") ? t("items.apiGateway.metrics") : undefined,
},
{
id: "2",
title: t("items.paymentEngine.title"),
description: t("items.paymentEngine.description"),
category: "backend",
tags: ["Java", "Kafka", "PostgreSQL", "gRPC"],
metrics: t.has("items.paymentEngine.metrics") ? t("items.paymentEngine.metrics") : undefined,
},
{
id: "3",
title: t("items.onboarding.title"),
description: t("items.onboarding.description"),
category: "frontend",
tags: ["React", "Next.js", "TypeScript", "Tailwind"],
repoUrl: "#",
liveUrl: "#",
},
{
id: "4",
title: t("items.dashboard.title"),
description: t("items.dashboard.description"),
category: "frontend",
tags: ["React", "Chart.js", "WebSocket", "REST"],
repoUrl: "#",
},
{
id: "5",
title: t("items.mobileApp.title"),
description: t("items.mobileApp.description"),
category: "mobile",
tags: ["React Native", "TypeScript", "Redux"],
},
{
id: "6",
title: t("items.authService.title"),
description: t("items.authService.description"),
category: "backend",
tags: ["Spring Security", "OAuth2", "JWT", "Redis"],
metrics: t.has("items.authService.metrics") ? t("items.authService.metrics") : undefined,
},
];
const filters = [
{ value: "all", label: t("filters.all"), icon: <Layers size={16} /> },
{ value: "backend", label: t("filters.backend"), icon: <Server size={16} /> },
{ value: "frontend", label: t("filters.frontend"), icon: <Globe size={16} /> },
{ value: "mobile", label: t("filters.mobile"), icon: <Smartphone size={16} /> },
];
const filteredProjects =
activeFilter === "all"
? projects
@@ -102,9 +87,9 @@ export function ProjectsSection() {
<div className="max-w-6xl mx-auto px-6">
<AnimatedSection>
<SectionHeading
badge="Portfolio"
title="Projects & Case Studies"
subtitle="Real-world enterprise solutions built for scale, security, and reliability."
badge={t("badge")}
title={t("title")}
subtitle={t("subtitle")}
/>
</AnimatedSection>

View File

@@ -16,83 +16,73 @@ import {
Cloud,
Workflow,
} from "lucide-react";
interface TechItem {
name: string;
icon: React.ReactNode;
}
interface TechCategory {
title: string;
description: string;
items: TechItem[];
accent: string;
}
const techCategories: TechCategory[] = [
{
title: "Enterprise Backend",
description: "Core systems & microservices",
accent: "from-blue-500 to-cyan-500",
items: [
{ name: "Java", icon: <Cpu size={22} /> },
{ name: "Spring Boot", icon: <Layers size={22} /> },
{ name: "Apache Kafka", icon: <Workflow size={22} /> },
{ name: "REST API", icon: <Server size={22} /> },
{ name: "gRPC", icon: <GitBranch size={22} /> },
{ name: "Spring Security", icon: <Shield size={22} /> },
],
},
{
title: "Database & Infrastructure",
description: "Data management & DevOps",
accent: "from-emerald-500 to-teal-500",
items: [
{ name: "PostgreSQL", icon: <Database size={22} /> },
{ name: "Redis", icon: <Database size={22} /> },
{ name: "Docker", icon: <Container size={22} /> },
{ name: "Kubernetes", icon: <Cloud size={22} /> },
{ name: "Jenkins CI/CD", icon: <GitBranch size={22} /> },
{ name: "Nginx", icon: <Server size={22} /> },
],
},
{
title: "Frontend Development",
description: "Modern web interfaces",
accent: "from-violet-500 to-purple-500",
items: [
{ name: "React / Next.js", icon: <Globe size={22} /> },
{ name: "TypeScript", icon: <Cpu size={22} /> },
{ name: "Tailwind CSS", icon: <Layers size={22} /> },
{ name: "Framer Motion", icon: <Workflow size={22} /> },
],
},
{
title: "Mobile Development",
description: "Cross-platform apps",
accent: "from-orange-500 to-rose-500",
items: [
{ name: "React Native", icon: <Smartphone size={22} /> },
{ name: "Flutter", icon: <MonitorSmartphone size={22} /> },
],
},
];
import { useTranslations } from "next-intl";
export function TechStackSection() {
const t = useTranslations("TechStack");
const techCategories = [
{
title: t("categories.backend.title"),
description: t("categories.backend.description"),
accent: "from-blue-500 to-cyan-500",
items: [
{ name: "Java", icon: <Cpu size={22} /> },
{ name: "Spring Boot", icon: <Layers size={22} /> },
{ name: "Apache Kafka", icon: <Workflow size={22} /> },
{ name: "REST API", icon: <Server size={22} /> },
{ name: "gRPC", icon: <GitBranch size={22} /> },
{ name: "Spring Security", icon: <Shield size={22} /> },
],
},
{
title: t("categories.infra.title"),
description: t("categories.infra.description"),
accent: "from-emerald-500 to-teal-500",
items: [
{ name: "PostgreSQL", icon: <Database size={22} /> },
{ name: "Redis", icon: <Database size={22} /> },
{ name: "Docker", icon: <Container size={22} /> },
{ name: "Kubernetes", icon: <Cloud size={22} /> },
{ name: "Jenkins CI/CD", icon: <GitBranch size={22} /> },
{ name: "Nginx", icon: <Server size={22} /> },
],
},
{
title: t("categories.frontend.title"),
description: t("categories.frontend.description"),
accent: "from-violet-500 to-purple-500",
items: [
{ name: "React / Next.js", icon: <Globe size={22} /> },
{ name: "TypeScript", icon: <Cpu size={22} /> },
{ name: "Tailwind CSS", icon: <Layers size={22} /> },
{ name: "Framer Motion", icon: <Workflow size={22} /> },
],
},
{
title: t("categories.mobile.title"),
description: t("categories.mobile.description"),
accent: "from-orange-500 to-rose-500",
items: [
{ name: "React Native", icon: <Smartphone size={22} /> },
{ name: "Flutter", icon: <MonitorSmartphone size={22} /> },
],
},
];
return (
<section
id="tech-stack"
className="section-padding relative bg-muted/30"
>
{/* Subtle background */}
<div className="absolute inset-0 grid-pattern opacity-20" />
<div className="relative max-w-6xl mx-auto px-6">
<AnimatedSection>
<SectionHeading
badge="Tech Arsenal"
title="Technology Stack"
subtitle="A comprehensive toolkit forged through years of enterprise development, from backend infrastructure to mobile interfaces."
badge={t("badge")}
title={t("title")}
subtitle={t("subtitle")}
/>
</AnimatedSection>
@@ -100,7 +90,6 @@ export function TechStackSection() {
{techCategories.map((category, catIndex) => (
<AnimatedSection key={category.title} delay={catIndex * 0.1}>
<div className="group relative h-full p-6 rounded-2xl bg-card border border-border/50 hover:border-accent/30 transition-all duration-500 hover:shadow-lg">
{/* Category header */}
<div className="flex items-center gap-3 mb-4">
<div
className={`w-10 h-10 rounded-xl bg-gradient-to-br ${category.accent} flex items-center justify-center text-white shadow-lg`}
@@ -115,7 +104,6 @@ export function TechStackSection() {
</div>
</div>
{/* Tech items grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{category.items.map((tech) => (
<div

15
src/i18n/request.ts Normal file
View File

@@ -0,0 +1,15 @@
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});

10
src/i18n/routing.ts Normal file
View File

@@ -0,0 +1,10 @@
import {defineRouting} from 'next-intl/routing';
import {createNavigation} from 'next-intl/navigation';
export const routing = defineRouting({
locales: ['id', 'en'],
defaultLocale: 'id',
localePrefix: 'as-needed' // Don't show /id for the default locale
});
export const {Link, redirect, usePathname, useRouter, getPathname} = createNavigation(routing);

9
src/middleware.ts Normal file
View File

@@ -0,0 +1,9 @@
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(id|en)/:path*']
};

View File

@@ -1,6 +1,9 @@
import { GitFork, Link2, Mail, ArrowUp } from "lucide-react";
import { useTranslations } from "next-intl";
export function Footer() {
const t = useTranslations("Footer");
return (
<footer className="relative border-t border-border/50 bg-card/50">
<div className="max-w-6xl mx-auto px-6 py-12">
@@ -49,7 +52,7 @@ export function Footer() {
href="#"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-accent transition-colors group"
>
Back to top
{t("backToTop")}
<ArrowUp
size={14}
className="transition-transform group-hover:-translate-y-0.5"
@@ -59,8 +62,7 @@ export function Footer() {
<div className="mt-8 pt-6 border-t border-border/30 text-center">
<p className="text-sm text-muted-foreground font-mono">
© {new Date().getFullYear()} Yolando. Built with Next.js & crafted
with purpose.
© {new Date().getFullYear()} {t("copyright")}
</p>
</div>
</div>

View File

@@ -1,20 +1,20 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useTransition } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
import { Menu, X, Languages } from "lucide-react";
import { ThemeToggle } from "@/shared/components/theme-toggle";
const navLinks = [
{ href: "#experience", label: "Experience" },
{ href: "#tech-stack", label: "Tech Stack" },
{ href: "#projects", label: "Projects" },
{ href: "#contact", label: "Contact" },
];
import { useTranslations, useLocale } from "next-intl";
import { usePathname, useRouter } from "@/i18n/routing";
export function Navbar() {
const [isScrolled, setIsScrolled] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations("Navigation");
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
useEffect(() => {
const handleScroll = () => setIsScrolled(window.scrollY > 20);
@@ -22,6 +22,20 @@ export function Navbar() {
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navLinks = [
{ href: "#experience", label: t("experience") },
{ href: "#tech-stack", label: t("techStack") },
{ href: "#projects", label: t("projects") },
{ href: "#contact", label: t("contact") },
];
const switchLocale = (newLocale: string) => {
startTransition(() => {
router.replace(pathname, { locale: newLocale });
setIsOpen(false);
});
};
return (
<motion.header
initial={{ y: -100 }}
@@ -56,7 +70,33 @@ export function Navbar() {
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-0 h-0.5 bg-accent rounded-full transition-all duration-300 group-hover:w-6" />
</a>
))}
<div className="ml-4 pl-4 border-l border-border/50">
<div className="ml-4 pl-4 border-l border-border/50 flex items-center gap-2">
{/* Language Switcher */}
<div className="flex items-center gap-1 bg-muted/50 p-1 rounded-xl border border-border/50">
<button
disabled={isPending}
onClick={() => switchLocale("id")}
className={`px-2 py-1 text-xs font-bold rounded-lg transition-colors ${
locale === "id"
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
ID
</button>
<button
disabled={isPending}
onClick={() => switchLocale("en")}
className={`px-2 py-1 text-xs font-bold rounded-lg transition-colors ${
locale === "en"
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
EN
</button>
</div>
<ThemeToggle />
</div>
</div>
@@ -94,6 +134,33 @@ export function Navbar() {
{link.label}
</a>
))}
<div className="px-4 py-3 mt-2 border-t border-border/30 flex items-center gap-4">
<span className="text-sm font-medium flex items-center gap-2 text-muted-foreground">
<Languages size={16} /> Language
</span>
<div className="flex items-center gap-2">
<button
onClick={() => switchLocale("id")}
className={`px-3 py-1 text-xs font-bold rounded-lg border ${
locale === "id"
? "bg-accent/10 border-accent/20 text-accent"
: "bg-muted/50 border-border/50 text-muted-foreground"
}`}
>
ID
</button>
<button
onClick={() => switchLocale("en")}
className={`px-3 py-1 text-xs font-bold rounded-lg border ${
locale === "en"
? "bg-accent/10 border-accent/20 text-accent"
: "bg-muted/50 border-border/50 text-muted-foreground"
}`}
>
EN
</button>
</div>
</div>
</div>
</motion.div>
)}