diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ed7d6cf..b906f7b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,9 +5,9 @@ name: Deploy Next.js site to Pages on: - # Runs on pushes targeting the default branch - push: - branches: ["main", "master", "feat/MVP"] # Including current working branch as well for testing + # Runs on pull request to master + pull_request: + branches: ["master"] # Rebuild the site when a new CV is published release: @@ -22,6 +22,9 @@ permissions: pages: write id-token: write +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: @@ -89,6 +92,7 @@ jobs: # Deployment job deploy: + if: github.event_name != 'pull_request' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/app/globals.css b/app/globals.css index d498622..3a43e6a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5,27 +5,42 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-open-sans); + --font-sans: var(--font-inter); + --font-heading: var(--font-outfit); --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --color-accent: var(--accent); + --color-secondary: var(--secondary); } /* Light Theme (Default) */ :root { --background: #ffffff; --foreground: #171717; - --accent: #64ACEC; + --accent: #6366f1; + --secondary: #10b981; } /* Dark Theme (Class-based) */ .dark { - --background: #0a0a0a; + --background: #050505; --foreground: #ededed; - --accent: #64ACEC; + --accent: #6366f1; + --secondary: #10b981; } body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +} + +@layer components { + .glass-card { + @apply bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl shadow-2xl; + } +} + +@keyframes shine { + 100% { + transform: translateX(200%); + } } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 7bbc5e0..33ff900 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,10 +1,15 @@ import type { Metadata, Viewport } from "next"; -import { Open_Sans } from "next/font/google"; +import { Inter, Outfit } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider" -const openSans = Open_Sans({ - variable: "--font-open-sans", +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], +}); + +const outfit = Outfit({ + variable: "--font-outfit", subsets: ["latin"], }); @@ -27,9 +32,13 @@ export default function RootLayout({ return ( +
+
+
+
{children} diff --git a/app/page.tsx b/app/page.tsx index 5ab7587..b0d00fb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,17 +5,20 @@ import { ProjectsSection } from '@/components/sections/projects' import { ExperienceSection } from '@/components/sections/experience' import { MoreAboutSection } from '@/components/sections/more-about' import { ContactSection } from '@/components/sections/contact' +import { PageTransition } from '@/components/ui/page-transition' export default function Home() { return (
- - - - - - + + + + + + + +
) } diff --git a/components/sections/about.tsx b/components/sections/about.tsx index 9054a7b..6592259 100644 --- a/components/sections/about.tsx +++ b/components/sections/about.tsx @@ -1,5 +1,8 @@ import { aboutController } from "@/src/di" import { AboutStat } from "@/src/entities/about" +import { Reveal } from "@/components/ui/reveal" +import { AnimatedStats } from "@/components/ui/animated-stats" +import { AnimatedSkills } from "@/components/ui/animated-skills" export async function AboutSection() { const about = await aboutController.getAbout() @@ -7,41 +10,36 @@ export async function AboutSection() { return (
-

- About Me -

+ +

+ About Me +

+
{/* Bio */}
- {about.bio.map((paragraph, i) => ( -

- {paragraph} -

- ))} + +
+ {about.bio.map((paragraph, i) => ( +

+ {paragraph} +

+ ))} +
+
{/* Quick Stats */} -
- {about.stats.map((stat) => ( - - ))} -
+
{/* Skills */}
-

Core Skills

-
- {about.skills.map((skill) => ( - - {skill} - - ))} -
+ +

Core Skills

+
+
diff --git a/components/sections/hero.tsx b/components/sections/hero.tsx index 6a28139..be1737a 100644 --- a/components/sections/hero.tsx +++ b/components/sections/hero.tsx @@ -1,6 +1,9 @@ import Image from 'next/image' import { aboutController } from '@/src/di' import { CvDialog } from '@/components/ui/cv-dialog' +import { HeroImage } from '@/components/ui/hero-image' +import { AnimatedMarquee } from '@/components/ui/animated-marquee' +import { TactileButton } from '@/components/ui/tactile-button' export async function Hero() { const about = await aboutController.getAbout() @@ -24,37 +27,30 @@ export async function Hero() { ))}
-
- + View Work - - + Contact Me - +
+ +
+ +
{/* Image Column */} -
- {/* Gradient Mask to blend image into black background */} - - - - Andres Sanchez +
+
diff --git a/components/ui/animated-marquee.tsx b/components/ui/animated-marquee.tsx new file mode 100644 index 0000000..a0c7086 --- /dev/null +++ b/components/ui/animated-marquee.tsx @@ -0,0 +1,38 @@ +"use client" + +import { motion } from "framer-motion" + +interface AnimatedMarqueeProps { + items: string[] +} + +export function AnimatedMarquee({ items }: AnimatedMarqueeProps) { + // Duplicate items to ensure smooth infinite loop + const marqueeItems = [...items, ...items, ...items, ...items] + + return ( +
+ + {marqueeItems.map((item, i) => ( +
+ {item} +
+ ))} +
+
+ ) +} diff --git a/components/ui/animated-skills.tsx b/components/ui/animated-skills.tsx new file mode 100644 index 0000000..6c39bad --- /dev/null +++ b/components/ui/animated-skills.tsx @@ -0,0 +1,39 @@ +"use client" +import { motion } from "framer-motion" + +const container = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05 + } + } +} + +const item = { + hidden: { opacity: 0, scale: 0.8 }, + visible: { opacity: 1, scale: 1 } +} + +export function AnimatedSkills({ skills }: { skills: string[] }) { + return ( + + {skills.map((skill) => ( + + {skill} + + ))} + + ) +} diff --git a/components/ui/animated-stats.tsx b/components/ui/animated-stats.tsx new file mode 100644 index 0000000..1c65506 --- /dev/null +++ b/components/ui/animated-stats.tsx @@ -0,0 +1,37 @@ +"use client" +import { motion } from "framer-motion" +import { AboutStat } from "@/src/entities/about" + +const container = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1 + } + } +} + +const item = { + hidden: { opacity: 0, y: 10 }, + visible: { opacity: 1, y: 0 } +} + +export function AnimatedStats({ stats }: { stats: AboutStat[] }) { + return ( + + {stats.map((stat) => ( + + {stat.label} + {stat.value} + + ))} + + ) +} diff --git a/components/ui/experience-card.tsx b/components/ui/experience-card.tsx index 592c58c..08b314c 100644 --- a/components/ui/experience-card.tsx +++ b/components/ui/experience-card.tsx @@ -2,6 +2,7 @@ import Image from "next/image" import { Experience } from "@/src/entities/experience" +import { motion } from "framer-motion" interface ExperienceCardProps { experience: Experience @@ -14,19 +15,21 @@ export function ExperienceCard({ experience, isEven }: ExperienceCardProps) { {/* Content Side */}
-
{experience.year} -

{experience.title}

+

{experience.title}

{experience.company}

{experience.description}

-
+
{/* Timeline Node (Image) */} diff --git a/components/ui/hero-image.tsx b/components/ui/hero-image.tsx new file mode 100644 index 0000000..587ec1a --- /dev/null +++ b/components/ui/hero-image.tsx @@ -0,0 +1,22 @@ +"use client" +import Image from 'next/image' +import { motion } from 'framer-motion' + +export function HeroImage() { + return ( + + Andres Sanchez + + ) +} diff --git a/components/ui/navbar.tsx b/components/ui/navbar.tsx index b9f1d34..f6fe30d 100644 --- a/components/ui/navbar.tsx +++ b/components/ui/navbar.tsx @@ -1,7 +1,38 @@ +"use client" import Image from 'next/image' import { ThemeToggle } from '@/components/ui/theme-toggle' +import { useState, useEffect } from 'react' +import { motion, useScroll } from 'framer-motion' export function Navbar() { + const { scrollYProgress } = useScroll() + const [activeSection, setActiveSection] = useState("") + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveSection(entry.target.id) + } + }) + }, + { rootMargin: "-20% 0px -80% 0px" } + ) + + const sections = document.querySelectorAll("section[id]") + sections.forEach((section) => observer.observe(section)) + + return () => observer.disconnect() + }, []) + + const navLinks = [ + { name: "About", href: "#about" }, + { name: "Projects", href: "#projects" }, + { name: "More", href: "#more-about" }, + { name: "Contact", href: "#contact" }, + ] + return ( <> Skip to content + + {/* Scroll Progress Bar */} + +