diff --git a/components/floating-title.tsx b/components/floating-title.tsx index 461ebab..2878841 100644 --- a/components/floating-title.tsx +++ b/components/floating-title.tsx @@ -1,126 +1,126 @@ -"use client" - -import { useRef, useEffect, useState } from "react" -import { useScrollContext } from "@/lib/scroll-context" -import { useTranslations, getTranslation } from "@/lib/i18n-context" - -/** - * Fixed-position element that interpolates between the hero h1 and the navbar - * brand text during scroll, creating a continuous "dock into navbar" effect. - * - * Uses translate3d + scale for GPU-composited animation. - * Purely decorative — the real heading and nav link remain in the DOM. - * - * Opacity-gated: the clone mounts with opacity 0, applies exact computed styles - * from the hero h1 (read via getComputedStyle), and only becomes visible after - * the first rAF tick to guarantee no unstyled flash frame. - */ -export function FloatingTitle() { - const { - scrollY, - scrollProgress, - heroTitleRect, - navTitleRect, - heroComputedStyles, - navFontSize, - isFloatingTitleActive, - } = useScrollContext() - const [translations] = useTranslations() - const [revealed, setRevealed] = useState(false) - const revealRafRef = useRef(0) - - // Determine if the clone should be in the DOM at all. - // Uses isFloatingTitleActive from context which includes: !prefersReducedMotion, - // fontsReady, heroWasVisible, 0 < scrollProgress < 1 - const shouldMount = - isFloatingTitleActive && - heroTitleRect != null && - navTitleRect != null && - heroComputedStyles != null && - navFontSize != null - - // Opacity-gated reveal: mount with opacity 0, then reveal after one rAF - // so the browser has time to apply computed styles before painting. - useEffect(() => { - if (shouldMount && !revealed) { - revealRafRef.current = requestAnimationFrame(() => { - setRevealed(true) - }) - } - if (!shouldMount && revealed) { - setRevealed(false) - } - return () => { - if (revealRafRef.current) cancelAnimationFrame(revealRafRef.current) - } - }, [shouldMount, revealed]) - - if (!shouldMount || !heroTitleRect || !navTitleRect || !heroComputedStyles || !navFontSize) { - return null - } - - const t = easeInOutCubic(scrollProgress) - - // Hero rect is in document coords — convert to viewport - const heroViewportTop = heroTitleRect.top - scrollY - const heroViewportLeft = heroTitleRect.left - - // Nav rect is already in viewport coords (fixed navbar) - const navViewportTop = navTitleRect.top - const navViewportLeft = navTitleRect.left - - // Position the element at the hero's initial viewport position, - // then translate toward the nav position - const deltaX = (navViewportLeft - heroViewportLeft) * t - const deltaY = (navViewportTop - heroViewportTop) * t - - // Scale based on font-size ratio (not element height) so line-wrapping - // on mobile doesn't cause the title to shrink more than it should. - const heroFontPx = parseFloat(heroComputedStyles.fontSize) - const navFontPx = parseFloat(navFontSize) - const targetScale = navFontPx / heroFontPx - const scale = lerp(1, targetScale, t) - - const name = getTranslation(translations, "hero.name", "Alejandro Repetto") - - return ( - - ) -} - -function lerp(a: number, b: number, t: number): number { - return a + (b - a) * t -} - -function easeInOutCubic(t: number): number { - return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2 -} +"use client" + +import { useRef, useEffect, useState } from "react" +import { useScrollContext } from "@/lib/scroll-context" +import { useTranslations, getTranslation } from "@/lib/i18n-context" + +/** + * Fixed-position element that interpolates between the hero h1 and the navbar + * brand text during scroll, creating a continuous "dock into navbar" effect. + * + * Uses translate3d + scale for GPU-composited animation. + * Purely decorative — the real heading and nav link remain in the DOM. + * + * Opacity-gated: the clone mounts with opacity 0, applies exact computed styles + * from the hero h1 (read via getComputedStyle), and only becomes visible after + * the first rAF tick to guarantee no unstyled flash frame. + */ +export function FloatingTitle() { + const { + scrollY, + scrollProgress, + heroTitleRect, + navTitleRect, + heroComputedStyles, + navFontSize, + isFloatingTitleActive, + } = useScrollContext() + const [translations] = useTranslations() + const [revealed, setRevealed] = useState(false) + const revealRafRef = useRef(0) + + // Determine if the clone should be in the DOM at all. + // Uses isFloatingTitleActive from context which includes: !prefersReducedMotion, + // fontsReady, heroWasVisible, 0 < scrollProgress < 1 + const shouldMount = + isFloatingTitleActive && + heroTitleRect != null && + navTitleRect != null && + heroComputedStyles != null && + navFontSize != null + + // Opacity-gated reveal: mount with opacity 0, then reveal after one rAF + // so the browser has time to apply computed styles before painting. + useEffect(() => { + if (shouldMount && !revealed) { + revealRafRef.current = requestAnimationFrame(() => { + setRevealed(true) + }) + } + if (!shouldMount && revealed) { + setRevealed(false) + } + return () => { + if (revealRafRef.current) cancelAnimationFrame(revealRafRef.current) + } + }, [shouldMount, revealed]) + + if (!shouldMount || !heroTitleRect || !navTitleRect || !heroComputedStyles || !navFontSize) { + return null + } + + const t = easeInOutCubic(scrollProgress) + + // Hero rect is in document coords — convert to viewport + const heroViewportTop = heroTitleRect.top - scrollY + const heroViewportLeft = heroTitleRect.left + + // Nav rect is already in viewport coords (fixed navbar) + const navViewportTop = navTitleRect.top + const navViewportLeft = navTitleRect.left + + // Position the element at the hero's initial viewport position, + // then translate toward the nav position + const deltaX = (navViewportLeft - heroViewportLeft) * t + const deltaY = (navViewportTop - heroViewportTop) * t + + // Scale based on font-size ratio (not element height) so line-wrapping + // on mobile doesn't cause the title to shrink more than it should. + const heroFontPx = parseFloat(heroComputedStyles.fontSize) + const navFontPx = parseFloat(navFontSize) + const targetScale = navFontPx / heroFontPx + const scale = lerp(1, targetScale, t) + + const name = getTranslation(translations, "hero.name") + + return ( + + ) +} + +function lerp(a: number, b: number, t: number): number { + return a + (b - a) * t +} + +function easeInOutCubic(t: number): number { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2 +} diff --git a/components/navigation.tsx b/components/navigation.tsx index 9117670..549d46c 100644 --- a/components/navigation.tsx +++ b/components/navigation.tsx @@ -1,90 +1,90 @@ -"use client" - -import { useState } from "react" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { Menu, X } from "lucide-react" -import { LanguageSwitcher } from "@/components/language-switcher" -import { ThemeToggle } from "@/components/theme-toggle" -import { useTranslations, getTranslation } from "@/lib/i18n-context" -import { useScrollContext } from "@/lib/scroll-context" -import { cn } from "@/lib/utils" - -export function Navigation() { - const [isOpen, setIsOpen] = useState(false) - const [translations, locale, loading] = useTranslations() - const { registerNavTitleRef, isFloatingTitleActive, scrollProgress, fontsReady } = useScrollContext() - - const navItems = [ - { href: "#about", label: getTranslation(translations, "navigation.about", "About") }, - { href: "#projects", label: getTranslation(translations, "navigation.projects", "Projects") }, - { href: "#contact", label: getTranslation(translations, "navigation.contact", "Contact") }, - ] - - return ( - - ) -} +"use client" + +import { useState } from "react" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Menu, X } from "lucide-react" +import { LanguageSwitcher } from "@/components/language-switcher" +import { ThemeToggle } from "@/components/theme-toggle" +import { useTranslations, getTranslation } from "@/lib/i18n-context" +import { useScrollContext } from "@/lib/scroll-context" +import { cn } from "@/lib/utils" + +export function Navigation() { + const [isOpen, setIsOpen] = useState(false) + const [translations, locale] = useTranslations() + const { registerNavTitleRef, isFloatingTitleActive, scrollProgress, fontsReady } = useScrollContext() + + const navItems = [ + { href: "#about", label: getTranslation(translations, "navigation.about") }, + { href: "#projects", label: getTranslation(translations, "navigation.projects") }, + { href: "#contact", label: getTranslation(translations, "navigation.contact") }, + ] + + return ( + + ) +} diff --git a/components/project-detail-modal.tsx b/components/project-detail-modal.tsx index c9c1ced..1558ddb 100644 --- a/components/project-detail-modal.tsx +++ b/components/project-detail-modal.tsx @@ -1,154 +1,154 @@ -"use client" - -import { useState } from "react" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { ScrollArea } from "@/components/ui/scroll-area" -import { ImageLightbox } from "@/components/ui/image-lightbox" -import { Github, ExternalLink } from "lucide-react" -import Link from "next/link" -import Image from "next/image" -import { useTranslations, getTranslation } from "@/lib/i18n-context" -import type { Project } from "@/types/project" - -interface ProjectDetailModalProps { - project: Project | null - isOpen: boolean - onClose: () => void -} - -export function ProjectDetailModal({ project, isOpen, onClose }: ProjectDetailModalProps) { - const [translations, locale] = useTranslations() - const [lightboxOpen, setLightboxOpen] = useState(false) - const [lightboxIndex, setLightboxIndex] = useState(0) - - if (!project) return null - - const content = project.locales[locale] || project.locales["en"] - - const handleOpenChange = (open: boolean) => { - if (!open) onClose() - } - - return ( - <> - - - -
- - {getTranslation(translations, `projects.categories.${project.category}`, project.category)} - - - {getTranslation(translations, `projects.status.${project.status}`, project.status)} - -
- {content.title} - {content.short} -
- - -
- {/* Image thumbnails */} - {project.images?.length > 0 && ( -
- {project.images.map((img, i) => ( - - ))} -
- )} - - {/* Description */} -

- {content.description} -

- - {/* Features */} - {content.features?.length > 0 && ( -
-

- {getTranslation(translations, "projects.features", "Key Features")} -

-
    - {content.features.map((f, i) => ( -
  • -
    - {f} -
  • - ))} -
-
- )} - - {/* Tech stack */} -
-

- {getTranslation(translations, "projects.techStack", "Technology Stack")} -

-
- {project.techStack.map((t) => ( - - {t} - - ))} -
-
- - {/* Links */} -
- {project.githubUrl && ( - - )} - {project.demoUrl && ( - - )} -
-
-
-
-
- - {/* Lightbox as sibling to Dialog to avoid z-index/focus conflicts */} - setLightboxOpen(false)} - projectTitle={content.title} - /> - - ) -} +"use client" + +import { useState } from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { ImageLightbox } from "@/components/ui/image-lightbox" +import { Github, ExternalLink } from "lucide-react" +import Link from "next/link" +import Image from "next/image" +import { useTranslations, getTranslation } from "@/lib/i18n-context" +import type { Project } from "@/types/project" + +interface ProjectDetailModalProps { + project: Project | null + isOpen: boolean + onClose: () => void +} + +export function ProjectDetailModal({ project, isOpen, onClose }: ProjectDetailModalProps) { + const [translations, locale] = useTranslations() + const [lightboxOpen, setLightboxOpen] = useState(false) + const [lightboxIndex, setLightboxIndex] = useState(0) + + if (!project) return null + + const content = project.locales[locale] || project.locales["en"] + + const handleOpenChange = (open: boolean) => { + if (!open) onClose() + } + + return ( + <> + + + +
+ + {getTranslation(translations, `projects.categories.${project.category}`, project.category)} + + + {getTranslation(translations, `projects.status.${project.status}`, project.status)} + +
+ {content.title} + {content.short} +
+ + +
+ {/* Image thumbnails */} + {project.images?.length > 0 && ( +
+ {project.images.map((img, i) => ( + + ))} +
+ )} + + {/* Description */} +

+ {content.description} +

+ + {/* Features */} + {content.features?.length > 0 && ( +
+

+ {getTranslation(translations, "projects.features")} +

+
    + {content.features.map((f, i) => ( +
  • +
    + {f} +
  • + ))} +
+
+ )} + + {/* Tech stack */} +
+

+ {getTranslation(translations, "projects.techStack")} +

+
+ {project.techStack.map((t) => ( + + {t} + + ))} +
+
+ + {/* Links */} +
+ {project.githubUrl && ( + + )} + {project.demoUrl && ( + + )} +
+
+
+
+
+ + {/* Lightbox as sibling to Dialog to avoid z-index/focus conflicts */} + setLightboxOpen(false)} + projectTitle={content.title} + /> + + ) +} diff --git a/components/project-timeline.tsx b/components/project-timeline.tsx index 0da85b1..68f4ff6 100644 --- a/components/project-timeline.tsx +++ b/components/project-timeline.tsx @@ -1,116 +1,116 @@ -"use client" - -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Calendar, CheckCircle2 } from "lucide-react" -import { cn } from "@/lib/utils" -import { useTranslations, getTranslation } from "@/lib/i18n-context" - -interface TimelineEvent { - date: string - title: string | { en: string; es: string } - description: string | { en: string; es: string } -} - -interface ProjectTimelineProps { - timeline: TimelineEvent[] - className?: string -} - -export function ProjectTimeline({ timeline, className }: ProjectTimelineProps) { - const [translations, locale] = useTranslations() - - // Parse dates and find current/future events - const now = new Date() - - // Helper to parse date as local date (not UTC) - const parseLocalDate = (dateString: string) => { - const [year, month, day] = dateString.split('-').map(Number) - return new Date(year, month - 1, day) // month is 0-indexed in JS - } - - // Helper to get localized content - const getLocalizedContent = (content: string | { en: string; es: string }) => { - if (typeof content === 'string') return content - return content[locale as 'en' | 'es'] || content.en - } - - return ( - - - - - {getTranslation(translations, "projects.timeline.title", "Project Timeline")} - - - -
- {/* Vertical line */} -
- - {timeline.map((event, index) => { - const eventDate = parseLocalDate(event.date) - const isPast = eventDate <= now - const isToday = eventDate.toDateString() === now.toDateString() - - return ( -
- {/* Timeline dot */} -
- {isPast && !isToday && ( - - )} -
- -
-
- - {isToday && ( - - {getTranslation(translations, "projects.timeline.today", "Today")} - - )} -
-

- {getLocalizedContent(event.title)} -

-

- {getLocalizedContent(event.description)} -

-
-
- ) - })} -
- - - ) -} +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Calendar, CheckCircle2 } from "lucide-react" +import { cn } from "@/lib/utils" +import { useTranslations, getTranslation } from "@/lib/i18n-context" + +interface TimelineEvent { + date: string + title: string | { en: string; es: string } + description: string | { en: string; es: string } +} + +interface ProjectTimelineProps { + timeline: TimelineEvent[] + className?: string +} + +export function ProjectTimeline({ timeline, className }: ProjectTimelineProps) { + const [translations, locale] = useTranslations() + + // Parse dates and find current/future events + const now = new Date() + + // Helper to parse date as local date (not UTC) + const parseLocalDate = (dateString: string) => { + const [year, month, day] = dateString.split('-').map(Number) + return new Date(year, month - 1, day) // month is 0-indexed in JS + } + + // Helper to get localized content + const getLocalizedContent = (content: string | { en: string; es: string }) => { + if (typeof content === 'string') return content + return content[locale as 'en' | 'es'] || content.en + } + + return ( + + + + + {getTranslation(translations, "projects.timeline.title")} + + + +
+ {/* Vertical line */} +
+ + {timeline.map((event, index) => { + const eventDate = parseLocalDate(event.date) + const isPast = eventDate <= now + const isToday = eventDate.toDateString() === now.toDateString() + + return ( +
+ {/* Timeline dot */} +
+ {isPast && !isToday && ( + + )} +
+ +
+
+ + {isToday && ( + + {getTranslation(translations, "projects.timeline.today")} + + )} +
+

+ {getLocalizedContent(event.title)} +

+

+ {getLocalizedContent(event.description)} +

+
+
+ ) + })} +
+ + + ) +} diff --git a/components/sections/about-section.tsx b/components/sections/about-section.tsx index a6d8a0e..df495d5 100644 --- a/components/sections/about-section.tsx +++ b/components/sections/about-section.tsx @@ -1,65 +1,65 @@ -"use client" - -import { Container } from "@/components/layout/container" -import { Section } from "@/components/layout/section" -import { SectionHeader } from "@/components/layout/section-header" -import { useTranslations, getTranslation } from "@/lib/i18n-context" - -export function AboutSection() { - const [translations] = useTranslations() - - return ( -
- -
- - -
- {/* Background */} -
-

- {getTranslation(translations, "about.bio.background.title", "Background")} -

-
- {getTranslation( - translations, - "about.bio.background.text", - "I've been programming seriously since 2022, when I placed 6th in my first ICPC tournament. A 2024 winter training camp was a turning point that transformed how I approach technical challenges.\n\nI'm learning distributed systems and design patterns through hands-on implementation: building load balancers, message brokers, and experimenting with database architectures.\n\nMy projects range from NASA Space Apps prediction algorithms to production systems with RAG architectures. I don't just use technologies. I dig into how they work and when they actually make sense architecturally.", - ) - .split("\n\n") - .map((paragraph, index) => ( -

{paragraph}

- ))} -
-
- - {/* Approach */} -
-

- {getTranslation(translations, "about.bio.approach.title", "Approach")} -

-
- {getTranslation( - translations, - "about.bio.approach.text", - "I think about efficiency first. Competitive programming trained me to analyze complexity before coding. But I'm pragmatic about execution: MVPs ship fast, production systems are built to last.\n\nMy process: research what exists, understand the problem deeply, consult when needed, then iterate rapidly. I focus on writing code that's maintainable and scalable without over-engineering early stages.\n\nI work well independently and in teams, and I adapt to what the project needs. Local hackathon wins and real-world deployments both prove the same thing: I deliver solutions that work now and scale later.", - ) - .split("\n\n") - .map((paragraph, index) => ( -

{paragraph}

- ))} -
-
-
-
-
-
- ) -} +"use client" + +import { Container } from "@/components/layout/container" +import { Section } from "@/components/layout/section" +import { SectionHeader } from "@/components/layout/section-header" +import { useTranslations, getTranslation } from "@/lib/i18n-context" + +export function AboutSection() { + const [translations] = useTranslations() + + return ( +
+ +
+ + +
+ {/* Background */} +
+

+ {getTranslation(translations, "about.bio.background.title")} +

+
+ {getTranslation( + translations, + "about.bio.background.text", + "I've been programming seriously since 2022, when I placed 6th in my first ICPC tournament. A 2024 winter training camp was a turning point that transformed how I approach technical challenges.\n\nI'm learning distributed systems and design patterns through hands-on implementation: building load balancers, message brokers, and experimenting with database architectures.\n\nMy projects range from NASA Space Apps prediction algorithms to production systems with RAG architectures. I don't just use technologies. I dig into how they work and when they actually make sense architecturally.", + ) + .split("\n\n") + .map((paragraph, index) => ( +

{paragraph}

+ ))} +
+
+ + {/* Approach */} +
+

+ {getTranslation(translations, "about.bio.approach.title")} +

+
+ {getTranslation( + translations, + "about.bio.approach.text", + "I think about efficiency first. Competitive programming trained me to analyze complexity before coding. But I'm pragmatic about execution: MVPs ship fast, production systems are built to last.\n\nMy process: research what exists, understand the problem deeply, consult when needed, then iterate rapidly. I focus on writing code that's maintainable and scalable without over-engineering early stages.\n\nI work well independently and in teams, and I adapt to what the project needs. Local hackathon wins and real-world deployments both prove the same thing: I deliver solutions that work now and scale later.", + ) + .split("\n\n") + .map((paragraph, index) => ( +

{paragraph}

+ ))} +
+
+
+
+
+
+ ) +} diff --git a/components/sections/awards-section.tsx b/components/sections/awards-section.tsx index ca03b24..4b8fa2a 100644 --- a/components/sections/awards-section.tsx +++ b/components/sections/awards-section.tsx @@ -1,170 +1,170 @@ -"use client" - -import { Card, CardContent, CardHeader } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Container } from "@/components/layout/container" -import { Section } from "@/components/layout/section" -import { SectionHeader } from "@/components/layout/section-header" -import { PressModal } from "@/components/press-modal" -import { - Trophy, - Award, - ExternalLink, - Users, - ArrowRight, - Lightbulb, - Globe, - Eye, -} from "lucide-react" -import Link from "next/link" -import { getVisibleAwards, localize } from "@/data/awards" -import type { Award as AwardType } from "@/data/awards" -import { useTranslations } from "@/lib/i18n-context" -import { hasMatchingProject } from "@/lib/project-utils" - -const ICON_MAP = { - award: Award, - trophy: Trophy, - lightbulb: Lightbulb, - globe: Globe, -} as const - -function AwardCard({ - award, - locale, - onSeeMore, -}: { - award: AwardType - locale: string - onSeeMore: (projectId: string) => void -}) { - const IconComponent = ICON_MAP[award.icon] ?? Trophy - const showSeeMore = hasMatchingProject(award.projectId) - - return ( - -
- - -
-
- -
- - {award.year} - -
- -
-

- {localize(award.title, locale)} -

-
-
- - -
- {localize(award.description, locale) - .split("\n") - .map((part, i) => ( -

{part}

- ))} -
- -
-

- - {locale === "es" ? "Tecnologias Clave" : "Key Technologies"} -

-
- {award.tags.map((tech) => ( - - {tech} - - ))} -
-
- -
- {/* Press CTA -- only if press is enabled and has links */} - {award.press?.enabled && award.press.links.length > 0 && ( -
- -
- )} - - {award.proofUrl && ( -
- -
- )} - - {/* See Project CTA -- scrolls to project and opens its modal */} - {showSeeMore && ( -
- -
- )} -
-
- - ) -} - -export function AwardsSection() { - const [, locale] = useTranslations() - const visibleAwards = getVisibleAwards() - - const handleSeeMore = (projectId: string) => { - window.dispatchEvent(new CustomEvent("open-project", { detail: projectId })) - } - - return ( -
- - - -
- {visibleAwards.map((award) => ( - - ))} -
-
-
- ) -} +"use client" + +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Container } from "@/components/layout/container" +import { Section } from "@/components/layout/section" +import { SectionHeader } from "@/components/layout/section-header" +import { PressModal } from "@/components/press-modal" +import { + Trophy, + Award, + ExternalLink, + Users, + ArrowRight, + Lightbulb, + Globe, + Eye, +} from "lucide-react" +import Link from "next/link" +import { getVisibleAwards, localize } from "@/data/awards" +import type { Award as AwardType } from "@/data/awards" +import { useTranslations } from "@/lib/i18n-context" +import { hasMatchingProject } from "@/lib/project-utils" + +const ICON_MAP = { + award: Award, + trophy: Trophy, + lightbulb: Lightbulb, + globe: Globe, +} as const + +function AwardCard({ + award, + locale, + onSeeMore, +}: { + award: AwardType + locale: string + onSeeMore: (projectId: string) => void +}) { + const IconComponent = ICON_MAP[award.icon] ?? Trophy + const showSeeMore = hasMatchingProject(award.projectId) + + return ( + +
+ + +
+
+ +
+ + {award.year} + +
+ +
+

+ {localize(award.title, locale)} +

+
+
+ + +
+ {localize(award.description, locale) + .split("\n") + .map((part, i) => ( +

{part}

+ ))} +
+ +
+

+ + {locale === "es" ? "Tecnologias Clave" : "Key Technologies"} +

+
+ {award.tags.map((tech) => ( + + {tech} + + ))} +
+
+ +
+ {/* Press CTA -- only if press is enabled and has links */} + {award.press?.enabled && award.press.links.length > 0 && ( +
+ +
+ )} + + {award.proofUrl && ( +
+ +
+ )} + + {/* See Project CTA -- scrolls to project and opens its modal */} + {showSeeMore && ( +
+ +
+ )} +
+
+ + ) +} + +export function AwardsSection() { + const [, locale] = useTranslations() + const visibleAwards = getVisibleAwards() + + const handleSeeMore = (projectId: string) => { + window.dispatchEvent(new CustomEvent("open-project", { detail: projectId })) + } + + return ( +
+ + + +
+ {visibleAwards.map((award) => ( + + ))} +
+
+
+ ) +} diff --git a/components/sections/contact-section.tsx b/components/sections/contact-section.tsx index 663f7d4..77d3e3b 100644 --- a/components/sections/contact-section.tsx +++ b/components/sections/contact-section.tsx @@ -1,397 +1,397 @@ -"use client" - -import React from "react" - -import { useState } from "react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { Label } from "@/components/ui/label" -import { - Mail, - Linkedin, - Github, - Send, - MapPin, - Clock, - CheckCircle, - AlertCircle, - Download, -} from "lucide-react" -import Link from "next/link" -import { useTranslations, getTranslation, getResumeUrl } from "@/lib/i18n-context" - -interface FormData { - name: string - email: string - subject: string - message: string -} - -export function ContactSection() { - const [translations, locale] = useTranslations() - const resumeUrl = getResumeUrl(locale) - - const [formData, setFormData] = useState({ - name: "", - email: "", - subject: "", - message: "", - }) - const [isSubmitting, setIsSubmitting] = useState(false) - const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle") - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsSubmitting(true) - setSubmitStatus("idle") - - try { - const response = await fetch("/api/contact", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(formData), - }) - - await response.json() - - if (response.ok) { - setSubmitStatus("success") - setFormData({ - name: "", - email: "", - subject: "", - message: "", - }) - } else { - setSubmitStatus("error") - } - } catch (error) { - setSubmitStatus("error") - } finally { - setIsSubmitting(false) - } - } - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - setFormData((prev) => ({ - ...prev, - [name]: value, - })) - } - - const isFormValid = formData.name && formData.email && formData.subject && formData.message - - return ( -
-
-
- {/* Contact Information */} -
-
-

- {getTranslation(translations, "contact.info.title", "Let's Connect")} -

-

- {getTranslation( - translations, - "contact.info.description", - "I'm always interested in discussing new opportunities, whether it's a challenging systems engineering project, AI/ML implementation, or automation solution. Feel free to reach out through any of the channels below." - )} -

- - - - {getTranslation(translations, "hero.cta.resume", "Download Resume")} - -
- - {/* Contact Methods */} -
-
-
- -
-
-
- {getTranslation(translations, "contact.methods.email", "Email")} -
- - repettoalejandroing@gmail.com - -
-
- -
-
- -
-
-
LinkedIn
- - linkedin.com/in/alejandro-repetto - -
-
- -
-
- -
-
-
GitHub
- - github.com/Repetto-A - -
-
- -
-
- -
-
-
- {getTranslation(translations, "contact.methods.location", "Location")} -
- Argentina -
-
- -
-
- -
-
-
- {getTranslation(translations, "contact.methods.timezone", "Timezone")} -
- UTC-3 (Argentina) -
-
-
- - {/* Availability */} - - - - {getTranslation( - translations, - "contact.availability.title", - "Current Availability" - )} - - - -
-
- - {getTranslation( - translations, - "contact.availability.consulting", - "Consulting" - )} - - - {getTranslation(translations, "contact.availability.available", "Available")} - -
-
- - {getTranslation( - translations, - "contact.availability.freelance", - "Freelance Projects" - )} - - - {getTranslation(translations, "contact.availability.available", "Available")} - -
-
- - {getTranslation( - translations, - "contact.availability.fulltime", - "Full-time Opportunities" - )} - - - {getTranslation( - translations, - "contact.availability.discuss", - "Open to Discuss" - )} - -
-
-
-
-
- - {/* Contact Form */} - - - - {getTranslation(translations, "contact.form.title", "Send a Message")} - - - -
-
-
- - -
- -
- - -
-
- -
- - -
- -
- -