feat: Enhance layout and styling with new fonts and components
- Integrated Space Grotesk font alongside Inter in layout. - Added FloatingSocials component for social media links. - Updated Footer with a new background color. - Modified Header to improve spacing and added LanguageSwitcher. - Refactored LanguageSwitcher to use a dropdown for language selection. - Updated ProjectCard to include images and improved layout. - Revamped About section to include categorized skills with animations. - Enhanced Contact section with animations and improved form styling. - Updated Hero section with type animation for dynamic text display. - Refactored Projects section to include animations for project cards. - Removed Skills section as it was integrated into the About section. - Updated global styles for light and dark themes, including new animations. - Updated translations for new skills and hero section text.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Inter } from "next/font/google";
|
||||
import { Inter, Space_Grotesk } from "next/font/google";
|
||||
import Header from "@/app/components/Header"; // Usando o Header que forneci
|
||||
import Footer from "@/app/components/Footer";
|
||||
import "@/app/globals.css"; // Importando o CSS global
|
||||
@@ -11,8 +11,18 @@ import {getTranslations, setRequestLocale} from 'next-intl/server';
|
||||
import { ReactNode } from "react";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { notFound } from "next/navigation";
|
||||
import FloatingSocials from "@/app/components/FloatingSocials";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: '--font-inter',
|
||||
});
|
||||
|
||||
const space_grotesk = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
weight: ['300', '400', '500', '700'],
|
||||
variable: '--font-space-grotesk',
|
||||
});
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
@@ -42,13 +52,13 @@ export default async function RootLayout({children, params}: Props) {
|
||||
setRequestLocale(locale);
|
||||
|
||||
return (
|
||||
<html lang={locale} className="scroll-smooth">
|
||||
<html lang={locale} className={`${inter.variable} ${space_grotesk.variable} scroll-smooth`}>
|
||||
<body className={`${inter.className} bg-background text-text-primary`}>
|
||||
<ThemeProvider>
|
||||
<NextIntlClientProvider locale={locale}>
|
||||
|
||||
<FloatingSocials />
|
||||
<Header />
|
||||
<main className="flex flex-col items-center">
|
||||
<main className="flex flex-col">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Hero from "@/app/components/sections/Hero";
|
||||
import About from "@/app/components/sections/About";
|
||||
import Skills from "@/app/components/sections/Skills";
|
||||
import Projects from "@/app/components/sections/Projects";
|
||||
import Contact from "@/app/components/sections/Contact";
|
||||
import { Locale } from "next-intl";
|
||||
@@ -10,19 +9,14 @@ import { setRequestLocale } from "next-intl/server";
|
||||
export default function Home({params}: {
|
||||
params: Promise<{locale: Locale}>;
|
||||
}) {
|
||||
|
||||
const {locale} = use(params);
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
setRequestLocale(use(params).locale);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4">
|
||||
<>
|
||||
<Hero />
|
||||
<About />
|
||||
<Skills />
|
||||
<Projects />
|
||||
<Contact />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
frontend/src/app/components/FloatingSocials.tsx
Normal file
30
frontend/src/app/components/FloatingSocials.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import Link from 'next/link';
|
||||
import { FaGithub, FaLinkedin } from 'react-icons/fa';
|
||||
import { HiOutlineMail } from 'react-icons/hi';
|
||||
|
||||
const socialLinks = [
|
||||
{ href: 'mailto:contato@joaoloureiro.dev.br', icon: <HiOutlineMail/>, label: 'Email' },
|
||||
{ href: process.env.NEXT_PUBLIC_GITHUB_URL || '#', icon: <FaGithub/>, label: 'GitHub' },
|
||||
{ href: process.env.NEXT_PUBLIC_LINKEDIN_URL || '#', icon: <FaLinkedin/>, label: 'LinkedIn' },
|
||||
];
|
||||
|
||||
export default function FloatingSocials() {
|
||||
return (
|
||||
<div className="hidden md:block fixed left-0 top-1/2 -translate-y-1/2 z-30">
|
||||
<div className="flex flex-col items-center space-y-1 bg-[var(--color-card)]/50 border border-[var(--color-border)] p-2 rounded-r-lg backdrop-blur-sm">
|
||||
{socialLinks.map(link => (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
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"
|
||||
>
|
||||
<div className="h-6 w-6">{link.icon}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export default function Footer() {
|
||||
const linkedinUrl = process.env.NEXT_PUBLIC_LINKEDIN_URL || '#';
|
||||
|
||||
return (
|
||||
<footer className="border-t border-[var(--color-border)] py-8 text-[var(--color-text-secondary)]">
|
||||
<footer className="border-t bg-black border-[var(--color-border)] py-8 text-[var(--color-text-secondary)]">
|
||||
<div className="container mx-auto flex flex-col md:flex-row justify-between items-center gap-4 px-4 sm:px-6 lg:px-8">
|
||||
<p className="text-sm">© {new Date().getFullYear()} João Loureiro. {t('copyright')}</p>
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
@@ -11,24 +11,25 @@ export default function Header() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-background)]/90 backdrop-blur-sm">
|
||||
<header className="sticky top-0 z-50 bg-[var(--color-background)]/90 backdrop-blur-sm">
|
||||
<nav className="container mx-auto flex items-center justify-between py-4 px-4 sm:px-6 lg:px-8">
|
||||
<Link href="/">
|
||||
<Image src="/logo.png" alt="João Loureiro Logo" width={40} height={40} priority />
|
||||
</Link>
|
||||
<div className="hidden md:flex items-center space-x-6 text-[var(--color-text-secondary)]">
|
||||
<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="#skills" className="hover:text-[var(--color-primary)] transition-colors">{t('tech')}</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>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="hidden md:flex items-center space-x-6 text-[var(--color-text-secondary)]">
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<button onClick={toggleTheme} className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700">
|
||||
{theme === 'dark'? <SunIcon className="h-6 w-6 text-yellow-400" /> : <MoonIcon className="h-6 w-6 text-gray-600" />}
|
||||
</button>
|
||||
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -2,31 +2,70 @@
|
||||
|
||||
import { useLocale } from 'next-intl';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useTransition } from 'react';
|
||||
import { useState, useTransition, useRef, useEffect } from 'react';
|
||||
import { FaChevronDown } from 'react-icons/fa6';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname().replaceAll(/^\/(pt|en)/g, '');
|
||||
const locale = useLocale();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const nextLocale = e.target.value;
|
||||
|
||||
const onSelectChange = (nextLocale: string) => {
|
||||
startTransition(() => {
|
||||
router.replace(`/${nextLocale}${pathname}`);
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleOutsideClick = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleOutsideClick);
|
||||
return () => document.removeEventListener('mousedown', handleOutsideClick);
|
||||
}, []);
|
||||
|
||||
const languages: { [key: string]: { flag: string; label: string } } = {
|
||||
en: { flag: '🇺🇸', label: 'English' },
|
||||
pt: { flag: '🇧🇷', label: 'Português' },
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
defaultValue={locale}
|
||||
onChange={onSelectChange}
|
||||
disabled={isPending}
|
||||
className="bg-[var(--color-card)] text-[var(--color-text-secondary)] border border-[var(--color-border)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||
>
|
||||
<option value="pt">🇧🇷 Português</option>
|
||||
<option value="en">🇺🇸 English</option>
|
||||
</select>
|
||||
<div className="relative" ref={containerRef}>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span>{languages[locale].flag} {languages[locale].label}</span>
|
||||
<FaChevronDown className={`ml-2 h-4 w-4 transition-transform ${isOpen ? 'transform rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-40 bg-[var(--color-card)] border border-[var(--color-border)] rounded-md shadow-lg z-10 lang-switcher-options">
|
||||
<ul className="py-1">
|
||||
{Object.keys(languages).map((langCode) => (
|
||||
<li key={langCode}>
|
||||
<button
|
||||
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>
|
||||
{languages[langCode].label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +1,44 @@
|
||||
import Link from 'next/link';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import { FaGithub, FaArrowUpRightFromSquare } from 'react-icons/fa6';
|
||||
import Image from 'next/image';
|
||||
|
||||
type ProjectCardProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
tech: string[];
|
||||
imageUrl: string;
|
||||
};
|
||||
|
||||
export default function ProjectCard({ title, description, tech }: ProjectCardProps) {
|
||||
export default function ProjectCard({ title, description, tech, imageUrl }: ProjectCardProps) {
|
||||
const t = useTranslations('projects');
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--color-card)] rounded-lg p-6 flex flex-col border border-[var(--color-border)] hover:border-[var(--color-primary)]/50 transition-colors duration-300">
|
||||
<h3 className="text-[var(--color-text-primary)] mb-2">{title}</h3>
|
||||
<p className="text-[var(--color-text-secondary)] mb-4 flex-grow">{description}</p>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{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>
|
||||
))}
|
||||
// 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">
|
||||
<div className="relative w-full h-48">
|
||||
<Image src={imageUrl} alt={title} layout="fill" objectFit="cover" />
|
||||
</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>
|
||||
<div className="p-6 flex flex-col flex-grow">
|
||||
<h3 className="text-[var(--color-text-primary)] mb-2">{title}</h3>
|
||||
<p className="text-[var(--color-text-secondary)] mb-4 flex-grow">{description}</p>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{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)]">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,72 @@
|
||||
'use client';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function About() {
|
||||
const t = useTranslations('about');
|
||||
// Updated skills based on your professional experience
|
||||
const skillsData = {
|
||||
backend: [".NET (Framework, Core, 6+)", "C#", "Node.js", "Entity Framework"],
|
||||
frontend: ["Angular", "TypeScript", "Next.js", "React", "HTML5 & CSS3"],
|
||||
databases: ["Oracle", "PostgreSQL", "Elasticsearch", "SQL"],
|
||||
cloud: ["Azure", "Docker", "RabbitMQ", "Git", "Kibana"]
|
||||
};
|
||||
|
||||
const SkillPill = ({ skill }: { skill: string }) => (
|
||||
<div className="bg-[var(--color-background)] text-[var(--color-text-secondary)] border border-[var(--color-border)] rounded-full px-4 py-2 text-sm font-medium">
|
||||
{skill}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function AboutAndSkills() {
|
||||
const tAbout = useTranslations('about');
|
||||
const tSkills = useTranslations('skills');
|
||||
|
||||
return (
|
||||
<section id="about" className="container mx-auto py-24 md:py-32 lg:py-48">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] mb-12">{t('title')}</h2>
|
||||
<div className="max-w-3xl mx-auto text-[var(--color-text-secondary)] space-y-6 text-lg text-left md:text-justify">
|
||||
<p>{t('paragraph1')}</p>
|
||||
<p>{t('paragraph2')}</p>
|
||||
<p>{t('paragraph3')}</p>
|
||||
<motion.section
|
||||
id="about"
|
||||
className="container mx-auto py-12 md:py-20"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<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>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{skillsData.backend.map(skill => <SkillPill key={skill} skill={skill} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-[var(--color-text-primary)] mb-3">{tSkills('frontend')}</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{skillsData.frontend.map(skill => <SkillPill key={skill} skill={skill} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-[var(--color-text-primary)] mb-3">{tSkills('databases')}</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{skillsData.databases.map(skill => <SkillPill key={skill} skill={skill} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-[var(--color-text-primary)] mb-3">{tSkills('cloud')}</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{skillsData.cloud.map(skill => <SkillPill key={skill} skill={skill} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import { FormEvent, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -72,33 +72,40 @@ export default function Contact() {
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="container mx-auto py-24 md:py-32 lg:py-48">
|
||||
<motion.section
|
||||
id="contact"
|
||||
className="container mx-auto py-12 md:py-20"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<div className="text-center max-w-2xl mx-auto">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">{t('title')}</h2>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] heading-underline">{t('title')}</h2>
|
||||
<p className="text-[var(--color-text-secondary)] mb-10">{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Centered Form */}
|
||||
<form onSubmit={handleSubmit} className="max-w-xl mx-auto">
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_name')}</label>
|
||||
<input type="text" name="name" id="name" value={formData.name} onChange={handleChange} required
|
||||
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" />
|
||||
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="email" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_email')}</label>
|
||||
<input type="email" name="email" id="email" value={formData.email} onChange={handleChange} required
|
||||
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" />
|
||||
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="message" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_message')}</label>
|
||||
<textarea name="message" id="message" rows={5} value={formData.message} onChange={handleChange} required
|
||||
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5"></textarea>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<button type="submit" className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white font-bold py-3 px-8 rounded-full transition-colors" disabled={isSubmitting}>
|
||||
{isSubmitting ? t('status_sending') : t('submit_button')}
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white font-bold py-3 px-8 rounded-full transition-colors w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? t('status_sending') : t('submit_button')}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,42 @@
|
||||
'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');
|
||||
|
||||
return (
|
||||
<section id="home" className="container mx-auto text-center py-24 md:py-32 lg:py-48">
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)]">
|
||||
{t('greeting')} <span className="block text-[var(--color-primary)]">{t('title')}</span>
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-[var(--color-text-secondary)] max-w-2xl mx-auto mt-6 mb-8">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
<Link href="#contact" className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white font-bold py-3 px-8 rounded-full transition-colors">
|
||||
{t('cta_button')}
|
||||
</Link>
|
||||
<section id="home" className="relative w-full flex flex-col items-center justify-center min-h-[calc(100vh-var(--header-height))]">
|
||||
<div className="container mx-auto px-4 text-center flex flex-col items-center">
|
||||
<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>
|
||||
<Link href="#contact" className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white font-bold py-3 px-8 rounded-full transition-colors">
|
||||
{t('cta_button')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-12">
|
||||
<div className="mouse"></div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,60 @@
|
||||
'use client';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import ProjectCard from '../ProjectCard';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// Você pode popular isso com seus dados reais
|
||||
const projectsData = [
|
||||
{ id: 1, tech: ["Next.js", "Stripe", "Tailwind CSS", "Prisma"] },
|
||||
{ id: 2, tech: ["Socket.IO", "Node.js", "React", "Express"] },
|
||||
{ id: 3, tech: ["Next.js", "MDX", "Tailwind CSS", "Vercel"] }
|
||||
{ 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" }
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.2, delayChildren: 0.1 }
|
||||
}
|
||||
};
|
||||
|
||||
const cardVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: { y: 0, opacity: 1 }
|
||||
};
|
||||
|
||||
export default function Projects() {
|
||||
const t = useTranslations('projects');
|
||||
|
||||
return (
|
||||
<section id="projects" className="container mx-auto py-24 md:py-32 lg:py-48">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] mb-12">{t('title')}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{projectsData.map(project => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
title={t(`project_${project.id}_title`)}
|
||||
description={t(`project_${project.id}_description`)}
|
||||
tech={project.tech}
|
||||
/>
|
||||
))}
|
||||
<motion.section
|
||||
id="projects"
|
||||
className="bg-[var(--color-card)] py-12 md:py-20"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] heading-underline">{t('title')}</h2>
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{projectsData.map(project => (
|
||||
<motion.div key={project.id} variants={cardVariants}>
|
||||
<ProjectCard
|
||||
title={t(`project_${project.id}_title`)}
|
||||
description={t(`project_${project.id}_description`)}
|
||||
tech={project.tech}
|
||||
imageUrl={project.imageUrl}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
const skillsData = {
|
||||
languages: ["JavaScript (ES6+)", "TypeScript", "HTML5", "CSS3", "Python"],
|
||||
frameworks: ["React", "Next.js", "Node.js", "Express", "Tailwind CSS", "tRPC"],
|
||||
tools: ["Git", "GitHub", "Docker", "VS Code", "Figma", "Postman", "Vercel"],
|
||||
databases: ["PostgreSQL", "MongoDB", "Redis", "Prisma ORM"]
|
||||
};
|
||||
|
||||
const SkillCategory = ({ title, skills }: { title: string, skills: string[] }) => (
|
||||
<div className="bg-[var(--color-card)] p-6 rounded-lg border border-[var(--color-border)]">
|
||||
<h3 className="text-[var(--color-primary)] mb-4">{title}</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{skills.map(skill => (
|
||||
<span key={skill} className="bg-[var(--color-border)]/50 text-[var(--color-text-secondary)] px-3 py-1 rounded-full text-sm font-medium">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function Skills() {
|
||||
const t = useTranslations('skills');
|
||||
|
||||
return (
|
||||
<section id="skills" className="container mx-auto py-24 md:py-32 lg:py-48">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] mb-12">{t('title')}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<SkillCategory title={t('languages')} skills={skillsData.languages} />
|
||||
<SkillCategory title={t('frameworks')} skills={skillsData.frameworks} />
|
||||
<SkillCategory title={t('tools')} skills={skillsData.tools} />
|
||||
<SkillCategory title={t('databases')} skills={skillsData.databases} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,142 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@tailwind utilities;
|
||||
|
||||
@theme {
|
||||
--color-background: #161616;
|
||||
--color-card: #121212;
|
||||
--color-primary: #636B97;
|
||||
--color-text-primary: #FFFFFF;
|
||||
--color-text-secondary: #EAE3DB;
|
||||
--color-border: #27272A;
|
||||
/* --- Indigo Dream | Light Theme (Default) --- */
|
||||
:root {
|
||||
--color-background: #ffffff;
|
||||
--color-card: #f7f7f7;
|
||||
--color-primary: #3b82f6; /* A vibrant, modern blue */
|
||||
--color-text-primary: #111827;
|
||||
--color-text-secondary: #6b7280;
|
||||
--color-border: #e5e7eb;
|
||||
--header-height: 4.6rem;
|
||||
}
|
||||
|
||||
/* --- Modern Gradient | Dark Theme --- */
|
||||
.dark {
|
||||
--color-background: #030712; /* A very deep, neutral blue-gray */
|
||||
--color-card: #111827;
|
||||
--color-primary: #3b82f6; /* Blue remains vibrant on the dark background */
|
||||
--color-text-primary: #f9fafb;
|
||||
--color-text-secondary: #9ca3af;
|
||||
--color-border: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--color-text-primary);
|
||||
/* Apply the IBM Plex Sans font variable to the body */
|
||||
font-family: var(--font-quantico);
|
||||
background-color: var(--color-background);
|
||||
font-family: Inter, sans-serif; /* Adicione sua fonte preferida */
|
||||
color: var(--color-text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
/* Apply the Poppins font variable to all headings */
|
||||
font-family: var(--font-quantico), sans-serif;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.75rem; }
|
||||
h3 { font-size: 1.75rem; }
|
||||
|
||||
/* --- Hero Section Styling --- */
|
||||
#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");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-blend-mode: normal;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* --- Scroll Mouse Animation --- */
|
||||
.mouse {
|
||||
width: 25px;
|
||||
height: 40px;
|
||||
/* Always white to be visible on the dark hero gradient */
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 60px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mouse::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
opacity: 1;
|
||||
animation: wheel 1.5s infinite;
|
||||
-webkit-animation: wheel 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes wheel {
|
||||
to {
|
||||
opacity: 0;
|
||||
top: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes wheel {
|
||||
to {
|
||||
opacity: 0;
|
||||
top: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Language Switcher Dropdown --- */
|
||||
.lang-switcher-options {
|
||||
transform-origin: top right;
|
||||
animation: scale-in 0.1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* For the accent line under headings */
|
||||
.heading-underline {
|
||||
position: relative;
|
||||
padding-bottom: 1.5rem;
|
||||
margin-bottom: 3rem !important; /* Ensure spacing */
|
||||
}
|
||||
|
||||
.heading-underline::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 5rem;
|
||||
height: 4px;
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
@@ -15,8 +15,11 @@
|
||||
"contact": "Contact"
|
||||
},
|
||||
"hero": {
|
||||
"greeting": "Hello, I'm João Loureiro.",
|
||||
"greeting": "Hello, I'm",
|
||||
"title": "Software Developer & Web Enthusiast",
|
||||
"sequence_1": "Full-Stack Software Developer",
|
||||
"sequence_2": ".NET & Cloud Enthusiast",
|
||||
"sequence_3": "Passionate about Technology",
|
||||
"subtitle": "I build modern, responsive, and user-friendly web applications from front-end to back-end.",
|
||||
"cta_button": "See My Work"
|
||||
},
|
||||
@@ -27,11 +30,11 @@
|
||||
"paragraph3": "When I'm not at my keyboard, I enjoy exploring the outdoors, reading about new tech trends, and contributing to open-source projects. I believe the best products are built at the intersection of great technology and human-centric design."
|
||||
},
|
||||
"skills": {
|
||||
"title": "My Tech Stack",
|
||||
"languages": "Languages",
|
||||
"frameworks": "Frameworks & Libraries",
|
||||
"tools": "Developer Tools",
|
||||
"databases": "Databases"
|
||||
"title": "Minhas Tecnologias",
|
||||
"backend": "Backend & Linguagens",
|
||||
"frontend": "Frontend",
|
||||
"databases": "Bancos de Dados & Dados",
|
||||
"cloud": "Cloud & DevOps"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projects I've Built",
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
"contact": "Contato"
|
||||
},
|
||||
"hero": {
|
||||
"greeting": "Olá, eu sou o João Loureiro.",
|
||||
"greeting": "Olá, eu sou o",
|
||||
"title": "Desenvolvedor de Software & Entusiasta Web",
|
||||
"sequence_1": "Desenvolvedor de Software Full-Stack",
|
||||
"sequence_2": "Entusiasta em .NET & Cloud",
|
||||
"sequence_3": "Apaixonado por Tecnologia",
|
||||
"subtitle": "Eu construo aplicações web modernas, responsivas e fáceis de usar, do front-end ao back-end.",
|
||||
"cta_button": "Veja Meus Projetos"
|
||||
},
|
||||
@@ -27,11 +30,11 @@
|
||||
"paragraph3": "Quando não estou no meu teclado, gosto de explorar a natureza, ler sobre novas tendências tecnológicas e contribuir para projetos de código aberto. Acredito que os melhores produtos são construídos na interseção de ótima tecnologia e design centrado no ser humano."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Minhas Tecnologias",
|
||||
"languages": "Linguagens",
|
||||
"frameworks": "Frameworks & Bibliotecas",
|
||||
"tools": "Ferramentas de Dev",
|
||||
"databases": "Bancos de Dados"
|
||||
"title": "My Tech Stack",
|
||||
"backend": "Backend & Languages",
|
||||
"frontend": "Frontend",
|
||||
"databases": "Databases & Data",
|
||||
"cloud": "Cloud & DevOps"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projetos que Construí",
|
||||
|
||||
Reference in New Issue
Block a user