From 34019c453dc9ef378f204a9c1b2e24ddf8e702fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:37:45 +0000 Subject: [PATCH 1/2] Initial plan From 29a7069521a230004d9bbdab20b5dffa974818ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:43:49 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20complete=20UX=20redesign=20?= =?UTF-8?q?=E2=80=94=20editorial=20minimal=20design=20with=20bento=20grid,?= =?UTF-8?q?=20refined=20tokens,=20and=20accessible=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: charlyautomatiza <89928062+charlyautomatiza@users.noreply.github.com> --- src/app/globals.css | 329 ++++++++++++----- src/components/Portfolio.tsx | 688 ++++++++++++++++++++--------------- tailwind.config.ts | 52 ++- 3 files changed, 668 insertions(+), 401 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 7142fd9..afc1351 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,124 +2,257 @@ @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; +/* ─── Design Tokens ──────────────────────────────────────────── */ :root { - --background: #fafafa; - --foreground: #1a1a1a; - --primary: #0066ff; - --accent: #0ea5e9; - --accent-hover: #0284c7; - --muted: #f1f5f9; - --border: #d1d5db; + /* Surfaces */ + --background: #f8f8f7; + --foreground: #111110; --card: #ffffff; - --card-hover: #f8fafc; - --gradient-start: #f8fafc; - --gradient-end: #f1f5f9; + --card-hover: #fafaf9; + --muted: #f2f1ef; + --muted-foreground: #706f6c; + + /* Borders */ + --border: #e6e4e1; + --border-strong: #c9c7c4; + + /* Brand accent — refined electric teal */ + --accent: #0d9488; + --accent-hover: #0f766e; + --accent-subtle: rgba(13, 148, 136, 0.08); + --accent-subtle-hover: rgba(13, 148, 136, 0.14); + + /* Legacy compat */ + --primary: #0d9488; + --gradient-start: #f8f8f7; + --gradient-end: #f2f1ef; } .dark { - --background: #0a0a0a; - --foreground: #ededed; - --primary: #0066ff; - --accent: #00ffaa; - --accent-hover: #00cc88; - --muted: #1a1a1a; - --border: #333333; - --card: #1a1a1a; - --card-hover: #262626; - --gradient-start: #1a1a1a; - --gradient-end: #0a0a0a; + --background: #0c0c0b; + --foreground: #eeede9; + --card: #161614; + --card-hover: #1d1d1b; + --muted: #1a1a18; + --muted-foreground: #8b8984; + + --border: #252523; + --border-strong: #3a3a37; + + --accent: #2dd4bf; + --accent-hover: #14b8a6; + --accent-subtle: rgba(45, 212, 191, 0.08); + --accent-subtle-hover: rgba(45, 212, 191, 0.14); + + --primary: #2dd4bf; + --gradient-start: #161614; + --gradient-end: #0c0c0b; +} + +/* ─── Base ───────────────────────────────────────────────────── */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; } body { color: var(--foreground); background: var(--background); - font-family: var(--font-geist), system-ui, -apple-system, sans-serif; - transition: background-color 0.3s ease, color 0.3s ease; + font-family: var(--font-geist), 'Inter', system-ui, -apple-system, sans-serif; + transition: background-color 0.25s ease, color 0.25s ease; line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -/* Enhanced hover effects */ -.card-hover { - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; +/* ─── Typography Scale ───────────────────────────────────────── */ +.display-xl { + font-size: clamp(3rem, 8vw, 6rem); + line-height: 1.05; + letter-spacing: -0.03em; + font-weight: 700; } -.card-hover::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient( - 90deg, - transparent, - rgba(0, 255, 170, 0.1), - transparent - ); - transition: left 0.6s ease; - z-index: 1; +.display-lg { + font-size: clamp(2rem, 5vw, 3.5rem); + line-height: 1.1; + letter-spacing: -0.025em; + font-weight: 700; } -.card-hover:hover::before { - left: 100%; +.display-md { + font-size: clamp(1.5rem, 3vw, 2.25rem); + line-height: 1.2; + letter-spacing: -0.02em; + font-weight: 600; +} + +/* ─── Card Primitives ────────────────────────────────────────── */ +.card-base { + background: var(--card); + border: 1px solid var(--border); + border-radius: 16px; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; +} + +.card-base:hover { + border-color: var(--border-strong); + box-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); +} + +.dark .card-base:hover { + box-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.4); +} + +/* ─── Legacy compat aliases ──────────────────────────────────── */ +.card-hover { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .card-hover:hover { - transform: translateY(-6px) scale(1.02); - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 255, 170, 0.1); + transform: translateY(-4px); + box-shadow: 0 16px 40px -8px rgba(0, 0, 0, 0.12); } .dark .card-hover:hover { - box-shadow: 0 25px 50px -12px rgba(0, 255, 170, 0.15), 0 0 30px rgba(0, 255, 170, 0.1); + box-shadow: 0 16px 40px -8px rgba(0, 0, 0, 0.5); } -/* Enhanced button glow effect */ .btn-glow { - position: relative; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-glow:hover { + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(13, 148, 136, 0.35); +} + +/* ─── Bento Grid ─────────────────────────────────────────────── */ +.bento-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(6, 1fr); +} + +.bento-cell { + background: var(--card); + border: 1px solid var(--border); + border-radius: 20px; overflow: hidden; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.25s ease; +} + +.bento-cell:hover { + border-color: var(--border-strong); + box-shadow: 0 12px 40px -8px rgba(0, 0, 0, 0.12); + transform: translateY(-3px); +} + +.dark .bento-cell:hover { + box-shadow: 0 12px 40px -8px rgba(0, 0, 0, 0.5); } -.btn-glow::before { +/* ─── Accent line decoration ─────────────────────────────────── */ +.section-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent); +} + +.section-label::before { content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.3), - transparent - ); - transition: left 0.6s ease; + display: block; + width: 24px; + height: 2px; + background: var(--accent); + border-radius: 2px; } -.btn-glow:hover::before { - left: 100%; +/* ─── Navigation ─────────────────────────────────────────────── */ +.nav-link { + position: relative; + font-size: 0.875rem; + font-weight: 500; + color: var(--muted-foreground); + transition: color 0.2s ease; + padding: 6px 12px; + border-radius: 8px; } -.btn-glow:hover { - box-shadow: 0 0 20px rgba(0, 255, 170, 0.4), 0 0 40px rgba(0, 255, 170, 0.2); - transform: translateY(-2px); +.nav-link:hover { + color: var(--foreground); + background: var(--accent-subtle); } -/* New shimmer effect for accent elements */ -.shimmer-bg { - background: linear-gradient( - 90deg, - var(--accent) 0%, - rgba(0, 255, 170, 0.8) 50%, - var(--accent) 100% - ); - background-size: 200% 100%; - animation: shimmer 2.5s linear infinite; +.nav-link.active { + color: var(--accent); + background: var(--accent-subtle); +} + +/* ─── Skill Badge ────────────────────────────────────────────── */ +.skill-badge { + display: inline-flex; + align-items: center; + padding: 5px 12px; + border-radius: 999px; + font-size: 0.8125rem; + font-weight: 500; + background: var(--muted); + color: var(--muted-foreground); + border: 1px solid var(--border); + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.skill-badge:hover { + background: var(--accent-subtle); + color: var(--accent); + border-color: var(--accent); +} + +/* ─── Timeline ───────────────────────────────────────────────── */ +.timeline-item { + position: relative; + padding-left: 1.75rem; +} + +.timeline-item::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 0 3px var(--accent-subtle); } -/* Enhanced mobile menu animation */ +.timeline-item::after { + content: ''; + position: absolute; + left: 3.5px; + top: 20px; + width: 1px; + bottom: -16px; + background: var(--border); +} + +.timeline-item:last-child::after { + display: none; +} + +/* ─── Mobile menu ────────────────────────────────────────────── */ @keyframes slideInLeft { from { transform: translateX(-100%); @@ -132,5 +265,39 @@ body { } .mobile-menu-enter { - animation: slideInLeft 0.3s cubic-bezier(0.4, 0, 0.2, 1); + animation: slideInLeft 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ─── Shimmer / Legacy ───────────────────────────────────────── */ +.shimmer-bg { + background: linear-gradient( + 90deg, + var(--accent) 0%, + var(--accent-hover) 50%, + var(--accent) 100% + ); + background-size: 200% 100%; + animation: shimmer 2.5s linear infinite; +} + +/* ─── Focus ring ─────────────────────────────────────────────── */ +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + border-radius: 4px; +} + +/* ─── Scrollbar ──────────────────────────────────────────────── */ +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--border-strong); + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); } diff --git a/src/components/Portfolio.tsx b/src/components/Portfolio.tsx index 9906bbb..08e80ec 100644 --- a/src/components/Portfolio.tsx +++ b/src/components/Portfolio.tsx @@ -4,26 +4,24 @@ import React, { useState, useEffect, useRef } from 'react' import { gsap } from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import { Button } from '@/components/ui/button' -import { Card } from '@/components/ui/card' -import { GithubIcon, LinkedinIcon, MailIcon, FileTextIcon, ChevronDownIcon, MoonIcon, SunIcon, MenuIcon, XIcon, PaletteIcon } from 'lucide-react' +import { GithubIcon, LinkedinIcon, MailIcon, FileTextIcon, MoonIcon, SunIcon, MenuIcon, XIcon, PaletteIcon, ExternalLinkIcon, MapPinIcon, ArrowDownIcon } from 'lucide-react' import { useTheme } from 'next-themes' import { useMultiTheme } from '@/components/MultiThemeProvider' import ThemeSelector from '@/components/ThemeSelector' import { PortfolioProps } from '@/types' -import Image from 'next/image' import CVTemplateSelector from '@/components/CVTemplateSelector' import CVPreview from '@/components/CVPreview' import { CVTemplate, createCVPdf } from '@/utils/pdfGenerator' gsap.registerPlugin(ScrollTrigger) -export default function Portfolio({ - projects, - cvData, - personalInfo, - socialLinks, - cvPdfUrl, - isDevMode = false, +export default function Portfolio({ + projects, + cvData, + personalInfo, + socialLinks, + cvPdfUrl, + isDevMode = false, featureFlags = { DEFAULT_CV_TEMPLATE: 'harvard' }, showThemeSelector = false, footerConfig = { @@ -75,29 +73,27 @@ export default function Portfolio({ } window.addEventListener('scroll', handleScroll) - handleScroll() // Set initial active section + handleScroll() return () => window.removeEventListener('scroll', handleScroll) }, [isContactEnabled]) useEffect(() => { if (mounted) { - gsap.from('.header', { y: -100, opacity: 0, duration: 1, ease: 'power3.out' }) - - Object.keys(sectionRefs.current).forEach((section) => { - if (sectionRefs.current[section]?.current) { - gsap.from(sectionRefs.current[section].current, { - opacity: 0, - y: 50, - duration: 1, - scrollTrigger: { - trigger: sectionRefs.current[section].current, - start: 'top 80%', - end: 'bottom 20%', - toggleActions: 'play none none reverse', - }, - }) - } + gsap.from('.header', { y: -60, opacity: 0, duration: 0.8, ease: 'power3.out' }) + + gsap.utils.toArray('.gsap-reveal').forEach((el) => { + gsap.from(el, { + opacity: 0, + y: 32, + duration: 0.7, + ease: 'power3.out', + scrollTrigger: { + trigger: el, + start: 'top 88%', + toggleActions: 'play none none none', + }, + }) }) } }, [mounted]) @@ -120,27 +116,23 @@ export default function Portfolio({ const message = formData.get('message') as string try { - const response = await fetch('/api/submit-form', { - method: 'POST', - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name, email, message }), - }) + const response = await fetch('/api/submit-form', { + method: 'POST', + mode: 'cors', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, email, message }), + }) - if (!response.ok) { - const errorText = await response.text(); // Obtener el texto de error - throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); - } + if (!response.ok) { + const errorText = await response.text() + throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`) + } - if (e.currentTarget) { - e.currentTarget.reset() - } - setSubmitStatus('success') - } catch (error: unknown) { // Especificar el tipo de error como 'unknown' - console.error('Error submitting form:', error) - setSubmitStatus('error') + if (e.currentTarget) e.currentTarget.reset() + setSubmitStatus('success') + } catch (error: unknown) { + console.error('Error submitting form:', error) + setSubmitStatus('error') } } @@ -161,29 +153,26 @@ export default function Portfolio({ } const handleCVDownload = () => { - // This will be called from the preview component handlePreviewClose() } const handleCVDownloadClick = async () => { if (isDevMode) { - // Development mode: Show template selector and preview setShowCVTemplateSelector(true) } else { - // Production mode: Direct download with default template try { const template = featureFlags.DEFAULT_CV_TEMPLATE const pdfArrayBuffer = await createCVPdf(cvData, personalInfo, socialLinks, { template }) const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }) const url = URL.createObjectURL(blob) - + const link = document.createElement('a') link.href = url link.download = `${personalInfo.name.replace(/\s+/g, '_')}_CV_${template}.pdf` document.body.appendChild(link) link.click() document.body.removeChild(link) - + URL.revokeObjectURL(url) } catch (error) { console.error('Error downloading CV:', error) @@ -199,70 +188,75 @@ export default function Portfolio({ if (isContactEnabled) navItems.push('contact') return ( -
-
-
-
- -

{personalInfo.name}

-
-