Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -89,6 +92,7 @@ jobs:

# Deployment job
deploy:
if: github.event_name != 'pull_request'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
Expand Down
25 changes: 20 additions & 5 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%);
}
}
17 changes: 13 additions & 4 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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"],
});

Expand All @@ -27,9 +32,13 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${openSans.variable} antialiased font-sans`}
className={`${inter.variable} ${outfit.variable} antialiased font-sans relative`}
suppressHydrationWarning
>
<div className="fixed inset-0 z-[-1] pointer-events-none overflow-hidden hidden dark:block">
<div className="absolute top-[-10%] left-[-10%] w-[50vw] h-[50vw] rounded-full bg-indigo-500/5 blur-[100px]" />
<div className="absolute bottom-[-10%] right-[-10%] w-[50vw] h-[50vw] rounded-full bg-purple-900/5 blur-[100px]" />
</div>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
Expand Down
15 changes: 9 additions & 6 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<main id="main-content" className="min-h-screen bg-background">
<Navbar />
<Hero />
<AboutSection />
<ProjectsSection />
<ExperienceSection />
<MoreAboutSection />
<ContactSection />
<PageTransition>
<Hero />
<AboutSection />
<ProjectsSection />
<ExperienceSection />
<MoreAboutSection />
<ContactSection />
</PageTransition>
</main>
)
}
46 changes: 22 additions & 24 deletions components/sections/about.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,45 @@
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()

return (
<section id="about" className="bg-background dark:bg-black py-24 border-t border-zinc-200 dark:border-zinc-900 relative overflow-hidden">
<div className="container mx-auto px-6 md:px-12 relative z-10">
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-16 text-center">
About <span className="text-accent">Me</span>
</h2>
<Reveal>
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-16 text-center">
About <span className="text-accent">Me</span>
</h2>
</Reveal>

<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12 items-start">
{/* Bio */}
<div className="md:col-span-2 flex flex-col gap-5">
{about.bio.map((paragraph, i) => (
<p key={i} className="text-zinc-600 dark:text-gray-300 text-lg leading-relaxed">
{paragraph}
</p>
))}
<Reveal delay={0.2}>
<div className="flex flex-col gap-5">
{about.bio.map((paragraph, i) => (
<p key={i} className="text-zinc-600 dark:text-gray-300 text-lg leading-relaxed">
{paragraph}
</p>
))}
</div>
</Reveal>
</div>

{/* Quick Stats */}
<div className="rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6 flex flex-col gap-5">
{about.stats.map((stat) => (
<StatItem key={stat.label} stat={stat} />
))}
</div>
<AnimatedStats stats={about.stats} />
</div>

{/* Skills */}
<div className="max-w-4xl mx-auto mt-16">
<h3 className="text-xl font-semibold text-foreground dark:text-white mb-6">Core Skills</h3>
<div className="flex flex-wrap gap-3">
{about.skills.map((skill) => (
<span
key={skill}
className="px-4 py-2 rounded-xl bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-gray-300 text-sm font-medium border border-zinc-200 dark:border-zinc-800"
>
{skill}
</span>
))}
</div>
<Reveal delay={0.3}>
<h3 className="text-xl font-semibold text-foreground dark:text-white mb-6">Core Skills</h3>
</Reveal>
<AnimatedSkills skills={about.skills} />
</div>
</div>

Expand Down
32 changes: 14 additions & 18 deletions components/sections/hero.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -24,37 +27,30 @@ export async function Hero() {
))}
</div>

<div className="flex gap-4 mt-4">
<a
<div className="flex gap-4 mt-4 items-center">
<TactileButton
href="#projects"
className="px-8 py-3 rounded-xl border-2 border-accent text-accent font-semibold hover:bg-accent hover:text-black transition-all duration-300"
>
View Work
</a>
<a
</TactileButton>
<TactileButton
href="#contact"
className="px-8 py-3 rounded-xl border-2 border-transparent text-zinc-500 dark:text-gray-400 font-semibold hover:text-zinc-900 dark:hover:text-white transition-all duration-300"
>
Contact Me
</a>
</TactileButton>
<CvDialog />
</div>

<div className="mt-12 w-full max-w-lg">
<AnimatedMarquee items={about.skills} />
</div>
</div>

{/* Image Column */}
<div className="relative h-[400px] md:h-[600px] w-full order-1 md:order-2">
{/* Gradient Mask to blend image into black background */}



<Image
src="/profile-cutout.webp"
alt="Andres Sanchez"
fill
className="object-cover"
priority
sizes="(max-width: 768px) 100vw, 50vw"
/>
<div className="order-1 md:order-2 flex items-center justify-center">
<HeroImage />
</div>
</div>

Expand Down
38 changes: 38 additions & 0 deletions components/ui/animated-marquee.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="w-full overflow-hidden flex items-center py-4"
style={{ WebkitMaskImage: "linear-gradient(to right, transparent, black 10%, black 90%, transparent)", maskImage: "linear-gradient(to right, transparent, black 10%, black 90%, transparent)" }}
>
<motion.div
className="flex gap-12 whitespace-nowrap min-w-max"
animate={{ x: ["0%", "-50%"] }}
transition={{
repeat: Infinity,
ease: "linear",
duration: 30,
}}
>
{marqueeItems.map((item, i) => (
<div
key={i}
className="text-zinc-500 dark:text-zinc-500 font-heading tracking-wider font-semibold text-sm uppercase flex items-center"
>
{item}
</div>
))}
</motion.div>
</div>
)
}
39 changes: 39 additions & 0 deletions components/ui/animated-skills.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<motion.div
className="flex flex-wrap gap-3"
variants={container}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
{skills.map((skill) => (
<motion.span
key={skill}
variants={item}
className="px-4 py-2 rounded-xl bg-zinc-100 dark:bg-zinc-900/50 text-zinc-700 dark:text-gray-300 text-sm font-medium border border-zinc-200 dark:border-zinc-800/50"
>
{skill}
</motion.span>
))}
</motion.div>
)
}
37 changes: 37 additions & 0 deletions components/ui/animated-stats.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<motion.div
className="rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6 flex flex-col gap-5 glass-card"
variants={container}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
{stats.map((stat) => (
<motion.div key={stat.label} variants={item} className="flex flex-col gap-1">
<span className="text-xs font-medium text-zinc-400 dark:text-gray-500 uppercase tracking-wider">{stat.label}</span>
<span className="text-lg font-bold text-foreground dark:text-white">{stat.value}</span>
</motion.div>
))}
</motion.div>
)
}
Loading
Loading