<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Heron Developer</title>
    <description>The latest articles on Forem by Heron Developer (@heronfelipe).</description>
    <link>https://forem.com/heronfelipe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3816887%2F221500d0-059e-463a-b7e6-f6455850f7a4.png</url>
      <title>Forem: Heron Developer</title>
      <link>https://forem.com/heronfelipe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/heronfelipe"/>
    <language>en</language>
    <item>
      <title>NutriTrack — How I Built a Full-Stack Nutrition Tracker with React, Supabase &amp; Real Food Data</title>
      <dc:creator>Heron Developer</dc:creator>
      <pubDate>Fri, 13 Mar 2026 17:19:24 +0000</pubDate>
      <link>https://forem.com/heronfelipe/nutritrack-how-i-built-a-full-stack-nutrition-tracker-with-react-supabase-real-food-data-k7g</link>
      <guid>https://forem.com/heronfelipe/nutritrack-how-i-built-a-full-stack-nutrition-tracker-with-react-supabase-real-food-data-k7g</guid>
      <description>&lt;h2&gt;
  
  
  👤 Who am I?
&lt;/h2&gt;

&lt;p&gt;I'm a self-taught developer from Brazil, still learning and building things that I actually want to use. I've been studying web development for a while and decided it was time to build something more than a to-do list.&lt;br&gt;
NutriTrack started as a weekend side project with one goal: stop guessing what I was eating. I wanted to know exactly how many calories and how much protein was in my daily meals — without paying for a subscription app, without dealing with a bloated interface, and without manually searching for every single food item.&lt;br&gt;
So I built it. From scratch. With the stack I was learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem I Was Trying to Solve 💭
&lt;/h2&gt;

&lt;p&gt;If you've ever tried to track your nutrition seriously, you know the pain. Most popular apps like MyFitnessPal are either behind a paywall for the features that actually matter, or they have a database focused entirely on American/European products — leaving Brazilian foods either missing or wrong. &lt;br&gt;
I wanted something that:&lt;br&gt;
• Works with real Brazilian supermarket products&lt;br&gt;
• Lets me add custom foods manually when the database doesn't have what I need&lt;br&gt;
• Shows my daily progress clearly — calories in vs. my goal, protein in vs. my goal&lt;br&gt;
• Lets me plan meals ahead of time, not just log them after eating&lt;br&gt;
• Doesn't require me to carry my phone into a grocery store just to scan a barcode&lt;/p&gt;

&lt;p&gt;That's NutriTrack. A nutrition control system designed around how Brazilians actually eat and shop — but flexible enough to work anywhere in the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is NutriTrack? 📊
&lt;/h2&gt;

&lt;p&gt;NutriTrack is a web application where you build your personal food bank, log what you eat each day, set daily nutrition goals, and schedule your meals for the next two weeks. It's connected to a real food product database so you don't have to type nutritional values by hand for every item. &lt;br&gt;
Think of it as your personal nutrition control panel, not a social app, not a fitness tracker, not a recipe generator. Just clean data about what you're putting in your body — and how it compares to what you set as your target.&lt;br&gt;
The core idea: if you can see exactly what you're eating and compare it against a goal you set yourself, you naturally start making better choices. No coach needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features — and why each one matters
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Social Login (Google &amp;amp; GitHub)
&lt;/h2&gt;

&lt;p&gt;The first decision was: no passwords. Nobody wants to create yet another account with yet another password to remember. With Supabase Auth, I plugged in Google and GitHub OAuth in about an hour. 🔐&lt;br&gt;
Every user gets a unique account. All your food data, your daily logs, your goals — completely isolated from everyone else. If someone else signs up with a different email, they see absolutely nothing of yours. This is enforced at the database level using Row Level Security (RLS) in PostgreSQL, not just in the frontend code.&lt;br&gt;
One interesting behavior worth knowing: if you log in with both Google and GitHub using the same email address, Supabase treats them as the same user — which is actually correct. You're the same person. Your data follows you regardless of how you log in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3p8lxw9546l74g1mcjoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3p8lxw9546l74g1mcjoo.png" alt=" " width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Personal Food Bank
&lt;/h2&gt;

&lt;p&gt;This is the foundation of the whole app. Before you can log anything, you build your personal food bank — a list of all the foods you regularly eat. Think of it like your pantry catalog. 🥗&lt;br&gt;
For each food item you can store:&lt;br&gt;
• Name and brand&lt;br&gt;
• Calories, protein, carbohydrates and fat per serving&lt;br&gt;
• Serving size and unit (grams, ml, units, tablespoons — whatever makes sense)&lt;br&gt;
• A product photo (uploaded directly to Supabase Storage, or imported automatically)&lt;/p&gt;

&lt;p&gt;The key feature here is the Open Food Facts integration. Instead of manually typing the nutritional table from the back of a package, you search the product name and the app hits the Open Food Facts API — a crowdsourced database with over 3 million products worldwide, including a strong Brazilian catalog.&lt;br&gt;
Search for 'Feijão Kicaldo', 'Aveia Quaker', 'Whey Gold Standard', or any product you find at Pão de Açúcar, Carrefour or Atacadão — and it comes back with the full nutritional info and even the product photo. One click and it's in your food bank.&lt;br&gt;
For foods that aren't in the database (homemade meals, restaurant dishes, that specific açaí bowl you get every Sunday), you can add them manually with your own estimated values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0r1rjns5m57cdfa4ggy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0r1rjns5m57cdfa4ggy.png" alt=" " width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Daily Plan — Your Nutritional Diary
&lt;/h2&gt;

&lt;p&gt;Every day you log what you actually ate. You pick a food from your food bank, select the meal type, enter the quantity in grams (or whatever unit you set), and the app calculates everything instantly. 📅&lt;br&gt;
Meals are organized into categories:&lt;br&gt;
• Café da Manhã (Breakfast)&lt;br&gt;
• Almoço (Lunch)&lt;br&gt;
• Lanche (Snack)&lt;br&gt;
• Jantar (Dinner)&lt;br&gt;
• Pré-treino (Pre-workout)&lt;br&gt;
• Pós-treino (Post-workout)&lt;/p&gt;

&lt;p&gt;At the top of the page you see your daily totals in real-time: total calories consumed, total protein consumed, and a progress bar showing how far you are from your personal goal. If your goal is 2,500 kcal and you've eaten 1,800, the bar shows you're at 72%.&lt;br&gt;
One technical decision I'm proud of: when you log a meal, the app saves a nutritional snapshot at that moment. This means if you later edit the food item (say, you got a more accurate calorie count for that food), your past logs are not affected. Your history is always accurate to what you actually tracked at that time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapn9rpbhs4xsfzahhnll.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapn9rpbhs4xsfzahhnll.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  14-Day Scheduling
&lt;/h2&gt;

&lt;p&gt;Beyond logging what you already ate, NutriTrack lets you plan ahead. The scheduling page shows a 14-day grid where you can build complete meal plans for future days. 📆&lt;br&gt;
Each day card shows the planned total calories and protein at a glance. Open a day and you see every meal broken down: what's in breakfast, what's in lunch, how many calories each section adds up to.&lt;br&gt;
The killer feature: the "Apply to Today" button. Built your perfect Monday meal plan? Hit that button and all those meals are instantly added to today's daily log. No re-entering anything. Your plan becomes your diary.&lt;br&gt;
This is especially useful if you eat similar things on a routine — athletes, people doing a cut, anyone with a structured nutrition protocol.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pvfj5qbjnzzi9kqojbv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pvfj5qbjnzzi9kqojbv.png" alt=" " width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Personalized Daily Goals
&lt;/h2&gt;

&lt;p&gt;Not everyone has the same needs. A 70kg person trying to maintain weight has different calorie targets than a 90kg person trying to build muscle. NutriTrack doesn't impose default values — you set your own targets. 🎯&lt;br&gt;
You define your daily goals for:&lt;br&gt;
• Total calories (kcal)&lt;br&gt;
• Total protein (grams)&lt;/p&gt;

&lt;p&gt;The daily plan page then shows your progress as a percentage of those targets. It's simple but it changes how you interact with food — you stop thinking in absolute numbers and start thinking proportionally. 'I'm at 60% of calories but only 35% of protein — I need a higher-protein dinner.'&lt;/p&gt;

&lt;h2&gt;
  
  
  Nearby Stores Map
&lt;/h2&gt;

&lt;p&gt;A bonus feature that actually comes in handy: find supermarkets and pharmacies near your current location. This uses GPS from your browser, the OpenStreetMap database, and the Overpass API — completely free, no API key needed. 🗺️&lt;br&gt;
You can filter by store type (supermarket vs. pharmacy), radius (from 500m up to 60km), country, and city. The country and city filters are smart: they detect location by GPS coordinates, not by street name — which matters a lot in border regions. The app correctly separates cities if they border other countries ones, even though they're all within a few kilometers of each other.&lt;br&gt;
The city filter also normalizes variations: 'City Two', 'City two', 'city Two' all collapse into a single filter option showing the most common written form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack — and why I chose each piece
&lt;/h2&gt;

&lt;p&gt;Technology /    Role /  Why this over alternatives 🛠️&lt;br&gt;
React 18 + Vite Frontend SPA /  Instant HMR, zero config, I know the ecosystem&lt;br&gt;
Tailwind CSS /  Styling CSS variables for theming, no class conflicts&lt;br&gt;
Supabase /  Backend + Auth + DB + Storage   PostgreSQL + OAuth + file storage in one free tier&lt;br&gt;
React Router v6 Client-side routing /   Declarative routes, no page reloads&lt;br&gt;
Open Food Facts API /   Food product database / Free, 3M+ products, strong Brazilian catalog&lt;br&gt;
OpenStreetMap / Overpass /  Nearby stores map / Completely free, no API key, global coverage&lt;/p&gt;

&lt;p&gt;The most important choice was Supabase. I initially considered Firebase, but the fact that Supabase uses real PostgreSQL was a dealbreaker in its favor. I wanted proper relational tables, real SQL queries, and Row Level Security — none of which Firebase offers in a clean way. The free tier is generous enough for a personal project, and the dashboard is one of the best developer experiences I've used.&lt;br&gt;
For the frontend build tool, Vite over Create React App was a no-brainer in 2024. CRA is essentially deprecated. Vite's startup and HMR speeds make development feel instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Architecture Works
&lt;/h2&gt;

&lt;p&gt;The db.js layer — single source of truth for database access 🏗️&lt;br&gt;
Every single database operation goes through a central file called db.js. This file has one job: get the current user's ID from the active session and attach it to every query.&lt;br&gt;
Why does this matter? Early in development I had a bug where data from one account would bleed into another account's view when switching sessions. The root cause was that some queries were built when the component mounted — capturing the old user ID in a closure — and not re-evaluated when the auth state changed.&lt;br&gt;
The fix was to make every query call getUid() at execution time, not at component mount time. Now it's impossible to accidentally query another user's data because the user ID is always fetched fresh from the active session right before the database call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Row Level Security — the real security layer
&lt;/h2&gt;

&lt;p&gt;Even though the frontend always filters by user_id, the real security lives in the database. Every table in Supabase has RLS policies that look like this:&lt;br&gt;
SELECT: only return rows where auth.uid() = user_id INSERT: only allow if auth.uid() = user_id UPDATE: only allow if auth.uid() = user_id DELETE: only allow if auth.uid() = user_id&lt;br&gt;
This means even if someone bypasses the frontend completely and hits the Supabase API directly with a crafted request, they cannot read, write, or delete another user's data. The database itself rejects it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nutritional snapshots — immutable history
&lt;/h2&gt;

&lt;p&gt;When you log '150g of chicken breast' on a Tuesday, the app saves the nutritional values as they were at that moment: snap_calories, snap_protein, snap_carbs, snap_fat. These are stored directly in the log row.&lt;br&gt;
Later, if you realize the chicken breast entry had the wrong protein value and you update it, your Tuesday log is not affected. Your historical data is always accurate to what you actually tracked at that time. This is especially important for people tracking over months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I Learned Building This
&lt;/h2&gt;

&lt;p&gt;The PKCE vs Implicit flow OAuth bug 💡&lt;br&gt;
When I first set up Google and GitHub login, I got mysterious 401 errors after the OAuth redirect. The issue: Supabase's default PKCE flow generates a code_verifier and stores it in localStorage before the redirect. But in localhost development, that storage gets wiped during the OAuth redirect, so when the app tries to exchange the code for a token, the verifier is gone.&lt;br&gt;
The fix was switching to implicit flow in the Supabase client config. Not ideal for production at scale, but perfectly fine for this use case — and it made OAuth work reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  Account linking by email
&lt;/h3&gt;

&lt;p&gt;Supabase (and most OAuth systems) link accounts by email. If your Google and GitHub accounts share the same email address, they're treated as the same person — same user_id, same data. This is the correct behavior from a UX perspective: you don't want to lose access to your data just because you chose a different login button.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenStreetMap data is collaborative — and messy
&lt;/h3&gt;

&lt;p&gt;The nearby stores feature uses OpenStreetMap via the Overpass API. OSM data is crowdsourced, which means it's incredibly broad but not always accurate. A hotel that sells groceries might be tagged as shop=convenience. A car wash near food stalls might appear in results.&lt;br&gt;
I built a filtering layer with a blacklist of OSM tags (tourism, leisure, historic, natural) and a keyword blacklist for store names (hotel, hostel, pet shop, bazar, etc.) to remove clearly wrong results. It's not perfect — OSM data will always have noise — but it's much cleaner than raw results.&lt;/p&gt;

&lt;h3&gt;
  
  
  City name normalization
&lt;/h3&gt;

&lt;p&gt;The city filter for nearby stores receives raw strings from OpenStreetMap: 'City Two', 'city two', 'city Two', 'CITY TWO' — all referring to the same city. Without normalization, the filter showed the same city four or five times.&lt;br&gt;
The solution: normalize everything to lowercase without accents as the grouping key, but display the most frequent raw variant as the label. So the user sees 'City Two once in the filter, but clicking it matches all stores regardless of how their city was typed in OSM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this works specifically for Brazil  🇧🇷
&lt;/h3&gt;

&lt;p&gt;Brazilian nutrition apps have historically been either underfunded local products or translations of American apps that don't know what 'pão de queijo' or 'cuscuz nordestino' is.&lt;br&gt;
NutriTrack hits the Open Food Facts API which has a surprisingly solid Brazilian product catalog, major supermarket brands like Kicaldo, Quaker BR, Olvebra, Nestlé BR, and hundreds of regional brands are all there with correct nutritional values in the Brazilian format (kcal per 100g, as required by ANVISA labeling).&lt;br&gt;
For foods that aren't in the database, the manual entry is designed to be fast. You don't need to fill in 20 fields. Name, serving size, calories, protein — that's enough to get going. Carbs and fat are optional.&lt;br&gt;
The nearby stores feature knows about the Brazilian territory, app interface is in Portuguese, all meal names, labels, buttons, and messages are localized for a Brazilian audience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Structure (4 tables, all with RLS)
&lt;/h2&gt;

&lt;p&gt;Table / What it stores  Key design decision 🗄️&lt;br&gt;
food_items /    Your personal food catalog /    One row per food. All macros stored per serving. Photo URL optional.&lt;br&gt;
daily_logs /    What you ate each day / Nutritional snapshot columns / freeze values at log time. Logs never change when you edit the food.&lt;br&gt;
scheduled_plans /   Meal plans for future dates /   References food_items by ID. When plan is applied, it creates daily_log rows with fresh snapshots.&lt;br&gt;
user_goals /    Your personal daily targets /   One row per user. Upserted on save. Defaults to 2500 kcal / 150g protein if not set.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project is Open Source
&lt;/h2&gt;

&lt;p&gt;NutriTrack is fully open source under the MIT license. If you want to run it yourself, everything is in the repo — including the full SQL schema to set up the Supabase database, a .env.example with the required variables, and step-by-step setup instructions in the README. 🔗&lt;br&gt;
It runs locally with npm install &amp;amp;&amp;amp; npm run dev. Deploy to Vercel takes about 5 minutes: connect the repo, add the two environment variables (Supabase URL and anon key), and it's live.&lt;br&gt;
One important Vercel step most tutorials skip: after deploying, go back to Supabase → Authentication → URL Configuration and add your Vercel URL to the Redirect URLs list. Without this, OAuth login will fail in production.&lt;br&gt;
If you find it useful, build something on top of it, or spot something that could be better — pull requests are welcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer
&lt;/h3&gt;

&lt;h4&gt;
  
  
  🐙 github.com/devheron/NutriTrack
&lt;/h4&gt;

&lt;h4&gt;
  
  
  🌐 &lt;a href="https://nutri-track-seven.vercel.app/" rel="noopener noreferrer"&gt;https://nutri-track-seven.vercel.app/&lt;/a&gt;
&lt;/h4&gt;

</description>
      <category>react</category>
      <category>supabase</category>
      <category>nutrition</category>
      <category>braziliandevs</category>
    </item>
    <item>
      <title>🚀 How I Built My Developer Portfolio with Next.js, GitHub API, DEV.to API + Trilingual Support and AI Chat in future</title>
      <dc:creator>Heron Developer</dc:creator>
      <pubDate>Wed, 11 Mar 2026 21:05:35 +0000</pubDate>
      <link>https://forem.com/heronfelipe/how-i-built-my-developer-portfolio-with-nextjs-github-api-devto-api-trilingual-support-and-42j2</link>
      <guid>https://forem.com/heronfelipe/how-i-built-my-developer-portfolio-with-nextjs-github-api-devto-api-trilingual-support-and-42j2</guid>
      <description>&lt;h2&gt;
  
  
  Who's me?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Inspired by: &lt;a href="https://dev.to/aldwin160/portfolio-with-gemini-simple-smooth-subtle-469m"&gt;Portfolio with Gemini — Simple, Smooth, Subtle&lt;/a&gt; by &lt;a class="mentioned-user" href="https://dev.to/aldwin160"&gt;@aldwin160&lt;/a&gt;. That post showed me how to approach the AI chat feature with a good system prompt. Highly recommend reading it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why I built this?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;p&gt;Before diving into the code, here's what the final portfolio does:&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Tool and Why&lt;/p&gt;
&lt;/blockquote&gt;

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

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

&lt;p&gt;The separation makes sense once you understand the roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sections/&lt;/code&gt; — page UI, one component per visual section&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data/&lt;/code&gt; — your content and translations, no UI logic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/&lt;/code&gt; — pure functions that talk to external APIs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hooks/&lt;/code&gt; — client-side data fetching with loading/error states&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api/&lt;/code&gt; — server-side routes that hide secret tokens from the browser&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  🐙 Integrating GitHub &amp;amp; DEV.to APIs
&lt;/h2&gt;

&lt;p&gt;DEV.to — Zero config&lt;br&gt;
The DEV.to API is completely public for reading articles. No token, no account, just fetch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/devto.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDevToPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DevToPost&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://dev.to/api/articles?username=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;per_page=10`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Next.js cache: 30min&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DevToApiPost&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;DevToPost&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag_list&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... rest of mapping&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub — Hiding the token&lt;br&gt;
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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/api/github/route.ts — runs on the server, token stays secret&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;endpoint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.github.com&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// server-only!&lt;/span&gt;
      &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.github.v3+json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Then the client hook calls &lt;code&gt;/api/github?endpoint=/users/username&lt;/code&gt; — it never sees the token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Filtering and sorting repos&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/github.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getGitHubRepos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.github.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/repos?sort=updated&amp;amp;per_page=20`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;repos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fork&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="c1"&gt;// no forks&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stargazers_count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stargazers_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// best first&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                                      &lt;span class="c1"&gt;// top 6 only&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  🌍 Trilingual Support (PT / EN / ES)
&lt;/h1&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/data/i18n.ts (simplified)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;translations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disponível para oportunidades&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open to opportunities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disponible para oportunidades&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// all sections follow the same pattern&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;--&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/data/LangContext.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LangProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLangState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Lang&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Lang&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setLangState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLangState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// persists across refreshes&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LangCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLang&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/LangCtx.Provider&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in any component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLang&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt; /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;renders&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The navbar has a dropdown with flag emojis 🇧🇷 🇺🇸 🇪🇸 — switching language re-renders everything instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 The Chat Assistant
&lt;/h2&gt;

&lt;p&gt;!(You can apply chatting with Google AI Studio in the future)!&lt;br&gt;
This was the most fun part to build. The idea came from this post by &lt;a class="mentioned-user" href="https://dev.to/aldwin160"&gt;@aldwin160&lt;/a&gt; — using an AI chat as a creative way for recruiters to learn about you.&lt;br&gt;
Current state: static responses&lt;br&gt;
I integrated Google Gemini but hit the free tier quota limit (&lt;code&gt;limit: 0&lt;/code&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/data/staticChat.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STATIC_RESPONSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;technology&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tecnologia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tecnología&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Minha stack principal é **Next.js**, **React**, **TypeScript**...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My main stack is **Next.js**, **React**, **TypeScript**...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mi stack principal es **Next.js**, **React**, **TypeScript**...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// available, projects, experience, contact, learning...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStaticReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;STATIC_RESPONSES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// fallback: "contact me directly!"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemini AI — ready to activate&lt;br&gt;
The full Gemini integration is built and commented in &lt;code&gt;ChatWidget.tsx&lt;/code&gt;. When I get a proper API key it's literally uncommenting one function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/api/chat/route.ts — server-side to avoid CORS&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_GEMINI_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;system_instruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No response.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After each reply, showMenu state triggers the suggestion buttons&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getStaticReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setMsgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nf"&gt;setShowMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// always show menu after response&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚡ Animation Presets with Framer Motion
&lt;/h2&gt;

&lt;p&gt;typescript&lt;br&gt;
Instead of writing the same animation object in every component, I extracted presets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/motion.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;framer-motion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fadeInUp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;whileInView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easeOut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stagger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;whileInView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easeOut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Float animation for the chat FAB button&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floatAnimate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easeInOut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 Things I Learned (the hard way)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;"use client"&lt;/code&gt; is not optional for interactive components&lt;br&gt;
Next.js App Router defaults to Server Components. Any component using &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;onMouseEnter&lt;/code&gt; etc. needs &lt;code&gt;"use client"&lt;/code&gt; at the top. I spent an embarrassing amount of time debugging this error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Error: Event handlers cannot be passed to Client Component props.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Never call external APIs directly from the browser&lt;br&gt;
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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Type your API responses, don't use &lt;code&gt;any&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ This will bite you later&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ This catches bugs immediately&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DevToApiPost&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;tag_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DevToApiPost&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;CSS Variables beat Tailwind for theming
For dark/light mode with dynamic values, CSS variables are much cleaner than Tailwind's &lt;code&gt;dark:&lt;/code&gt; classes:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt;              &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#16a34a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f4f4f8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#7fffb2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#090910&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;style={{ color: "var(--accent)" }}&lt;/code&gt; works everywhere, no config needed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;{app&lt;/code&gt; mystery folder
At one point I ended up with a literal folder named &lt;code&gt;{app&lt;/code&gt; in my project because of a bad bash command. Always double-check your shell scripts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🔗 Final links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Portfolio: portfoliodevheron.xyz&lt;/li&gt;
&lt;li&gt;🐙 GitHub repo: github.com/devheron/portfolioFull&lt;/li&gt;
&lt;li&gt;💬 Inspiration: Portfolio with Gemini by &lt;a class="mentioned-user" href="https://dev.to/aldwin160"&gt;@aldwin160&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>portfolio</category>
      <category>braziliandevs</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
  </channel>
</rss>
