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() {
))}
-
+
+
{/* Image Column */}
-
- {/* Gradient Mask to blend image into black background */}
-
-
-
-
+
+
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 (
+
+
+
+ )
+}
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 */}
+
+