feat: add project detail page with dynamic content and navigation

This commit is contained in:
2025-08-29 01:30:58 -03:00
parent 395d0af36f
commit 3979899efb
25 changed files with 177 additions and 685 deletions

View File

@@ -1,10 +1,9 @@
import { Inter, Space_Grotesk } from "next/font/google";
import Header from "@/app/components/Header"; // Usando o Header que forneci
import Header from "@/app/components/Header";
import Footer from "@/app/components/Footer";
import "@/app/globals.css"; // Importando o CSS global
import "@/app/globals.css";
import { Toaster } from 'react-hot-toast';
// Importações importantes do next-intl
import {hasLocale, Locale, NextIntlClientProvider} from 'next-intl';
import { ThemeProvider } from "@/configuration/ThemeContext";
import {getTranslations, setRequestLocale} from 'next-intl/server';
@@ -18,12 +17,6 @@ const inter = Inter({
variable: '--font-inter',
});
const space_grotesk = Space_Grotesk({
subsets: ["latin"],
weight: ['300', '400', '500', '700'],
variable: '--font-space-grotesk',
});
type Props = {
children: ReactNode;
params: Promise<{locale: Locale}>;
@@ -52,37 +45,34 @@ export default async function RootLayout({children, params}: Props) {
setRequestLocale(locale);
return (
<html lang={locale} className={`${inter.variable} ${space_grotesk.variable} scroll-smooth`}>
<body className={`${inter.className} bg-background text-text-primary`}>
<html lang={locale} className={`${inter.variable} scroll-smooth`}>
<body className={`${inter.className} bg-background text-primary flex flex-col min-h-screen`}>
<ThemeProvider>
<NextIntlClientProvider locale={locale}>
<FloatingSocials />
<Header />
<main className="flex flex-col">
<main className="flex flex-col flex-grow">
{children}
</main>
<Footer />
<Toaster
position="bottom-right"
toastOptions={{
// General styles for all toasts
style: {
background: 'var(--color-card)', //
color: 'var(--color-text-primary)', //
border: '1px solid var(--color-border)', //
background: 'var(--color-card)',
color: 'var(--color-text-primary)',
border: '1px solid var(--color-border)',
},
// Specific styles for success toasts
success: {
iconTheme: {
primary: 'var(--color-primary)', //
secondary: 'var(--color-card)', //
primary: 'var(--color-primary)',
secondary: 'var(--color-card)',
},
},
// Specific styles for error toasts
error: {
iconTheme: {
primary: '#ef4444', // A standard red color for errors
secondary: 'var(--color-card)', //
primary: '#ef4444',
secondary: 'var(--color-card)',
},
},
}}

View File

@@ -0,0 +1,64 @@
'use client';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import { FaArrowLeft } from 'react-icons/fa6';
import Link from 'next/link';
const projectsData = [
{ id: 1, tech: ["Next.js", "Tailwind CSS", "TypeScript", "Framer Motion"], imageUrl: "/project1.jpg", repoUrl: "https://github.com/joaoloureiro/portfolio-app" },
{ id: 2, tech: ["Traefik", "Docker", "Linux", "Homelab"], imageUrl: "/project2.jpg" },
{ id: 3, tech: ["React", "TypeScript", "Styled-Components"], imageUrl: "/project3.jpg", liveUrl: "https://happy.joaoloureiro.dev.br/" }
];
export default function ProjectPage() {
const t = useTranslations('projects');
const { id } = useParams();
const projectId = parseInt(id as string, 10);
const project = projectsData.find(p => p.id === projectId);
if (!project) {
return <div>Project not found</div>;
}
return (
<section className="py-12 md:py-20">
<div className="container mx-auto px-4">
<Link href="/#projects" className="flex items-center gap-2 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors mb-8">
<FaArrowLeft />
{t('back_to_projects')}
</Link>
<h1 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] heading-underline">{t(`project_${project.id}_title`)}</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mt-12">
<div>
<img src={project.imageUrl} alt={t(`project_${project.id}_title`)} className="rounded-lg" />
</div>
<div>
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">{t('about_project')}</h2>
<p className="text-[var(--color-text-secondary)] mb-4">{t(`project_${project.id}_description`)}</p>
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-2">{t('tech_used')}</h3>
<div className="flex flex-wrap gap-2 mb-4">
{project.tech.map(t => (
<span key={t} className="bg-[var(--color-border)]/50 text-[var(--color-text-secondary)] text-xs font-semibold px-2.5 py-1 rounded-full">
{t}
</span>
))}
</div>
<div className="flex items-center space-x-8 mt-auto pt-4 border-t border-[var(--color-border)]">
{project.liveUrl && (
<Link href={project.liveUrl} target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
{t('live_link')}
</Link>
)}
{project.repoUrl && (
<Link href={project.repoUrl} target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
{t('repo_link')}
</Link>
)}
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -19,7 +19,7 @@ export default function FloatingSocials() {
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
className="p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--color-background)] rounded-md transition-colors"
className="p-2 text-[var(--color-text-primary)] hover:text-[var(--color-primary)] hover:bg-[var(--color-background)] rounded-md transition-colors"
>
<div className="h-4 w-4">{link.icon}</div>
</Link>

View File

@@ -17,9 +17,9 @@ export default function Header() {
<Image src="/logo.png" alt="João Loureiro Logo" width={40} height={40} priority />
</Link>
<div className="hidden md:flex items-center space-x-12 text-[var(--color-text-secondary)]">
<Link href="#about" className="hover:text-[var(--color-primary)] transition-colors">{t('about')}</Link>
<Link href="#projects" className="hover:text-[var(--color-primary)] transition-colors">{t('projects')}</Link>
<Link href="#contact" className="hover:text-[var(--color-primary)] transition-colors">{t('contact')}</Link>
<Link href="#about" className="text-[var(--color-text-primary)] hover:text-[var(--color-primary)] transition-colors">{t('about')}</Link>
<Link href="#projects" className="text-[var(--color-text-primary)] hover:text-[var(--color-primary)] transition-colors">{t('projects')}</Link>
<Link href="#contact" className="text-[var(--color-text-primary)] hover:text-[var(--color-primary)] transition-colors">{t('contact')}</Link>
</div>

View File

@@ -4,6 +4,7 @@ import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';
import { useState, useTransition, useRef, useEffect } from 'react';
import { FaChevronDown } from 'react-icons/fa6';
import ReactCountryFlag from 'react-country-flag';
export default function LanguageSwitcher() {
const [isPending, startTransition] = useTransition();
@@ -32,9 +33,9 @@ export default function LanguageSwitcher() {
return () => document.removeEventListener('mousedown', handleOutsideClick);
}, []);
const languages: { [key: string]: { flag: string; label: string } } = {
en: { flag: '🇺🇸', label: 'English' },
pt: { flag: '🇧🇷', label: 'Português' },
const languages: { [key: string]: { label: string; countryCode: string } } = {
en: { label: 'English', countryCode: 'US' },
pt: { label: 'Português', countryCode: 'BR' },
};
return (
@@ -43,10 +44,9 @@ export default function LanguageSwitcher() {
type="button"
disabled={isPending}
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between w-full px-3 py-2 text-sm font-medium text-[var(--color-text-secondary)] bg-[var(--color-card)] border border-[var(--color-border)] rounded-md hover:bg-[var(--color-border)]/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-[var(--color-background)] focus:ring-[var(--color-primary)] transition-colors"
className="flex items-center justify-center w-10 h-10 text-sm font-medium text-[var(--color-text-secondary)] bg-[var(--color-card)] border border-[var(--color-border)] rounded-md hover:bg-[var(--color-border)]/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-[var(--color-background)] focus:ring-[var(--color-primary)] transition-colors"
>
<span>{languages[locale].flag} {languages[locale].label}</span>
<FaChevronDown className={`ml-2 h-4 w-4 transition-transform ${isOpen ? 'transform rotate-180' : ''}`} />
<ReactCountryFlag countryCode={languages[locale].countryCode} svg style={{ width: '24px', height: '24px', borderRadius: '20%' }} />
</button>
{isOpen && (
@@ -58,7 +58,7 @@ export default function LanguageSwitcher() {
onClick={() => onSelectChange(langCode)}
className="flex items-center w-full px-4 py-2 text-sm text-left text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]/50"
>
<span className="mr-2">{languages[langCode].flag}</span>
<ReactCountryFlag countryCode={languages[langCode].countryCode} svg style={{ width: '20px', height: '20px', marginRight: '8px', borderRadius: '20%' }} />
{languages[langCode].label}
</button>
</li>

View File

@@ -1,5 +1,5 @@
import Link from 'next/link';
import {useTranslations} from 'next-intl';
import {useTranslations, useLocale} from 'next-intl';
import { FaGithub, FaArrowUpRightFromSquare } from 'react-icons/fa6';
import Image from 'next/image';
@@ -8,14 +8,17 @@ type ProjectCardProps = {
description: string;
tech: string[];
imageUrl: string;
liveUrl?: string;
repoUrl?: string;
id: number;
};
export default function ProjectCard({ title, description, tech, imageUrl }: ProjectCardProps) {
export default function ProjectCard({ title, description, tech, imageUrl, liveUrl, repoUrl, id }: ProjectCardProps) {
const t = useTranslations('projects');
const locale = useLocale();
return (
// Added 'h-full' to make all cards in a row the same height
<div className="h-full bg-[var(--color-background)] rounded-lg overflow-hidden flex flex-col border border-[var(--color-border)] hover:border-[var(--color-primary)]/50 transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<Link href={`/${locale}/project/${id}`} className="h-full bg-[var(--color-background)] rounded-lg overflow-hidden flex flex-col border border-[var(--color-border)] hover:border-[var(--color-primary)]/50 transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div className="relative w-full h-48">
<Image src={imageUrl} alt={title} layout="fill" objectFit="cover" />
</div>
@@ -30,16 +33,20 @@ export default function ProjectCard({ title, description, tech, imageUrl }: Proj
))}
</div>
<div className="flex items-center space-x-8 mt-auto pt-4 border-t border-[var(--color-border)]">
<Link href="#" target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
{t('live_link')}
<FaArrowUpRightFromSquare size={12} />
</Link>
<Link href="#" target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
<FaGithub size={20} />
{t('repo_link')}
</Link>
{liveUrl && (
<Link href={liveUrl} target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
{t('live_link')}
<FaArrowUpRightFromSquare size={12} />
</Link>
)}
{repoUrl && (
<Link href={repoUrl} target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
<FaGithub size={20} />
{t('repo_link')}
</Link>
)}
</div>
</div>
</div>
</Link>
);
}

View File

@@ -2,11 +2,10 @@
import {useTranslations} from 'next-intl';
import { motion } from 'framer-motion';
// Updated skills based on your professional experience
const skillsData = {
backend: [".NET (Framework, Core, 6+)", "C#", "Node.js", "Entity Framework"],
backend: [".NET (Framework, Core)", "C#", "Node.js", "Entity Framework"],
frontend: ["Angular", "TypeScript", "Next.js", "React", "HTML5 & CSS3"],
databases: ["Oracle", "PostgreSQL", "Elasticsearch", "SQL"],
databases: ["Oracle", "SQL Server", "Elasticsearch", "Redis"],
cloud: ["Azure", "Docker", "RabbitMQ", "Git", "Kibana"]
};
@@ -32,14 +31,12 @@ export default function AboutAndSkills() {
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] heading-underline">{tAbout('title')}</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
{/* About Me Column */}
<div className="space-y-4 text-lg text-left text-[var(--color-text-secondary)]">
<p>{tAbout('paragraph1')}</p>
<p>{tAbout('paragraph2')}</p>
<p>{tAbout('paragraph3')}</p>
</div>
{/* Skills Column - Updated with new categories */}
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-[var(--color-text-primary)] mb-3">{tSkills('backend')}</h3>

View File

@@ -1,7 +1,6 @@
'use client';
import {useTranslations} from 'next-intl';
import Link from 'next/link';
import { TypeAnimation } from 'react-type-animation';
export default function Hero() {
const t = useTranslations('hero');
@@ -12,20 +11,6 @@ export default function Hero() {
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white">
{t('greeting')} <span className="text-[var(--color-primary)]">João Loureiro</span>
</h1>
<TypeAnimation
sequence={[
t('sequence_1'),
1500,
t('sequence_2'),
1500,
t('sequence_3'),
1500,
]}
wrapper="p"
speed={50}
className="text-xl md:text-2xl lg:text-3xl font-medium text-slate-300 max-w-3xl mx-auto mt-6 mb-8"
repeat={Infinity}
/>
<p className="text-lg md:text-xl text-slate-200 max-w-2xl mx-auto mt-6 mb-8">
{t('subtitle')}
</p>

View File

@@ -4,9 +4,9 @@ import ProjectCard from '../ProjectCard';
import { motion } from 'framer-motion';
const projectsData = [
{ id: 1, tech: ["Next.js", "Stripe", "Tailwind CSS", "Prisma"], imageUrl: "/project1.jpg" },
{ id: 2, tech: ["Socket.IO", "Node.js", "React", "Express"], imageUrl: "/project2.jpg" },
{ id: 3, tech: ["Next.js", "MDX", "Tailwind CSS", "Vercel"], imageUrl: "/project3.jpg" }
{ id: 1, tech: ["Next.js", "Tailwind CSS", "TypeScript", "Framer Motion"], imageUrl: "/Portfolio.png", repoUrl: "https://github.com/joaonloureiro/portfolio-app" },
{ id: 2, tech: ["Traefik", "Docker", "Linux", "Homelab"], imageUrl: "/ProxmoxServer.png" },
{ id: 3, tech: ["React", "TypeScript", "Styled-Components"], imageUrl: "/Happy.png", repoUrl: "https://github.com/joaonloureiro/happy-app", liveUrl: "https://happy.joaoloureiro.dev.br/" }
];
const containerVariants = {
@@ -46,10 +46,13 @@ export default function Projects() {
{projectsData.map(project => (
<motion.div key={project.id} variants={cardVariants}>
<ProjectCard
id={project.id}
title={t(`project_${project.id}_title`)}
description={t(`project_${project.id}_description`)}
tech={project.tech}
imageUrl={project.imageUrl}
liveUrl={project.liveUrl}
repoUrl={project.repoUrl}
/>
</motion.div>
))}

View File

@@ -48,20 +48,13 @@ h3 { font-size: 1.75rem; }
#home {
/* Re-added your background image underneath the gradient */
background:
linear-gradient(125deg, rgba(5, 7, 21, 0.85) 0%, rgba(23, 33, 84, 0.85) 50%, rgba(67, 61, 125, 0.85) 100%),
url("/pattern-randomized (3).svg");
url("/pattern-randomized.svg"),
linear-gradient(150deg, rgba(3, 7, 18, 0.95) 0%, rgba(23, 33, 84, 0.95) 50%, rgba(67, 61, 125, 0.95) 100%);
background-position: center;
background-size: cover;
background-blend-mode: normal;
background-blend-mode: multiply;
}
/* Adds a subtle glow to your name in the hero section */
.hero-name-accent {
color: var(--color-primary);
text-shadow: 0 0 12px rgba(var(--color-primary-rgb), 0.5);
}
/* Adds a subtle shadow to hero text for better readability on the gradient */
#home h1, #home p {
text-shadow: 0px 4px 8px rgba(0, 0, 0, 0.7);
}

View File

@@ -4,8 +4,6 @@ type Props = {
children: ReactNode;
};
// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({children}: Props) {
return children;
}

View File

@@ -1,6 +1,5 @@
import {redirect} from 'next/navigation';
// This page only renders when the app is built statically (output: 'export')
export default function RootPage() {
redirect('/pt');
redirect('/en');
}

View File

@@ -38,15 +38,17 @@
},
"projects": {
"title": "Projects I've Built",
"project_1_title": "E-commerce Platform 'ShopNext'",
"project_1_description": "A full-featured e-commerce website built with Next.js, featuring product catalogs, a shopping cart, user authentication, and a Stripe integration for payments.",
"project_2_title": "Real-time Chat Application 'Converse'",
"project_2_description": "A web-based chat app using Socket.IO and Node.js, allowing users to join rooms and exchange messages in real-time. Features include typing indicators and user presence.",
"project_3_title": "Personal Portfolio & Blog",
"project_3_description": "The very site you are on now! Built with Next.js and Tailwind CSS, statically exported for maximum performance. Includes a blog powered by MDX.",
"project_1_title": "Portfolio",
"project_1_description": "The very site you are on now! Built with Next.js and Tailwind CSS, statically exported for maximum performance.",
"project_2_title": "Homelab",
"project_2_description": "My personal homelab, running on a Raspberry Pi with Docker and Traefik. I host several services, including this portfolio.",
"project_3_title": "Happy",
"project_3_description": "A site to find orphanages to visit. Built with React, TypeScript and Styled-Components.",
"tech_used": "Technologies Used:",
"live_link": "Live Demo",
"repo_link": "View Code"
"repo_link": "View Code",
"back_to_projects": "Back to Projects",
"about_project": "About this Project"
},
"contact": {
"title": "Let's Connect",

View File

@@ -38,15 +38,17 @@
},
"projects": {
"title": "Projetos que Construí",
"project_1_title": "Plataforma de E-commerce 'ShopNext'",
"project_1_description": "Um site de e-commerce completo construído com Next.js, com catálogos de produtos, carrinho de compras, autenticação de usuários e integração com Stripe para pagamentos.",
"project_2_title": "Aplicação de Chat em Tempo Real 'Converse'",
"project_2_description": "Um aplicativo de chat baseado na web usando Socket.IO e Node.js, permitindo que usuários entrem em salas e troquem mensagens em tempo real. Inclui indicadores de digitação e presença de usuário.",
"project_3_title": "Portfólio Pessoal & Blog",
"project_3_description": "O próprio site em que você está agora! Construído com Next.js e Tailwind CSS, exportado estaticamente para máxima performance. Inclui um blog com tecnologia MDX.",
"project_1_title": "Portfólio",
"project_1_description": "O próprio site em que você está agora! Construído com Next.js e Tailwind CSS, exportado estaticamente para máxima performance.",
"project_2_title": "Homelab",
"project_2_description": "Meu homelab pessoal, rodando em um Raspberry Pi com Docker e Traefik. Eu hospedo vários serviços, incluindo este portfólio.",
"project_3_title": "Happy",
"project_3_description": "Um site para encontrar orfanatos para visitar. Construído com React, TypeScript e Styled-Components.",
"tech_used": "Tecnologias Utilizadas:",
"live_link": "Ver ao Vivo",
"repo_link": "Ver Código"
"repo_link": "Ver Código",
"back_to_projects": "Voltar para Projetos",
"about_project": "Sobre este Projeto"
},
"contact": {
"title": "Vamos Conversar",