feat: implement internationalization with next-intl and add core portfolio sections
This commit is contained in:
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
15
src/i18n/request.ts
Normal 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
10
src/i18n/routing.ts
Normal 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
9
src/middleware.ts
Normal 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*']
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user