Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a644027
refactor: migrate src/di.ts into src/di/modules/ per clean architecture
AndSanG May 27, 2026
422a253
chore: expand local permissions to wildcard allow-all
AndSanG May 27, 2026
e182416
refactor: add presenter layer to interface-adapters
AndSanG May 27, 2026
a1c555e
feat: implement animated fluid blob background shapes in the hero sec…
AndSanG May 27, 2026
2585178
feat: implement react cache for about data and update UI components a…
AndSanG May 27, 2026
6cf913a
style: update hero description container width to full-width and remo…
AndSanG May 27, 2026
eb8b055
refactor: replace hero description container div with paragraph for s…
AndSanG May 27, 2026
fd93543
feat: implement mobile-responsive hero layout with conditional image …
AndSanG May 27, 2026
6d31fcc
style: reduce vertical padding across all section components from 24 …
AndSanG May 27, 2026
85ed310
feat: add expandable descriptions to experience cards and apply line-…
AndSanG May 27, 2026
8de3007
style: enforce uniform height and line clamping for project card cont…
AndSanG May 27, 2026
f082219
feat: add project thumbnails to project entities and display them in …
AndSanG May 27, 2026
610bd39
feat: back-to-top navigation component
AndSanG May 28, 2026
8d28186
refactor: hoist navbar
AndSanG May 28, 2026
d40440f
refactor: make cv button tactile
AndSanG May 28, 2026
ad66c8d
feat: implement expandable project cards with dynamic height animatio…
AndSanG May 28, 2026
8b1c0d6
refactor: update experience descriptions, project titles, and UI them…
AndSanG May 28, 2026
1c18959
feat: redesign projects section as Apple-style slide gallery
AndSanG May 28, 2026
f89a78b
docs: add todo list with i18n and o11y feature plans
AndSanG May 28, 2026
e26c7f2
fix: restore TactileButton shimmer and align icon/text inline
AndSanG May 28, 2026
8adc2a1
feat: upgrade CvDialog — TactileButton download, lazy-loaded markdown
AndSanG May 28, 2026
6a282e2
perf: wrap DI controllers with React.cache for per-request deduplication
AndSanG May 28, 2026
57ae89b
feat: animated SMIL SVG hamburger for mobile navbar
AndSanG May 28, 2026
871551a
feat: horizontal timeline gallery for experience section
AndSanG May 28, 2026
c92e5d7
fix: project gallery equal heights, no-grow on expand, UX polish
AndSanG May 28, 2026
c466693
fix: ensure text contrast passes WCAG AA in both light and dark modes
AndSanG May 28, 2026
8810964
fix: close CV dialog on Escape key press
AndSanG May 28, 2026
bf8d461
feat: liquid glass navbar — floating pill with strong blur and specul…
AndSanG May 28, 2026
2aacf09
feat: upgrade glass-card utility to liquid glass standard
AndSanG May 28, 2026
47dd511
feat: liquid glass ThemeToggle — frosted pill to match navbar
AndSanG May 28, 2026
85ca701
feat: liquid glass BackToTop — remove redundant opaque border override
AndSanG May 28, 2026
3e8706d
feat: liquid glass CvDialog — frosted surface, stronger backdrop blur
AndSanG May 28, 2026
4d84c39
feat: liquid glass contact icon circles — frosted surface with specul…
AndSanG May 28, 2026
c1c0630
feat: liquid glass ghost buttons — frosted glass surface on secondary…
AndSanG May 28, 2026
591ada5
feat: liquid glass ProjectCard — glass challenge block and tech badges
AndSanG May 28, 2026
a7a62b4
feat: liquid glass ExperienceCard timeline nodes — frosted company lo…
AndSanG May 28, 2026
39b8818
feat: liquid glass code snippet in MoreAbout — subtle glass tint on d…
AndSanG May 28, 2026
2594a03
feat: add backdrop-saturate-150 to all glass surfaces
AndSanG May 28, 2026
ed84970
feat: SVG displacement filter for liquid glass edge refraction
AndSanG May 28, 2026
bb73c59
feat: mouse-reactive 3D tilt on navbar pill and experience cards
AndSanG May 28, 2026
6ba792f
fix: revert SVG displacement filter — distorts text content
AndSanG May 28, 2026
4751597
feat: liquid glass ExperienceCard timeline details — softer lines and…
AndSanG May 28, 2026
b0236aa
feat: fade mask on experience gallery right edge for card hint
AndSanG May 28, 2026
773ed6e
fix: add -webkit-mask-image prefix for iOS Safari fade mask
AndSanG May 28, 2026
bd5e6dd
fix: steeper fade mask — start at 70% so hint card fades more aggress…
AndSanG May 28, 2026
df28927
fix: remove fade mask from experience gallery — hint card now shows g…
AndSanG May 28, 2026
2afa1bf
fix: remove hint spacer div — next card peeks naturally via scroll ov…
AndSanG May 28, 2026
8f70714
fix: contact circles visible in light mode — zinc tint replaces white…
AndSanG May 28, 2026
c5d3fa1
fix: glass-card visible in light mode — zinc tint replaces white-on-w…
AndSanG May 28, 2026
6cdf528
fix: halve section spacing and normalize heading margins
AndSanG May 29, 2026
24384b7
fix: align experience card elements — equalize title heights across c…
AndSanG May 29, 2026
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
8 changes: 6 additions & 2 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"permissions": {
"allow": [
"Bash(node -e \"const l = require\\(''lucide-react''\\); console.log\\(Object.keys\\(l\\).filter\\(k => k.toLowerCase\\(\\).includes\\(''github''\\) || k.toLowerCase\\(\\).includes\\(''linkedin''\\)\\)\\)\")",
"Bash(node -e \"const l = require\\(''lucide-react''\\); console.log\\(l.Github === l.GithubIcon, l.Linkedin === l.LinkedinIcon\\)\")"
"Bash(*)",
"Edit(*)",
"Write(*)",
"Read(*)",
"Glob(*)",
"Grep(*)"
]
}
}
40 changes: 16 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# andsang.github.io

## Getting Started
Personal portfolio site for Andrés Sánchez. Built with Next.js (App Router, TypeScript, Tailwind CSS), deployed to GitHub Pages as a static export.

First, run the development server:
## Dev

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
npx next dev --port 3000 --webpack
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
> Use `--webpack` — Turbopack panics with `output: 'export'` mode in Next.js 16.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
Open [http://localhost:3000](http://localhost:3000).

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Build

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
```bash
npm run build
```

## Deploy on Vercel
Outputs to `out/` via static export.

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
## Stack

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
- Next.js 16 (App Router, static export)
- TypeScript
- Tailwind CSS
- Clean Architecture (entities → use cases → interface adapters → infrastructure)
69 changes: 68 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
--foreground: #171717;
--accent: #6366f1;
--secondary: #10b981;
--blob-primary: #06e2ff;
/* suit blue — hue 220°, full saturation */
--blob-secondary: #d97706;
/* tie gold — hue 38°, full saturation */
--blob-tertiary: #9333ea;
/* shirt lavender — hue 285°, full saturation */
}

/* Dark Theme (Class-based) */
Expand All @@ -35,12 +41,73 @@ body {

@layer components {
.glass-card {
@apply bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl shadow-2xl;
@apply bg-zinc-100/80 dark:bg-zinc-900/40 backdrop-blur-2xl backdrop-saturate-150 border border-zinc-200 dark:border-white/10 rounded-2xl shadow-lg shadow-black/8 dark:shadow-black/25 ring-1 ring-inset ring-zinc-100 dark:ring-white/5;
}

.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
}

@keyframes shine {
100% {
transform: translateX(200%);
}
}

@keyframes floatFluid {
0% {
transform: translate(0, 0) scale(1) rotate(0deg);
}

100% {
transform: translate(30px, 50px) scale(1.1) rotate(10deg);
}
}

.fluid-shape {
position: absolute;
border-radius: 50%;
filter: blur(50px);
opacity: 0.8;
animation: floatFluid 20s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate;
pointer-events: none;
}

.fluid-shape-1 {
width: 60vh;
height: 60vh;
top: -10%;
right: -10%;
background: radial-gradient(circle, var(--blob-primary), transparent 65%);
animation-delay: 0s;
}

.fluid-shape-2 {
width: 50vh;
height: 50vh;
bottom: -10%;
left: -10%;
background: radial-gradient(circle, var(--blob-secondary), transparent 65%);
animation-delay: -5s;
}

.fluid-shape-3 {
width: 30vh;
height: 30vh;
top: 40%;
left: 40%;
background: radial-gradient(circle, var(--blob-tertiary), transparent 65%);
animation-delay: -10s;
opacity: 0.65;
}

@media (prefers-reduced-motion: reduce) {
.fluid-shape {
animation: none;
}
}
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function RootLayout({
<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>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
Expand Down
2 changes: 2 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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'
import { BackToTop } from '@/components/ui/back-to-top'

export default function Home() {
return (
Expand All @@ -19,6 +20,7 @@ export default function Home() {
<MoreAboutSection />
<ContactSection />
</PageTransition>
<BackToTop />
</main>
)
}
8 changes: 4 additions & 4 deletions components/sections/about.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { aboutController } from "@/src/di"
import { getAbout } from "@/src/di"
import { Reveal } from "@/components/ui/reveal"
import { AnimatedStats } from "@/components/ui/animated-stats"
import { AnimatedMarquee } from '@/components/ui/animated-marquee'

export async function AboutSection() {
const about = await aboutController.getAbout()
const about = await 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">
<section id="about" className="bg-background dark:bg-black py-8 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">
<Reveal>
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-16 text-center">
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-8 text-center">
About <span className="text-accent">Me</span>
</h2>
</Reveal>
Expand Down
12 changes: 6 additions & 6 deletions components/sections/contact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ export async function ContactSection() {
const contact = await contactController.getContact()

return (
<section id="contact" className="py-24 bg-background dark:bg-zinc-950 border-t border-zinc-200 dark:border-zinc-900">
<section id="contact" className="py-8 bg-background dark:bg-zinc-950 border-t border-zinc-200 dark:border-zinc-900">
<div className="container mx-auto px-6 md:px-12 text-center">
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-12">
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-8">
Get in <span className="text-accent">Touch</span>
</h2>

<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 justify-items-center">
{/* Location */}
<div className="flex flex-col items-center gap-3 text-center">
<div className="p-4 rounded-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 text-accent">
<div className="p-4 rounded-full bg-zinc-100/80 dark:bg-zinc-900/50 backdrop-blur-md backdrop-saturate-150 border border-zinc-200 dark:border-white/10 ring-1 ring-inset ring-zinc-100 dark:ring-white/5 text-accent">
<MapPin className="w-6 h-6" />
</div>
<div>
Expand All @@ -25,23 +25,23 @@ export async function ContactSection() {

{/* Email */}
<a href={`mailto:${contact.email}`} className="group flex flex-col items-center gap-3 text-center">
<div className="p-4 rounded-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 text-accent group-hover:border-accent group-hover:bg-accent/10 transition-all duration-300">
<div className="p-4 rounded-full bg-zinc-100/80 dark:bg-zinc-900/50 backdrop-blur-md backdrop-saturate-150 border border-zinc-200 dark:border-white/10 ring-1 ring-inset ring-zinc-100 dark:ring-white/5 text-accent group-hover:border-accent/50 group-hover:bg-accent/10 transition-all duration-300">
<Mail className="w-6 h-6" />
</div>
<span className="text-zinc-700 dark:text-gray-300 group-hover:text-zinc-900 dark:group-hover:text-white transition-colors text-sm">{contact.email}</span>
</a>

{/* LinkedIn */}
<a href={contact.linkedinUrl} target="_blank" rel="noopener noreferrer" className="group flex flex-col items-center gap-3 text-center">
<div className="p-4 rounded-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 text-accent group-hover:border-accent group-hover:bg-accent/10 transition-all duration-300">
<div className="p-4 rounded-full bg-zinc-100/80 dark:bg-zinc-900/50 backdrop-blur-md backdrop-saturate-150 border border-zinc-200 dark:border-white/10 ring-1 ring-inset ring-zinc-100 dark:ring-white/5 text-accent group-hover:border-accent/50 group-hover:bg-accent/10 transition-all duration-300">
<LinkedinIcon className="w-6 h-6" />
</div>
<span className="text-zinc-700 dark:text-gray-300 group-hover:text-zinc-900 dark:group-hover:text-white transition-colors">LinkedIn</span>
</a>

{/* GitHub */}
<a href={contact.githubUrl} target="_blank" rel="noopener noreferrer" className="group flex flex-col items-center gap-3 text-center">
<div className="p-4 rounded-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 text-accent group-hover:border-accent group-hover:bg-accent/10 transition-all duration-300">
<div className="p-4 rounded-full bg-zinc-100/80 dark:bg-zinc-900/50 backdrop-blur-md backdrop-saturate-150 border border-zinc-200 dark:border-white/10 ring-1 ring-inset ring-zinc-100 dark:ring-white/5 text-accent group-hover:border-accent/50 group-hover:bg-accent/10 transition-all duration-300">
<GithubIcon className="w-6 h-6" />
</div>
<span className="text-zinc-700 dark:text-gray-300 group-hover:text-zinc-900 dark:group-hover:text-white transition-colors">GitHub</span>
Expand Down
27 changes: 5 additions & 22 deletions components/sections/experience.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import { ExperienceCard } from "@/components/ui/experience-card"
import { ExperienceList } from "@/components/ui/experience-list"
import { experienceController } from "@/src/di"

export async function ExperienceSection() {
const experiences = await experienceController.getExperiences()

return (
<section id="experience" 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-4 md:px-12 relative z-10">
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-20 text-center">
<section id="experience" className="bg-background dark:bg-black py-8 border-t border-zinc-200 dark:border-zinc-900">
<div className="container mx-auto px-6 md:px-12">
<h2 className="text-3xl md:text-5xl font-bold text-foreground dark:text-white mb-8">
Professional <span className="text-accent">Journey</span>
</h2>

<div className="relative max-w-5xl mx-auto">
{/* Vertical Center Line */}
<div className="absolute left-4 md:left-1/2 top-0 bottom-0 w-1 md:w-1.5 bg-zinc-300 dark:bg-zinc-800 md:-translate-x-1/2 rounded-full hidden md:block"></div>

{/* Mobile Line (Left aligned) */}
<div className="absolute left-8 top-0 bottom-0 w-1 bg-zinc-300 dark:bg-zinc-800 md:hidden rounded-full"></div>


<div className="space-y-12 md:space-y-0">
{experiences.map((exp, index) => (
<ExperienceCard
key={exp.id}
experience={exp}
isEven={index % 2 === 0}
/>
))}
</div>
</div>
<ExperienceList experiences={experiences} />
</div>
</section>
)
Expand Down
35 changes: 22 additions & 13 deletions components/sections/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import { aboutController } from '@/src/di'
import { getAbout } from '@/src/di'
import { CvDialog } from '@/components/ui/cv-dialog'
import { HeroImage } from '@/components/ui/hero-image'
import { TactileButton } from '@/components/ui/tactile-button'

export async function Hero() {
const about = await aboutController.getAbout()
const about = await getAbout()

return (
<section id="hero" className="relative w-full min-h-screen flex items-center bg-background dark:bg-black text-foreground dark:text-white overflow-x-hidden pt-20">
<section id="hero" className="relative w-full min-h-screen flex items-center bg-background dark:bg-black text-foreground dark:text-white overflow-x-hidden pt-20 pb-12 md:pb-0">
<div className="container mx-auto px-6 md:px-12 grid grid-cols-1 md:grid-cols-2 gap-12 items-center z-10">
{/* Text Column */}
<div className="flex flex-col items-start gap-6 order-2 md:order-1">
<h1 className="text-4xl md:text-5xl font-bold leading-tight">
{about.name}
</h1>
<h1 className="text-4xl md:text-3xl font-bold leading-tight">
<h2 className="text-4xl md:text-3xl font-bold leading-tight">
Developing <br />
<span className="text-accent">High-Availability</span> <br />
Mobile Apps
</h1>
<div className="flex flex-col gap-3 text-zinc-500 dark:text-gray-400 text-lg md:text-xl max-w-lg leading-relaxed">
</h2>
<p className="text-zinc-500 dark:text-gray-400 text-lg md:text-xl leading-relaxed w-full">
{about.heroDescription.map((line, i) => (
<p key={i}>{line}</p>
<span key={i} className="md:block">{line}{' '}</span>
))}
</p>

{/* Mobile-only image, between description and CTAs */}
<div className="md:hidden w-full flex items-center justify-center">
<HeroImage />
</div>

<div className="flex gap-4 mt-4 items-center">
<div className="flex flex-wrap gap-3 mt-2 md: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"
Expand All @@ -34,23 +39,27 @@ export async function Hero() {
</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"
className="px-8 py-3 rounded-xl border border-white/30 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-sm backdrop-saturate-150 text-zinc-600 dark:text-gray-400 font-semibold hover:bg-white/50 dark:hover:bg-zinc-900/40 hover:text-zinc-900 dark:hover:text-white transition-all duration-300"
>
Contact Me
</TactileButton>
<CvDialog />
</div>
</div>

{/* Image Column */}
<div className="order-1 md:order-2 flex items-center justify-center">
{/* Image Column — desktop only */}
<div className="hidden md:flex md:order-2 items-center justify-center">
<HeroImage />
</div>

</div>

{/* Background Glow Effect */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-[#7c3aed] opacity-5 blur-[120px] rounded-full pointer-events-none" />
{/* Background Fluid Blobs */}
<div className="absolute inset-0 overflow-hidden pointer-events-none z-0">
<div className="fluid-shape fluid-shape-1" />
<div className="fluid-shape fluid-shape-2" />
<div className="fluid-shape fluid-shape-3" />
</div>
</section>
)
}
8 changes: 4 additions & 4 deletions components/sections/more-about.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Image from 'next/image'
import { aboutController } from "@/src/di"
import { getAbout } from "@/src/di"

export async function MoreAboutSection() {
const about = await aboutController.getAbout()
const about = await getAbout()

return (
<section id="more-about" className="bg-background dark:bg-black py-24 border-t border-zinc-200 dark:border-zinc-900 relative overflow-hidden">
<section id="more-about" className="bg-background dark:bg-black py-8 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 grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
{/* Text Column */}
<div className="flex flex-col gap-6 order-2 md:order-1">
Expand All @@ -24,7 +24,7 @@ export async function MoreAboutSection() {
</p>

{/* Swift-styled code snippet */}
<div className="rounded-2xl bg-zinc-950 border border-zinc-800 px-6 py-5 font-mono text-sm md:text-base w-full overflow-x-auto">
<div className="rounded-2xl bg-zinc-950/85 backdrop-blur-sm backdrop-saturate-150 border border-white/8 ring-1 ring-inset ring-white/5 px-6 py-5 font-mono text-sm md:text-base w-full overflow-x-auto">
<span className="text-purple-400">let</span>
<span className="text-white"> hobbies</span>
<span className="text-zinc-400"> = </span>
Expand Down
Loading
Loading