mirror of
https://github.com/domfelipe/portfolio.git
synced 2026-08-07 04:16:40 +00:00
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:
commit
be348f70f6
32 changed files with 8631 additions and 0 deletions
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal 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
40
README.md
Normal 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
18
eslint.config.mjs
Normal 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
7
next.config.ts
Normal 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
6882
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
29
package.json
Normal file
29
package.json
Normal 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
7
postcss.config.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
93
src/app/[locale]/layout.tsx
Normal file
93
src/app/[locale]/layout.tsx
Normal 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
37
src/app/[locale]/page.tsx
Normal 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
132
src/app/globals.css
Normal 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
5
src/app/icon.svg
Normal 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 |
11
src/components/Background.tsx
Normal file
11
src/components/Background.tsx
Normal 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
112
src/components/Contact.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
src/components/CountUp.tsx
Normal file
44
src/components/CountUp.tsx
Normal 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
26
src/components/Footer.tsx
Normal 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
113
src/components/Hero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
src/components/Marquee.tsx
Normal file
24
src/components/Marquee.tsx
Normal 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
135
src/components/Navbar.tsx
Normal 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
157
src/components/Projects.tsx
Normal 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
29
src/components/Reveal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
21
src/components/SectionHeading.tsx
Normal file
21
src/components/SectionHeading.tsx
Normal 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
40
src/components/Skills.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
99
src/components/Terminal.tsx
Normal file
99
src/components/Terminal.tsx
Normal 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
19
src/components/icons.tsx
Normal 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
72
src/data/projects.ts
Normal 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
6
src/i18n/config.ts
Normal 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
34
src/i18n/context.tsx
Normal 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
174
src/i18n/dictionaries/en.ts
Normal 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;
|
||||
6
src/i18n/dictionaries/index.ts
Normal file
6
src/i18n/dictionaries/index.ts
Normal 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
167
src/i18n/dictionaries/pt.ts
Normal 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
17
src/proxy.ts
Normal 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
34
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue