🚀 How I Built My Developer Portfolio with Next.js, GitHub API, DEV.to API + Trilingual Support and AI Chat in future

Who’s me?

I’m a self-taught developer still learning, and a few weeks ago I decided it was time to stop putting it off and actually build my portfolio.

💡 Inspired by: Portfolio with Gemini — Simple, Smooth, Subtle by @aldwin160. That post showed me how to approach the AI chat feature with a good system prompt. Highly recommend reading it.

Why I built this?

I had been studying web development for some time, and noticed that many developers had already built their portfolio models, so based on that, I decided to make mine, a complete portfolio of information for those who understand it. With the help of Next.js and Tailwind CSS frameworks with applications: API integration, server-side routing, TypeScript, animations, i18n.

✨ Features

Before diving into the code, here’s what the final portfolio does:

🌍 Trilingual ── PT 🇧🇷 / EN 🇺🇸 / ES 🇪🇸 with a navbar language switcher (saved in localStorage)
🌙 Dark / Light mode toggle
🐙 GitHub API ── live repos, commit count, follower count in the Hero section
✍️ DEV.to API ── posts fetched automatically, zero token needed
📬 Contact form via EmailJS (no backend needed)
🤖 Chat Assistant ── smart static responses in 3 languages, with Gemini AI ready to activate
📱 Fully responsive ── built mobile-first with inline styles + CSS variables

🛠️ Tech Stack & Why I Chose Each

Tool and Why

1. Next.js 14 (App Router) ── Server components, API routes, file-based routing — all in one
2. TypeScript ── Caught so many bugs at compile time instead of runtime
3. Tailwind CSS ── Utility classes are fast, but I ended up using CSS variables + inline styles for reliability
4. Framer Motion ── The animation presets made scroll animations trivial
5. GitHub API ── Free, well-documented, no special setup for public repos
6. DEV.to API ── Completely public — no token, no account needed to read posts
7. EmailJS ── Contact form without needing my own email server
8. Google Gemini ── Free tier, easy API — integrated but commented for now (rate limits)

📁 Project Structure

porfolioFull/
src/
├── app/
│ ├── globals.css
│ ├── layout.tsx # fonts, metadata, providers
│ ├── page.tsx # mounts all sections
│ ├── favicon.ico
│ └── api/
│ ├── github/
│ │ └── route.ts # server-side proxy (hides token)
│ ├── contact/
│ │ └── route.ts # EmailJS server-side
│ └── chat/
│ └── route.ts # Gemini API route (ready to activate)
├── components/
│ ├── sections/ # one file per page section
│ │ ├── Hero.tsx
│ │ ├── About.tsx
│ │ ├── Skills.tsx
│ │ ├── Projects.tsx
│ │ ├── Posts.tsx
│ │ └── Contact.tsx
│ ├── Navbar.tsx # with language switcher dropdown
│ ├── Footer.tsx
│ ├── ChatWidget.tsx # floating AI chat
│ └── ThemeProvider.tsx # dark/light context
├── data/
│ ├── portfolio.ts # ← only file you edit: your info, skills, projects
│ ├── i18n.ts # all PT/EN/ES translations
│ ├── LangContext.tsx # language context + localStorage
│ └── staticChat.ts # static chat responses by language
├── hooks/
│ ├── useGitHub.ts # fetches profile, repos, commits
│ └── useDevTo.ts # fetches your DEV.to posts
├── lib/
│ ├── github.ts # GitHub API functions
│ ├── devto.ts # DEV.to API functions
│ └── motion.ts # Framer Motion
│ └── utils.ts
└── types/
│ └── index.ts # TypeScript interfaces
└── etc/
│ └── .env.local #env
│ └── .gitignore
│ └── .components.json
│ └── eslint.config.mjs
│ └── next-env.d.ts
│ └── next.config.ts
│ └── package-lock.json
│ └── package.json
│ └── postcss.config.mjs
│ └── README.md
│ └── tailwind.config.ts
│ └── tsconfig.json

The separation makes sense once you understand the roles:

  • sections/ — page UI, one component per visual section
  • data/ — your content and translations, no UI logic
  • lib/ — pure functions that talk to external APIs
  • hooks/ — client-side data fetching with loading/error states
  • api/ — server-side routes that hide secret tokens from the browser

🐙 Integrating GitHub & DEV.to APIs

DEV.to — Zero config
The DEV.to API is completely public for reading articles. No token, no account, just fetch:

// src/lib/devto.ts
export async function getDevToPosts(username: string): Promise<DevToPost[]> {
  const res = await fetch(
    `https://dev.to/api/articles?username=${username}&per_page=10`,
    { next: { revalidate: 1800 } } // Next.js cache: 30min
  );
  if (!res.ok) return [];

  const posts: DevToApiPost[] = await res.json();
  return posts.map((p): DevToPost => ({
    id:    p.id,
    title: p.title,
    url:   p.url,
    tags:  p.tag_list || [],
    date:  p.published_at,
    // ... rest of mapping
  }));
}

GitHub — Hiding the token
The GitHub API works without a token for public repos, but you hit rate limits fast (60 req/hour unauthenticated vs 5,000 with a token). The trick: never expose the token to the browser. Use a Next.js API route as a server-side proxy:

// src/app/api/github/route.ts — runs on the server, token stays secret
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const endpoint = request.nextUrl.searchParams.get("endpoint");

  const res = await fetch(`https://api.github.com${endpoint}`, {
    headers: {
      Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, // server-only!
      Accept: "application/vnd.github.v3+json",
    },
  });

  const data = await res.json();
  return NextResponse.json(data);
}

Then the client hook calls /api/github?endpoint=/users/username — it never sees the token.

Filtering and sorting repos

// src/lib/github.ts
export async function getGitHubRepos(username: string): Promise<GitHubRepo[]> {
  const res = await fetch(
    `https://api.github.com/users/${username}/repos?sort=updated&per_page=20`,
    { next: { revalidate: 3600 } }
  );
  const repos: GitHubRepo[] = await res.json();

  return repos
    .filter((r) => !r.name.includes("fork"))          // no forks
    .sort((a, b) => b.stargazers_count - a.stargazers_count) // best first
    .slice(0, 6);                                      // top 6 only
}

🌍 Trilingual Support (PT / EN / ES)

This was something I added because I want to reach developers from different countries and also practice thinking in English and Spanish. The approach was simple: one big translations object and a React context.

// src/data/i18n.ts (simplified)
export const translations = {
  hero: {
    available: {
      pt: "disponível para oportunidades",
      en: "open to opportunities",
      es: "disponible para oportunidades",
    },
    // ...
  },
  // all sections follow the same pattern
} as const;

// src/data/LangContext.tsx
export function LangProvider({ children }: { children: React.ReactNode }) {
  const [lang, setLangState] = useState<Lang>("pt");

  useEffect(() => {
    const saved = localStorage.getItem("lang") as Lang | null;
    if (saved) setLangState(saved);
  }, []);

  const setLang = (l: Lang) => {
    setLangState(l);
    localStorage.setItem("lang", l); // persists across refreshes
  };

  return <LangCtx.Provider value={{ lang, setLang }}>{children}</LangCtx.Provider>;
}

Then in any component:

const { lang } = useLang();
const h = translations.hero;

<p>{h.available[lang]}</p> // renders in current language

The navbar has a dropdown with flag emojis 🇧🇷 🇺🇸 🇪🇸 — switching language re-renders everything instantly.

🤖 The Chat Assistant

!(You can apply chatting with Google AI Studio in the future)!
This was the most fun part to build. The idea came from this post by @aldwin160 — using an AI chat as a creative way for recruiters to learn about you.
Current state: static responses
I integrated Google Gemini but hit the free tier quota limit (limit: 0 error — the key was linked to a project without the free quota). Rather than block the feature entirely, I built a static response engine that covers the most common recruiter questions:

// src/data/staticChat.ts
const STATIC_RESPONSES = [
  {
    keywords: ["stack", "technology", "tecnologia", "tecnología"],
    answer: {
      pt: "Minha stack principal é **Next.js**, **React**, **TypeScript**...",
      en: "My main stack is **Next.js**, **React**, **TypeScript**...",
      es: "Mi stack principal es **Next.js**, **React**, **TypeScript**...",
    },
  },
  // available, projects, experience, contact, learning...
];

export function getStaticReply(input: string, lang: Lang): string {
  const lower = input.toLowerCase();
  for (const item of STATIC_RESPONSES) {
    if (item.keywords.some((kw) => lower.includes(kw))) {
      return item.answer[lang];
    }
  }
  return FALLBACK[lang]; // fallback: "contact me directly!"
}

Gemini AI — ready to activate
The full Gemini integration is built and commented in ChatWidget.tsx. When I get a proper API key it’s literally uncommenting one function:

// src/app/api/chat/route.ts — server-side to avoid CORS
export async function POST(request: NextRequest) {
  const { messages, systemPrompt } = await request.json();
  const key = process.env.NEXT_PUBLIC_GEMINI_KEY;

  const res = await fetch(
    `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        system_instruction: { parts: [{ text: systemPrompt }] },
        contents: messages,
      }),
    }
  );

  const data = await res.json();
  const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? "No response.";
  return NextResponse.json({ text });
}

Important: The Gemini call goes through a Next.js API route, not directly from the browser. This avoids CORS errors and keeps the key server-side.

UX detail: the menu system
After every response, the chat shows the 3 quick-question buttons again plus a "↩ Back to main menu" button. This keeps the conversation flowing even for non-technical visitors who might not know what to ask:

// After each reply, showMenu state triggers the suggestion buttons
const send = (text: string) => {
  const reply = getStaticReply(text, lang);
  setMsgs(p => [...p,
    { role: "user",  content: text  },
    { role: "model", content: reply },
  ]);
  setShowMenu(true); // always show menu after response
};

⚡ Animation Presets with Framer Motion

typescript
Instead of writing the same animation object in every component, I extracted presets:

// src/lib/motion.ts
import type { Transition } from "framer-motion";

export const fadeInUp = {
  initial:     { opacity: 0, y: 24 },
  whileInView: { opacity: 1, y: 0  },
  viewport:    { once: true },
  transition:  { duration: 0.5, ease: "easeOut" } as Transition,
};

export const stagger = (i: number) => ({
  initial:     { opacity: 0, y: 20 },
  whileInView: { opacity: 1, y: 0  },
  viewport:    { once: true },
  transition:  { duration: 0.4, delay: i * 0.08, ease: "easeOut" } as Transition,
});

// Float animation for the chat FAB button
export const floatAnimate = {
  y: [0, -8, 0],
  transition: { duration: 3, repeat: Infinity, ease: "easeInOut" } as Transition,
};

💡 Things I Learned (the hard way)

  1. "use client" is not optional for interactive components
    Next.js App Router defaults to Server Components. Any component using useState, useEffect, onMouseEnter etc. needs "use client" at the top. I spent an embarrassing amount of time debugging this error:

    Error: Event handlers cannot be passed to Client Component props.

  2. Never call external APIs directly from the browser
    The Gemini API blocks browser requests with CORS. The GitHub token would be visible in network tab. Always proxy through a Next.js API route.

  3. Type your API responses, don’t use any

// ❌ This will bite you later
const posts: any[] = await res.json();

// ✅ This catches bugs immediately
interface DevToApiPost { id: number; title: string; tag_list: string[]; ... }
const posts: DevToApiPost[] = await res.json();
  1. CSS Variables beat Tailwind for theming
    For dark/light mode with dynamic values, CSS variables are much cleaner than Tailwind’s dark: classes:
:root              { --accent: #16a34a; --bg: #f4f4f8; }
[data-theme="dark"]{ --accent: #7fffb2; --bg: #090910; }

Then style={{ color: "var(--accent)" }} works everywhere, no config needed.

  1. The {app mystery folder
    At one point I ended up with a literal folder named {app in my project because of a bad bash command. Always double-check your shell scripts.
  • 🌐 Portfolio: portfoliodevheron.xyz
  • 🐙 GitHub repo: github.com/devheron/portfolioFull
  • 💬 Inspiration: Portfolio with Gemini by @aldwin160
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Competitive Intelligence Certified: Masters in-person

Next Post

Lovable says it added $100M in revenue last month alone, with just 146 employees

Related Posts
鸿蒙next应用国际化:时间与日期格式化

鸿蒙Next应用国际化:时间与日期格式化

本文旨在深入探讨华为鸿蒙HarmonyOS Next系统(截止目前API12)在应用国际化中时间与日期格式化方面的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。 在全球化的应用场景中,正确处理时间与日期的格式化是提供优质用户体验的关键因素之一。不同地区和语言对于时间与日期的表示方式存在显著差异,鸿蒙Next系统提供了丰富的功能来满足这种多样化的需求。本文将详细介绍时间日期格式化选项、相对时间格式化、时间段格式化,以及常见时间日期格式化问题及解决方案,抛砖引玉。 一、时间日期格式化选项 (一)日期显示格式(dateStyle) 格式取值与示例 full:显示完整的日期信息,包括年、月、日、星期。例如,在中文环境下可能显示为“2023年10月15日 星期日”。 long:显示较为详细的日期,通常包含年、月、日和星期的缩写。如“2023年10月15日 周日”。 medium:显示适中的日期格式,一般有年、月、日。例如“2023-10-15”。 short:显示简洁的日期,可能只包含月、日和年的部分信息。比如“10/15/23”(在某些地区格式)。 根据区域和语言选择格式 开发者可以使用 DateTimeFormat 类,根据用户所在区域的语言和文化习惯选择合适的 dateStyle 进行日期格式化。例如:…
Read More