feat: portfolio bilíngue (PT/EN) — Next.js 16 + Tailwind v4

- Hero com terminal animado e identidade de engenheiro de agentes de IA
- 10 projetos curados (destaques: Mika e Agentes de IA com métricas)
- Skills, contato, dark mode, SEO/OG por locale
- Rotas /pt e /en com redirect por Accept-Language
This commit is contained in:
Felipe Domingues 2026-07-21 01:47:54 -03:00
commit be348f70f6
32 changed files with 8631 additions and 0 deletions

41
.gitignore vendored Normal file
View file

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

40
README.md Normal file
View file

@ -0,0 +1,40 @@
# Portfolio — Felipe Domingues
Portfólio pessoal bilíngue (PT/EN) — Next.js 16 + TypeScript + Tailwind CSS v4 + Framer Motion.
## Stack
- **Next.js 16** (App Router, SSG) + React 19
- **Tailwind CSS v4** + dark mode (next-themes)
- **Framer Motion** (scroll reveals, terminal animado, contadores)
- **i18n** próprio: rotas `/pt` e `/en`, redirect por `Accept-Language`
## Comandos
```sh
npm install
npm run dev # http://localhost:3000
npm run build
npm run lint
```
## Deploy (Vercel)
1. Crie o repositório no GitHub e faça push:
```sh
git remote add origin git@github.com:domfelipe/portfolio.git
git push -u origin main
```
2. No [Vercel](https://vercel.com/new), importe o repositório — framework detectado automaticamente (Next.js), sem config extra.
3. Após o primeiro deploy, defina a URL final em `src/i18n/config.ts` (`SITE_URL`) ou via env `NEXT_PUBLIC_SITE_URL` para as meta tags canônicas/OG.
## Estrutura
```
src/
├── app/[locale]/ # layout + page (SSG por locale)
├── components/ # Hero (terminal), Projects, Skills, Contact...
├── data/ # projetos (repos, tags)
├── i18n/ # config, dicionários pt/en, context
└── proxy.ts # redirect / → /pt ou /en
```

18
eslint.config.mjs Normal file
View file

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
next.config.ts Normal file
View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6882
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "portfolio",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"framer-motion": "^12.42.2",
"lucide-react": "^1.25.0",
"next": "16.2.10",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.10",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View file

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View file

@ -0,0 +1,93 @@
import type { Metadata, Viewport } from "next";
import { IBM_Plex_Sans, JetBrains_Mono, Space_Grotesk } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { defaultLocale, locales, SITE_URL } from "@/i18n/config";
import type { Locale } from "@/i18n/config";
import { dictionaries } from "@/i18n/dictionaries";
import "../globals.css";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
display: "swap",
});
const ibmPlexSans = IBM_Plex_Sans({
subsets: ["latin"],
weight: ["400", "500", "600"],
variable: "--font-ibm-plex-sans",
display: "swap",
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains-mono",
display: "swap",
});
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
function resolveLocale(raw: string): Locale {
return locales.includes(raw as Locale) ? (raw as Locale) : defaultLocale;
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const locale = resolveLocale((await params).locale);
const { meta } = dictionaries[locale];
return {
metadataBase: new URL(SITE_URL),
title: meta.title,
description: meta.description,
alternates: {
canonical: `/${locale}`,
languages: {
pt: "/pt",
en: "/en",
"x-default": "/pt",
},
},
openGraph: {
title: meta.title,
description: meta.description,
url: `/${locale}`,
siteName: "Felipe Domingues",
type: "website",
locale: locale === "pt" ? "pt_BR" : "en_US",
},
};
}
export const viewport: Viewport = {
themeColor: "#0b1017",
};
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const locale = resolveLocale((await params).locale);
return (
<html
lang={locale === "pt" ? "pt-BR" : "en"}
suppressHydrationWarning
className={`${spaceGrotesk.variable} ${ibmPlexSans.variable} ${jetbrainsMono.variable}`}
>
<body>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</body>
</html>
);
}

37
src/app/[locale]/page.tsx Normal file
View file

@ -0,0 +1,37 @@
import { I18nProvider } from "@/i18n/context";
import { defaultLocale, locales } from "@/i18n/config";
import type { Locale } from "@/i18n/config";
import Background from "@/components/Background";
import Navbar from "@/components/Navbar";
import Hero from "@/components/Hero";
import Marquee from "@/components/Marquee";
import Projects from "@/components/Projects";
import Skills from "@/components/Skills";
import Contact from "@/components/Contact";
import Footer from "@/components/Footer";
export default async function Home({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const raw = (await params).locale;
const locale: Locale = locales.includes(raw as Locale) ? (raw as Locale) : defaultLocale;
return (
<I18nProvider locale={locale}>
<Background />
<Navbar />
<div id="top">
<main>
<Hero />
<Marquee />
<Projects />
<Skills />
<Contact />
</main>
<Footer />
</div>
</I18nProvider>
);
}

132
src/app/globals.css Normal file
View file

@ -0,0 +1,132 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
:root {
--bg: #f2f1ec;
--surface: #ffffff;
--raised: #faf9f5;
--line: #e2e0d6;
--line-strong: #c8c5b8;
--ink: #141a21;
--mute: #5b6672;
--brand: #b06e08;
--brand-strong: #8f5905;
--accent: #0d8f63;
--sky: #0a6fa4;
--glow-a: rgba(176, 110, 8, 0.16);
--glow-b: rgba(13, 143, 99, 0.13);
--term-bg: #10161f;
--term-line: #223042;
}
.dark {
--bg: #0b1017;
--surface: #111925;
--raised: #17212f;
--line: #1e2a3a;
--line-strong: #2e3d52;
--ink: #e9eef5;
--mute: #8fa0b4;
--brand: #f5a524;
--brand-strong: #ffc24d;
--accent: #3ecf8e;
--sky: #5ec8f2;
--glow-a: rgba(245, 165, 36, 0.12);
--glow-b: rgba(62, 207, 142, 0.09);
--term-bg: #0d131c;
--term-line: #1d2a3c;
}
@theme inline {
--color-bg: var(--bg);
--color-surface: var(--surface);
--color-raised: var(--raised);
--color-line: var(--line);
--color-line-strong: var(--line-strong);
--color-ink: var(--ink);
--color-mute: var(--mute);
--color-brand: var(--brand);
--color-brand-strong: var(--brand-strong);
--color-accent: var(--accent);
--color-sky: var(--sky);
--font-display: var(--font-space-grotesk), ui-sans-serif, system-ui, sans-serif;
--font-sans: var(--font-ibm-plex-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-jetbrains-mono), ui-monospace, monospace;
--animate-marquee: marquee 36s linear infinite;
--animate-drift: drift 14s ease-in-out infinite alternate;
--animate-drift-slow: drift 22s ease-in-out infinite alternate-reverse;
--animate-blink: blink 1.1s step-end infinite;
}
@keyframes marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
@keyframes drift {
from {
transform: translate(0, 0) scale(1);
}
to {
transform: translate(60px, 40px) scale(1.12);
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--bg);
color: var(--ink);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
transition: background-color 0.3s ease, color 0.3s ease;
}
::selection {
background: var(--brand);
color: var(--bg);
}
.bg-grid {
background-image: linear-gradient(var(--line) 1px, transparent 1px),
linear-gradient(90deg, var(--line) 1px, transparent 1px);
background-size: 56px 56px;
}
.noise {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.text-stroke {
color: transparent;
-webkit-text-stroke: 1.5px var(--ink);
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}

5
src/app/icon.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#0b1017"/>
<path d="M17 21l13 11-13 11" stroke="#f5a524" stroke-width="5.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="34" y="40" width="14" height="5.5" rx="2.75" fill="#3ecf8e"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

View file

@ -0,0 +1,11 @@
export default function Background() {
return (
<div aria-hidden className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
<div className="absolute inset-0 bg-bg" />
<div className="bg-grid absolute inset-0 opacity-60 [mask-image:radial-gradient(ellipse_80%_55%_at_50%_0%,black_30%,transparent_100%)]" />
<div className="absolute -top-48 -left-48 h-[36rem] w-[36rem] animate-drift rounded-full bg-[var(--glow-a)] blur-3xl" />
<div className="absolute -right-48 -bottom-56 h-[40rem] w-[40rem] animate-drift-slow rounded-full bg-[var(--glow-b)] blur-3xl" />
<div className="noise absolute inset-0 opacity-[0.05]" />
</div>
);
}

112
src/components/Contact.tsx Normal file
View file

@ -0,0 +1,112 @@
"use client";
import { ArrowUpRight, Mail } from "lucide-react";
import { useI18n } from "@/i18n/context";
import SectionHeading from "./SectionHeading";
import Reveal from "./Reveal";
import { GithubIcon, LinkedinIcon } from "./icons";
const EMAIL = "feliperdomingues@gmail.com";
const LINKEDIN = "https://www.linkedin.com/in/feliperdomingues/";
const GITHUB = "https://github.com/domfelipe";
export default function Contact() {
const { t } = useI18n();
const rows = [
{ label: t.contact.card.email, value: EMAIL, href: `mailto:${EMAIL}`, icon: Mail },
{ label: t.contact.card.github, value: "@domfelipe", href: GITHUB, icon: GithubIcon },
{
label: t.contact.card.linkedin,
value: "/in/feliperdomingues",
href: LINKEDIN,
icon: LinkedinIcon,
},
];
return (
<section id="contact" className="mx-auto max-w-6xl scroll-mt-24 px-5 py-24 sm:px-8 lg:py-32">
<div className="grid items-center gap-14 lg:grid-cols-[1.15fr_0.85fr]">
<div>
<SectionHeading
label={t.contact.label}
heading={t.contact.heading}
sub={t.contact.sub}
/>
<Reveal delay={0.15}>
<div className="mt-9 flex flex-wrap items-center gap-4">
<a
href={`mailto:${EMAIL}`}
className="group inline-flex items-center gap-2 rounded-lg bg-brand px-6 py-3 font-mono text-sm font-semibold text-bg transition-all hover:-translate-y-0.5 hover:bg-brand-strong hover:shadow-lg hover:shadow-[var(--glow-a)]"
>
<Mail className="size-4" />
{t.contact.emailBtn}
</a>
<a
href={LINKEDIN}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-line-strong px-6 py-3 font-mono text-sm text-ink transition-all hover:-translate-y-0.5 hover:border-[#0a66c2] hover:text-[#0a66c2]"
>
<LinkedinIcon className="size-4" />
{t.contact.linkedinBtn}
</a>
<a
href={GITHUB}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-line-strong px-6 py-3 font-mono text-sm text-ink transition-all hover:-translate-y-0.5 hover:border-brand hover:text-brand"
>
<GithubIcon className="size-4" />
{t.contact.githubBtn}
</a>
</div>
</Reveal>
</div>
<Reveal delay={0.2}>
<div className="overflow-hidden rounded-xl border border-[var(--term-line)] bg-[var(--term-bg)] shadow-2xl shadow-black/40">
<div className="flex items-center gap-2 border-b border-[var(--term-line)] px-4 py-3">
<span className="size-3 rounded-full bg-[#ff5f57]" />
<span className="size-3 rounded-full bg-[#febc2e]" />
<span className="size-3 rounded-full bg-[#28c840]" />
<span className="ml-3 font-mono text-xs text-[#7d8da1]">~/felipe/contato</span>
</div>
<div className="space-y-4 p-6 font-mono text-[13px]">
<p className="text-[#e9eef5]">
<span className="font-semibold text-[#f5a524]">$</span>
<span className="ml-2 text-[#7d8da1]">{t.contact.card.prompt}</span>
</p>
{rows.map((row) => (
<a
key={row.label}
href={row.href}
target={row.href.startsWith("mailto") ? undefined : "_blank"}
rel="noreferrer"
className="group flex items-center gap-3 rounded-lg border border-transparent px-3 py-2.5 transition-all hover:border-[var(--term-line)] hover:bg-white/5"
>
<row.icon className="size-4 shrink-0 text-[#7d8da1] transition-colors group-hover:text-[#f5a524]" />
<span className="w-20 shrink-0 text-[#7d8da1]">{row.label}</span>
<span className="truncate text-[#e9eef5] transition-colors group-hover:text-[#f5a524]">
{row.value}
</span>
<ArrowUpRight className="ml-auto size-3.5 shrink-0 text-[#7d8da1] opacity-0 transition-all group-hover:opacity-100" />
</a>
))}
<p className="flex items-center gap-3 px-3 py-2.5">
<span className="w-20 shrink-0 text-[#7d8da1]">{t.contact.card.statusLabel}</span>
<span className="flex items-center gap-2 text-[#3ecf8e]">
<span className="relative flex size-1.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-[#3ecf8e] opacity-60" />
<span className="relative inline-flex size-1.5 rounded-full bg-[#3ecf8e]" />
</span>
{t.contact.card.status}
</span>
</p>
</div>
</div>
</Reveal>
</div>
</section>
);
}

View file

@ -0,0 +1,44 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useInView } from "framer-motion";
export default function CountUp({
to,
locale,
suffix = "",
className,
}: {
to: number;
locale: string;
suffix?: string;
className?: string;
}) {
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, margin: "-40px" });
const [value, setValue] = useState(0);
useEffect(() => {
if (!inView) return;
let raf = 0;
const start = performance.now();
const duration = 1900;
const tick = (now: number) => {
const progress = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 4);
setValue(Math.round(to * eased));
if (progress < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [inView, to]);
const formatted = new Intl.NumberFormat(locale === "pt" ? "pt-BR" : "en-US").format(value);
return (
<span ref={ref} className={className}>
{formatted}
{suffix}
</span>
);
}

26
src/components/Footer.tsx Normal file
View file

@ -0,0 +1,26 @@
"use client";
import { ChevronUp } from "lucide-react";
import { useI18n } from "@/i18n/context";
export default function Footer() {
const { t } = useI18n();
return (
<footer className="border-t border-line">
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-4 px-5 py-10 sm:flex-row sm:px-8">
<div className="text-center sm:text-left">
<p className="font-mono text-xs text-mute">{t.footer.built}</p>
<p className="mt-1.5 font-mono text-xs text-mute/70">{t.footer.rights}</p>
</div>
<a
href="#top"
className="group inline-flex items-center gap-1.5 rounded-lg border border-line bg-surface px-4 py-2 font-mono text-xs text-mute transition-all hover:-translate-y-0.5 hover:border-brand hover:text-brand"
>
<ChevronUp className="size-3.5 transition-transform group-hover:-translate-y-0.5" />
{t.footer.top}
</a>
</div>
</footer>
);
}

113
src/components/Hero.tsx Normal file
View file

@ -0,0 +1,113 @@
"use client";
import { ArrowRight } from "lucide-react";
import { motion } from "framer-motion";
import { useI18n } from "@/i18n/context";
import Terminal from "./Terminal";
export default function Hero() {
const { t, locale } = useI18n();
return (
<section className="relative mx-auto grid max-w-6xl items-center gap-14 px-5 pt-32 pb-20 sm:px-8 lg:grid-cols-[1.05fr_0.95fr] lg:gap-10 lg:pt-40 lg:pb-28">
<div>
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="inline-flex items-center gap-2.5 rounded-full border border-line bg-surface px-4 py-1.5"
>
<span className="relative flex size-2">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-accent opacity-60" />
<span className="relative inline-flex size-2 rounded-full bg-accent" />
</span>
<span className="font-mono text-[11px] tracking-widest text-mute uppercase">
{t.hero.status}
</span>
</motion.div>
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1, ease: [0.22, 1, 0.36, 1] }}
className="mt-8 font-mono text-sm text-brand"
>
{"// "}
{t.hero.eyebrow}
</motion.p>
<motion.h1
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.18, ease: [0.22, 1, 0.36, 1] }}
className="mt-4 font-display text-6xl leading-[0.95] font-bold tracking-tight text-ink sm:text-7xl lg:text-8xl"
>
Felipe
<br />
<span className="text-stroke">Domingues</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="mt-7 max-w-xl text-lg leading-relaxed text-mute"
>
{t.hero.bio}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.42, ease: [0.22, 1, 0.36, 1] }}
className="mt-9 flex flex-wrap items-center gap-4"
>
<a
href="#projects"
className="group inline-flex items-center gap-2 rounded-lg bg-brand px-6 py-3 font-mono text-sm font-semibold text-bg transition-all hover:-translate-y-0.5 hover:bg-brand-strong hover:shadow-lg hover:shadow-[var(--glow-a)]"
>
{t.hero.ctaPrimary}
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
</a>
<a
href="#contact"
className="inline-flex items-center gap-2 rounded-lg border border-line-strong px-6 py-3 font-mono text-sm text-ink transition-all hover:-translate-y-0.5 hover:border-brand hover:text-brand"
>
{t.hero.ctaSecondary}
</a>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.55 }}
className="mt-10 flex flex-wrap items-center gap-x-3 gap-y-2 font-mono text-xs text-mute"
>
{t.hero.badges.map((badge, i) => (
<span key={badge} className="flex items-center gap-3">
{i > 0 && <span className="text-brand">·</span>}
{badge}
</span>
))}
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 32 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.35, ease: [0.22, 1, 0.36, 1] }}
className="relative"
>
<div className="absolute -inset-6 -z-10 rounded-3xl bg-[var(--glow-a)] blur-2xl" />
<Terminal key={locale} title={t.hero.terminal.title} lines={t.hero.terminal.lines} />
<div className="absolute -bottom-5 -left-4 flex items-center gap-2.5 rounded-lg border border-line bg-surface px-4 py-2.5 shadow-xl sm:-left-8">
<span className="relative flex size-2">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-accent opacity-60" />
<span className="relative inline-flex size-2 rounded-full bg-accent" />
</span>
<span className="font-mono text-xs text-ink">{t.hero.floatingBadge}</span>
</div>
</motion.div>
</section>
);
}

View file

@ -0,0 +1,24 @@
"use client";
import { useI18n } from "@/i18n/context";
export default function Marquee() {
const { t } = useI18n();
const items = [...t.marquee, ...t.marquee];
return (
<div className="group overflow-hidden border-y border-line bg-surface/70">
<div className="flex w-max animate-marquee group-hover:[animation-play-state:paused]">
{items.map((item, i) => (
<span
key={`${item}-${i}`}
className="flex items-center gap-8 px-4 py-3.5 font-mono text-xs tracking-[0.22em] whitespace-nowrap text-mute uppercase"
>
{item}
<span className="text-brand"></span>
</span>
))}
</div>
</div>
);
}

135
src/components/Navbar.tsx Normal file
View file

@ -0,0 +1,135 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import { Menu, Moon, Sun, X } from "lucide-react";
import { useI18n } from "@/i18n/context";
import { locales } from "@/i18n/config";
import type { Locale } from "@/i18n/config";
export default function Navbar() {
const { t } = useI18n();
const pathname = usePathname();
const { resolvedTheme, setTheme } = useTheme();
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const links = [
{ href: "#projects", label: t.nav.projects },
{ href: "#skills", label: t.nav.skills },
{ href: "#contact", label: t.nav.contact },
];
const currentLocale = (pathname.split("/")[1] ?? "pt") as Locale;
return (
<header
className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ${
scrolled
? "border-b border-line bg-bg/85 backdrop-blur-md"
: "border-b border-transparent bg-transparent"
}`}
>
<nav className="mx-auto flex h-16 max-w-6xl items-center justify-between px-5 sm:px-8">
<a
href={`/${currentLocale}`}
className="group flex items-center gap-1 font-mono text-sm font-semibold text-ink"
>
<span className="text-brand">~/</span>felipe-domingues
<span className="inline-block h-4 w-[7px] translate-y-[2px] animate-blink bg-brand" />
</a>
<div className="hidden items-center gap-7 md:flex">
{links.map((link) => (
<a
key={link.href}
href={link.href}
className="font-mono text-[13px] text-mute transition-colors hover:text-brand"
>
{link.label}
</a>
))}
<div className="flex items-center gap-1 rounded-lg border border-line bg-surface p-1">
{locales.map((l) => (
<Link
key={l}
href={`/${l}`}
className={`rounded-md px-2 py-0.5 font-mono text-[11px] uppercase transition-colors ${
l === currentLocale
? "bg-brand text-bg font-bold"
: "text-mute hover:text-ink"
}`}
>
{l}
</Link>
))}
</div>
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
aria-label="toggle theme"
className="rounded-lg border border-line bg-surface p-2 text-mute transition-colors hover:border-brand hover:text-brand"
>
{resolvedTheme === "dark" ? <Sun className="size-4" /> : <Moon className="size-4" />}
</button>
</div>
<div className="flex items-center gap-2 md:hidden">
<div className="flex items-center gap-1 rounded-lg border border-line bg-surface p-1">
{locales.map((l) => (
<Link
key={l}
href={`/${l}`}
className={`rounded-md px-2 py-0.5 font-mono text-[11px] uppercase ${
l === currentLocale ? "bg-brand text-bg font-bold" : "text-mute"
}`}
>
{l}
</Link>
))}
</div>
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
aria-label="toggle theme"
className="rounded-lg border border-line bg-surface p-2 text-mute"
>
{resolvedTheme === "dark" ? <Sun className="size-4" /> : <Moon className="size-4" />}
</button>
<button
onClick={() => setOpen(!open)}
aria-label="menu"
className="rounded-lg border border-line bg-surface p-2 text-ink"
>
{open ? <X className="size-4" /> : <Menu className="size-4" />}
</button>
</div>
</nav>
{open && (
<div className="border-b border-line bg-bg/95 backdrop-blur-md md:hidden">
<div className="flex flex-col gap-1 px-5 py-4">
{links.map((link) => (
<a
key={link.href}
href={link.href}
onClick={() => setOpen(false)}
className="rounded-lg px-3 py-2 font-mono text-sm text-mute transition-colors hover:bg-surface hover:text-brand"
>
{"// "}
{link.label}
</a>
))}
</div>
</div>
)}
</header>
);
}

157
src/components/Projects.tsx Normal file
View file

@ -0,0 +1,157 @@
"use client";
import { ArrowUpRight } from "lucide-react";
import { useI18n } from "@/i18n/context";
import { projects } from "@/data/projects";
import type { Project } from "@/data/projects";
import SectionHeading from "./SectionHeading";
import Reveal from "./Reveal";
import CountUp from "./CountUp";
import { GithubIcon } from "./icons";
export default function Projects() {
const { t, locale } = useI18n();
return (
<section id="projects" className="mx-auto max-w-6xl scroll-mt-24 px-5 py-24 sm:px-8 lg:py-32">
<SectionHeading
label={t.projects.label}
heading={t.projects.heading}
sub={t.projects.sub}
/>
<div className="mt-14 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-12">
{projects.map((project, i) =>
project.id === "agentes" ? (
<AgentsCard key={project.id} project={project} index={i + 1} locale={locale} />
) : (
<ProjectCard key={project.id} project={project} index={i + 1} />
)
)}
</div>
</section>
);
}
function spanFor(project: Project) {
if (project.id === "mika") return "md:col-span-2 lg:col-span-7";
if (project.id === "agentes") return "md:col-span-2 lg:col-span-5";
return "lg:col-span-4";
}
function ProjectCard({ project, index }: { project: Project; index: number }) {
const { t } = useI18n();
const info = t.projects.items[project.id];
const inner = (
<>
<div className="flex items-center justify-between">
<span className="font-mono text-xs text-mute">
{String(index).padStart(2, "0")}
</span>
{project.repo ? (
<ArrowUpRight className="size-4 text-mute transition-all group-hover:translate-x-0.5 group-hover:-translate-y-0.5 group-hover:text-brand" />
) : (
<GithubIcon className="size-4 text-mute transition-colors group-hover:text-brand" />
)}
</div>
<h3 className="mt-5 font-display text-2xl font-semibold tracking-tight text-ink transition-colors group-hover:text-brand">
{info.title}
</h3>
<p className="mt-3 flex-1 leading-relaxed text-mute">{info.description}</p>
<div className="mt-6 flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span
key={tag}
className="rounded-md border border-line bg-raised px-2.5 py-1 font-mono text-[11px] text-mute"
>
{tag}
</span>
))}
</div>
</>
);
const classes =
"group relative flex h-full flex-col rounded-xl border border-line bg-surface p-6 transition-all duration-300 hover:-translate-y-1 hover:border-brand/50 hover:shadow-[0_16px_48px_-16px_var(--glow-a)]";
return (
<Reveal className={spanFor(project)} delay={(index % 3) * 0.08}>
{project.repo ? (
<a href={project.repo} target="_blank" rel="noreferrer" className={classes}>
{inner}
</a>
) : (
<div className={classes}>{inner}</div>
)}
</Reveal>
);
}
function AgentsCard({
project,
index,
locale,
}: {
project: Project;
index: number;
locale: string;
}) {
const { t } = useI18n();
const info = t.projects.items.agentes;
return (
<Reveal className={spanFor(project)} delay={0.08}>
<div className="relative flex h-full flex-col overflow-hidden rounded-xl border border-brand/40 bg-raised p-6">
<div className="pointer-events-none absolute -top-24 -right-24 size-64 rounded-full bg-[var(--glow-a)] blur-3xl" />
<div className="flex items-center justify-between">
<span className="font-mono text-xs text-mute">{String(index).padStart(2, "0")}</span>
<span className="flex items-center gap-2 rounded-full border border-accent/40 bg-surface px-3 py-1 font-mono text-[10px] tracking-widest text-accent uppercase">
<span className="relative flex size-1.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-accent opacity-60" />
<span className="relative inline-flex size-1.5 rounded-full bg-accent" />
</span>
live
</span>
</div>
<h3 className="mt-5 font-display text-2xl font-semibold tracking-tight text-ink">
{info.title}
</h3>
<p className="mt-3 leading-relaxed text-mute">{info.description}</p>
<div className="mt-auto grid grid-cols-2 gap-4 pt-8">
<div>
<CountUp
to={15000}
locale={locale}
suffix="+"
className="font-display text-4xl font-bold tracking-tight text-brand"
/>
<p className="mt-1 font-mono text-[11px] tracking-wide text-mute uppercase">
{t.projects.metrics.people}
</p>
</div>
<div>
<CountUp
to={600000}
locale={locale}
suffix="+"
className="font-display text-4xl font-bold tracking-tight text-accent"
/>
<p className="mt-1 font-mono text-[11px] tracking-wide text-mute uppercase">
{t.projects.metrics.messages}
</p>
</div>
</div>
<div className="mt-6 flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span
key={tag}
className="rounded-md border border-line bg-surface px-2.5 py-1 font-mono text-[11px] text-mute"
>
{tag}
</span>
))}
</div>
</div>
</Reveal>
);
}

29
src/components/Reveal.tsx Normal file
View file

@ -0,0 +1,29 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import type { ReactNode } from "react";
export default function Reveal({
children,
delay = 0,
y = 28,
className,
}: {
children: ReactNode;
delay?: number;
y?: number;
className?: string;
}) {
const reduce = useReducedMotion();
return (
<motion.div
className={className}
initial={reduce ? { opacity: 0 } : { opacity: 0, y }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.7, delay, ease: [0.22, 1, 0.36, 1] }}
>
{children}
</motion.div>
);
}

View file

@ -0,0 +1,21 @@
import Reveal from "./Reveal";
export default function SectionHeading({
label,
heading,
sub,
}: {
label: string;
heading: string;
sub?: string;
}) {
return (
<Reveal>
<p className="font-mono text-sm tracking-wide text-brand">{"// " + label}</p>
<h2 className="mt-3 font-display text-4xl font-bold tracking-tight text-ink sm:text-5xl">
{heading}
</h2>
{sub && <p className="mt-4 max-w-2xl text-lg text-mute">{sub}</p>}
</Reveal>
);
}

40
src/components/Skills.tsx Normal file
View file

@ -0,0 +1,40 @@
"use client";
import { useI18n } from "@/i18n/context";
import SectionHeading from "./SectionHeading";
import Reveal from "./Reveal";
export default function Skills() {
const { t } = useI18n();
const groups = Object.entries(t.skills.groups);
return (
<section id="skills" className="mx-auto max-w-6xl scroll-mt-24 px-5 py-24 sm:px-8 lg:py-32">
<SectionHeading label={t.skills.label} heading={t.skills.heading} sub={t.skills.sub} />
<div className="mt-14 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{groups.map(([key, group], i) => (
<Reveal key={key} delay={i * 0.08}>
<div className="group h-full rounded-xl border border-line bg-surface p-6 transition-all duration-300 hover:-translate-y-1 hover:border-accent/50 hover:shadow-[0_16px_48px_-16px_var(--glow-b)]">
<p className="font-mono text-xs text-brand">{`0${i + 1}.`}</p>
<h3 className="mt-2 font-display text-lg font-semibold tracking-tight text-ink">
{group.title}
</h3>
<ul className="mt-5 space-y-2.5">
{group.items.map((item) => (
<li
key={item}
className="flex items-center gap-2.5 text-sm text-mute transition-all duration-200 hover:translate-x-1 hover:text-ink"
>
<span className="text-accent"></span>
{item}
</li>
))}
</ul>
</div>
</Reveal>
))}
</div>
</section>
);
}

View file

@ -0,0 +1,99 @@
"use client";
import { useEffect, useState } from "react";
import type { TermLine } from "@/i18n/dictionaries/en";
const TYPE_SPEED = 26;
const LINE_PAUSE = 320;
export default function Terminal({ title, lines }: { title: string; lines: TermLine[] }) {
const [step, setStep] = useState(0);
const [chars, setChars] = useState(0);
useEffect(() => {
if (step >= lines.length) return;
const line = lines[step];
if (line.kind === "cmd") {
if (chars < line.text.length) {
const id = setTimeout(() => setChars((c) => c + 1), TYPE_SPEED);
return () => clearTimeout(id);
}
const id = setTimeout(() => {
setStep((s) => s + 1);
setChars(0);
}, LINE_PAUSE);
return () => clearTimeout(id);
}
const id = setTimeout(() => setStep((s) => s + 1), LINE_PAUSE);
return () => clearTimeout(id);
}, [step, chars, lines]);
const done = step >= lines.length;
return (
<div className="overflow-hidden rounded-xl border border-[var(--term-line)] bg-[var(--term-bg)] shadow-2xl shadow-black/40">
<div className="flex items-center gap-2 border-b border-[var(--term-line)] px-4 py-3">
<span className="size-3 rounded-full bg-[#ff5f57]" />
<span className="size-3 rounded-full bg-[#febc2e]" />
<span className="size-3 rounded-full bg-[#28c840]" />
<span className="ml-3 font-mono text-xs text-[#7d8da1]">{title}</span>
</div>
<div className="min-h-[264px] p-5 font-mono text-[13px] leading-7">
{lines.slice(0, step).map((line, i) => (
<TermLineRow key={i} line={line} />
))}
{!done && lines[step] && (
<TermLineRow
line={{
...lines[step],
text: lines[step].text.slice(0, lines[step].kind === "cmd" ? chars : lines[step].text.length),
}}
cursor
/>
)}
{done && (
<p className="text-[#e9eef5]">
<Prompt />
<span className="ml-2 inline-block h-4 w-2 translate-y-[3px] animate-blink bg-[#f5a524]" />
</p>
)}
</div>
</div>
);
}
function Prompt() {
return <span className="font-semibold text-[#f5a524]">$</span>;
}
function TermLineRow({ line, cursor }: { line: TermLine; cursor?: boolean }) {
const cursorEl = cursor && (
<span className="ml-0.5 inline-block h-4 w-2 translate-y-[3px] animate-blink bg-[#f5a524]" />
);
if (line.kind === "cmd") {
return (
<p className="text-[#e9eef5]">
<Prompt />
<span className="ml-2">{line.text}</span>
{cursorEl}
</p>
);
}
if (line.kind === "ok") {
return (
<p className="text-[#7d8da1]">
<span className="font-semibold text-[#3ecf8e]"></span>
<span className="ml-2">{line.text}</span>
{cursorEl}
</p>
);
}
return (
<p className="text-[#7d8da1]">
<span className="font-semibold text-[#5ec8f2]"></span>
<span className="ml-2">{line.text}</span>
{cursorEl}
</p>
);
}

19
src/components/icons.tsx Normal file
View file

@ -0,0 +1,19 @@
interface IconProps {
className?: string;
}
export function GithubIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55 0-.27-.01-1.17-.02-2.12-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.03 1.76 2.69 1.25 3.35.96.1-.75.4-1.25.72-1.54-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .96-.31 3.15 1.18a10.9 10.9 0 0 1 2.87-.39c.97 0 1.95.13 2.87.39 2.19-1.49 3.15-1.18 3.15-1.18.62 1.59.23 2.76.11 3.05.73.81 1.18 1.83 1.18 3.09 0 4.41-2.69 5.38-5.25 5.67.41.35.77 1.05.77 2.12 0 1.53-.01 2.76-.01 3.14 0 .3.2.66.8.55A10.52 10.52 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
</svg>
);
}
export function LinkedinIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
<path d="M20.45 20.45h-3.55v-5.57c0-1.33-.03-3.04-1.85-3.04-1.86 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.47-.9 1.63-1.85 3.36-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.06 2.06 0 1 1 0-4.12 2.06 2.06 0 0 1 0 4.12ZM7.12 20.45H3.56V9h3.56v11.45Z" />
</svg>
);
}

72
src/data/projects.ts Normal file
View file

@ -0,0 +1,72 @@
export type ProjectId =
| "mika"
| "agentes"
| "medix"
| "vitadiet"
| "licitaos"
| "boraatender"
| "barberpass"
| "govtech"
| "memoryos"
| "arch";
export interface Project {
id: ProjectId;
repo?: string;
tags: string[];
featured?: boolean;
}
export const projects: Project[] = [
{
id: "mika",
repo: "https://github.com/domfelipe/mika-agent-assist",
tags: ["React", "TanStack Start", "Supabase", "Telegram API", "Railway", "Paddle"],
featured: true,
},
{
id: "agentes",
tags: ["AI Agents", "LLMs", "Telegram", "WhatsApp", "Memória"],
featured: true,
},
{
id: "medix",
repo: "https://github.com/domfelipe/medix-app",
tags: ["TypeScript", "Supabase", "Lovable Cloud"],
},
{
id: "vitadiet",
repo: "https://github.com/domfelipe/vitadiet",
tags: ["TanStack Start", "Supabase", "Cloudflare Workers"],
},
{
id: "licitaos",
repo: "https://github.com/domfelipe/licitaos-ai",
tags: ["TanStack Start", "Supabase", "Playwright", "IA"],
},
{
id: "boraatender",
repo: "https://github.com/domfelipe/bora-botucatu",
tags: ["TanStack Start", "Capacitor", "Mercado Pago"],
},
{
id: "barberpass",
repo: "https://github.com/domfelipe/barber-pass-app",
tags: ["React", "Mobile-first", "Supabase"],
},
{
id: "govtech",
repo: "https://github.com/domfelipe/domcodosul-govtech",
tags: ["TanStack Start", "Supabase", "Cloudflare"],
},
{
id: "memoryos",
repo: "https://github.com/domfelipe/memory-os-visual-map",
tags: ["Memory Routing", "Benchmarks", "Agentes"],
},
{
id: "arch",
repo: "https://github.com/domfelipe/arch",
tags: ["Rust", "TUI", "Coding Agents"],
},
];

6
src/i18n/config.ts Normal file
View file

@ -0,0 +1,6 @@
export const locales = ["pt", "en"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "pt";
export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL ?? "https://felipedomingues.vercel.app";

34
src/i18n/context.tsx Normal file
View file

@ -0,0 +1,34 @@
"use client";
import { createContext, useContext } from "react";
import type { ReactNode } from "react";
import type { Locale } from "./config";
import type { Dictionary } from "./dictionaries/en";
import { dictionaries } from "./dictionaries";
interface I18nContextValue {
locale: Locale;
t: Dictionary;
}
const I18nContext = createContext<I18nContextValue | null>(null);
export function I18nProvider({
locale,
children,
}: {
locale: Locale;
children: ReactNode;
}) {
return (
<I18nContext.Provider value={{ locale, t: dictionaries[locale] }}>
{children}
</I18nContext.Provider>
);
}
export function useI18n() {
const ctx = useContext(I18nContext);
if (!ctx) throw new Error("useI18n must be used within I18nProvider");
return ctx;
}

174
src/i18n/dictionaries/en.ts Normal file
View file

@ -0,0 +1,174 @@
export type TermLineKind = "cmd" | "ok" | "out";
export interface TermLine {
kind: TermLineKind;
text: string;
}
export const en = {
meta: {
title: "Felipe Domingues — Full-Stack Developer & AI Agent Engineer",
description:
"I build digital products and AI agents that talk to real people: 15,000+ people served and 600,000+ messages exchanged.",
},
nav: {
projects: "projects",
skills: "stack",
contact: "contact",
},
hero: {
status: "online — agents answering in real time",
eyebrow: "full-stack developer · AI agent engineer",
bio: "I turn ideas into digital products and AI agents that hold real conversations — from database to deploy, focused on experience and results.",
ctaPrimary: "see projects",
ctaSecondary: "get in touch",
badges: ["15,000+ people served", "600,000+ messages", "10 products shipped"],
terminal: {
title: "felipe@agents: ~",
lines: [
{ kind: "cmd", text: "mika status --agent felipe" },
{ kind: "ok", text: "telegram: connected" },
{ kind: "ok", text: "memory: synced" },
{ kind: "ok", text: "agents uptime: 24/7" },
{ kind: "cmd", text: "felipe --summary" },
{ kind: "out", text: "Full-stack dev building AI agents" },
{ kind: "out", text: "for real customer conversations." },
] satisfies TermLine[],
},
floatingBadge: "agents online 24/7",
},
marquee: [
"TypeScript",
"React",
"Next.js",
"AI Agents",
"Supabase",
"Telegram Bots",
"Tailwind",
"Rust",
"Cloudflare Workers",
"LLMs",
"Railway",
"TanStack",
],
projects: {
label: "01 — projects",
heading: "Things I've built",
sub: "Real products in production — from SaaS platforms to AI agents serving thousands of people.",
viewRepo: "view repo",
items: {
mika: {
title: "Mika",
description:
"SaaS platform for personal AI agents with native Telegram integration. Multi-tenant container provisioning, one-tap bot onboarding, Paddle payments and a custom agent runtime.",
},
agentes: {
title: "AI Agents in production",
description:
"AI agents serving real people every day — from the first “hi” to resolution, with memory, brand voice and human handoff when it matters.",
},
medix: {
title: "Medix",
description:
"Medical practice management: scheduling, electronic records and full clinic operations in one fast, focused interface.",
},
vitadiet: {
title: "Vitadiet",
description:
"Nutrition platform for clinics: protocols and meal-plan templates, per-patient planning and a patient app with published plans.",
},
licitaos: {
title: "LicitaOS",
description:
"Public procurement intelligence: nationwide PNCP radar, fit-score screening and AI-generated analysis in an operational cockpit.",
},
boraatender: {
title: "BoraAtender",
description:
"Services marketplace with a coin economy, in-app quotes and Mercado Pago payments — web and Android via Capacitor.",
},
barberpass: {
title: "BarberPass",
description:
"A relationship club for barbershops: booking, conversation continuity and membership across client and shop modes.",
},
govtech: {
title: "GovTech",
description:
"Municipal platform: transparency portal, ombudsman, official gazette, health triage & records, and internal management.",
},
memoryos: {
title: "Memory-OS",
description:
"Memory system for AI agents with layered routing — benchmark hit@1 improved from 3/8 to 8/8.",
},
arch: {
title: "Arch",
description:
"A coding-agent harness and TUI in Rust: fullscreen, mouse-interactive, with an extensible plugin architecture.",
},
},
metrics: {
people: "people served",
messages: "messages exchanged",
},
},
skills: {
label: "02 — stack",
heading: "What I build with",
sub: "The toolbox behind ten shipped products and agents that never sleep.",
groups: {
languages: {
title: "languages",
items: ["TypeScript", "JavaScript", "Python", "Rust", "SQL"],
},
frontend: {
title: "frontend",
items: ["React", "Next.js", "TanStack Start", "Tailwind CSS", "Framer Motion"],
},
ai: {
title: "AI & agents",
items: [
"Agent architecture",
"LLMs & prompt design",
"Telegram / WhatsApp bots",
"Ollama",
"Memory & RAG",
],
},
infra: {
title: "infra & backend",
items: [
"Supabase · Edge Functions",
"Cloudflare Workers",
"Railway · Docker",
"Vercel",
"Paddle · Mercado Pago",
],
},
},
},
contact: {
label: "03 — contact",
heading: "Let's build something together?",
sub: "Open to opportunities, projects and conversations about AI agents. Guaranteed human reply — no agent in between.",
emailBtn: "send an email",
linkedinBtn: "LinkedIn",
githubBtn: "GitHub",
card: {
prompt: "cat contacts.txt",
email: "email",
github: "github",
linkedin: "linkedin",
statusLabel: "status",
status: "open to opportunities",
},
},
footer: {
built: "Built with Next.js, Tailwind and a few AI agents.",
rights: "© 2026 Felipe Domingues. All rights reserved.",
top: "back to top",
},
};
export type Dictionary = typeof en;

View file

@ -0,0 +1,6 @@
import type { Locale } from "../config";
import type { Dictionary } from "./en";
import { en } from "./en";
import { pt } from "./pt";
export const dictionaries: Record<Locale, Dictionary> = { en, pt };

167
src/i18n/dictionaries/pt.ts Normal file
View file

@ -0,0 +1,167 @@
import type { Dictionary } from "./en";
export const pt = {
meta: {
title: "Felipe Domingues — Desenvolvedor Full-Stack & Engenheiro de Agentes de IA",
description:
"Construo produtos digitais e agentes de IA que conversam com pessoas reais: 15.000+ pessoas atendidas e 600.000+ mensagens trocadas.",
},
nav: {
projects: "projetos",
skills: "stack",
contact: "contato",
},
hero: {
status: "online — agentes respondendo em tempo real",
eyebrow: "desenvolvedor full-stack · engenheiro de agentes de IA",
bio: "Transformo ideias em produtos digitais e agentes de IA que conversam com pessoas reais — do banco de dados ao deploy, com foco em experiência e resultado.",
ctaPrimary: "ver projetos",
ctaSecondary: "fale comigo",
badges: ["15.000+ pessoas atendidas", "600.000+ mensagens", "10 produtos lançados"],
terminal: {
title: "felipe@agentes: ~",
lines: [
{ kind: "cmd", text: "mika status --agente felipe" },
{ kind: "ok", text: "telegram: conectado" },
{ kind: "ok", text: "memoria: sincronizada" },
{ kind: "ok", text: "agentes ativos: 24/7" },
{ kind: "cmd", text: "felipe --resumo" },
{ kind: "out", text: "Dev full-stack construindo agentes" },
{ kind: "out", text: "de IA para atendimento real." },
],
},
floatingBadge: "agentes online 24/7",
},
marquee: [
"TypeScript",
"React",
"Next.js",
"Agentes de IA",
"Supabase",
"Bots de Telegram",
"Tailwind",
"Rust",
"Cloudflare Workers",
"LLMs",
"Railway",
"TanStack",
],
projects: {
label: "01 — projetos",
heading: "Coisas que construí",
sub: "Produtos reais, em produção — de plataformas SaaS a agentes de IA atendendo milhares de pessoas.",
viewRepo: "ver repositório",
items: {
mika: {
title: "Mika",
description:
"Plataforma SaaS de agentes pessoais de IA com integração nativa ao Telegram. Provisionamento multi-tenant de containers, onboarding do bot em um toque, pagamentos com Paddle e runtime próprio de agentes.",
},
agentes: {
title: "Agentes de IA em produção",
description:
"Agentes de IA atendendo pessoas reais todos os dias — do primeiro “oi” à resolução, com memória, tom de voz e handoff humano quando precisa.",
},
medix: {
title: "Medix",
description:
"Sistema de gestão médica: agenda, prontuário e operação completa do consultório em uma interface rápida e focada.",
},
vitadiet: {
title: "Vitadiet",
description:
"Plataforma de nutrição para consultórios: protocolos e modelos de cardápio, planejamento por paciente e app do paciente com planos publicados.",
},
licitaos: {
title: "LicitaOS",
description:
"Inteligência em licitações: radar nacional do PNCP, triagem por score de aderência e análises geradas por IA em um cockpit operacional.",
},
boraatender: {
title: "BoraAtender",
description:
"Marketplace de serviços com economia de moedas, orçamentos in-app e pagamentos via Mercado Pago — web e Android com Capacitor.",
},
barberpass: {
title: "BarberPass",
description:
"Clube de relacionamento para barbearias: agendamento, conversa contínua e fidelidade em dois modos — cliente e barbearia.",
},
govtech: {
title: "GovTech",
description:
"Plataforma municipal: portal de transparência, ouvidoria, diário oficial, saúde com triagem e prontuário, e gestão interna.",
},
memoryos: {
title: "Memory-OS",
description:
"Sistema de memória para agentes de IA com roteamento em camadas — hit rate de 3/8 para 8/8 no benchmark hit@1.",
},
arch: {
title: "Arch",
description:
"Harness e TUI para coding agents em Rust: fullscreen, interação por mouse e arquitetura extensível de plugins.",
},
},
metrics: {
people: "pessoas atendidas",
messages: "mensagens trocadas",
},
},
skills: {
label: "02 — stack",
heading: "Com o que construo",
sub: "A caixa de ferramentas por trás de dez produtos lançados e agentes que não dormem.",
groups: {
languages: {
title: "linguagens",
items: ["TypeScript", "JavaScript", "Python", "Rust", "SQL"],
},
frontend: {
title: "frontend",
items: ["React", "Next.js", "TanStack Start", "Tailwind CSS", "Framer Motion"],
},
ai: {
title: "IA & agentes",
items: [
"Arquitetura de agentes",
"LLMs & prompt design",
"Bots Telegram / WhatsApp",
"Ollama",
"Memória & RAG",
],
},
infra: {
title: "infra & backend",
items: [
"Supabase · Edge Functions",
"Cloudflare Workers",
"Railway · Docker",
"Vercel",
"Paddle · Mercado Pago",
],
},
},
},
contact: {
label: "03 — contato",
heading: "Vamos construir algo juntos?",
sub: "Aberto a oportunidades, projetos e conversas sobre agentes de IA. Resposta garantida de um humano — sem agente no meio.",
emailBtn: "mandar e-mail",
linkedinBtn: "LinkedIn",
githubBtn: "GitHub",
card: {
prompt: "cat contatos.txt",
email: "email",
github: "github",
linkedin: "linkedin",
statusLabel: "status",
status: "aberto a propostas",
},
},
footer: {
built: "Construído com Next.js, Tailwind e alguns agentes de IA.",
rights: "© 2026 Felipe Domingues. Todos os direitos reservados.",
top: "voltar ao topo",
},
} satisfies Dictionary;

17
src/proxy.ts Normal file
View file

@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { defaultLocale } from "./i18n/config";
export function proxy(request: NextRequest) {
const preferred = request.headers
.get("accept-language")
?.toLowerCase()
.startsWith("en")
? "en"
: defaultLocale;
return NextResponse.redirect(new URL(`/${preferred}`, request.url));
}
export const config = {
matcher: ["/"],
};

34
tsconfig.json Normal file
View file

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}