feat: add project detail page with dynamic content and navigation
This commit is contained in:
@@ -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)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
64
frontend/src/app/(i18n)/[locale]/project/[id]/page.tsx
Normal file
64
frontend/src/app/(i18n)/[locale]/project/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user