Skip to content

Commit 2d9724f

Browse files
committed
Added animation | Enhanced Project Cards
1 parent 31154b0 commit 2d9724f

File tree

5 files changed

+124
-23
lines changed

5 files changed

+124
-23
lines changed

components/Hero.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function Hero() {
2121
immediate
2222
className="text-4xl sm:text-6xl md:text-7xl lg:text-8xl text-white font-semibold tracking-tight"
2323
>
24-
DAMJAN ZIMBAKOV
24+
Damjan Zimbakov
2525
</Reveal>
2626
<Reveal
2727
immediate

components/ProjectCard.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import Link from "next/link";
22
import Image from "next/image";
3+
import { Calendar, User } from "lucide-react";
34
import { Reveal } from "./animation/Reveal";
5+
import TechBadge from "./TechBadge";
46

57
export type Project = {
68
slug: string;
79
title: string;
810
blurb: string;
911
image: string;
1012
tech: string[];
13+
year: number;
14+
role: string;
15+
categories: ("Web Apps" | "Mobile Apps" | "AI/ML")[];
1116
};
1217

1318
export default function ProjectCard({ project }: { project: Project }) {
@@ -29,18 +34,34 @@ export default function ProjectCard({ project }: { project: Project }) {
2934
<Reveal as="h3" className="text-lg font-semibold">
3035
{project.title}
3136
</Reveal>
32-
<Reveal as="p" className="text-sm text-text-secondary mt-1">
37+
{/* Meta row: year and role */}
38+
<Reveal
39+
as="div"
40+
className="mt-1 flex items-center gap-4 text-xs text-text-secondary"
41+
>
42+
<span className="inline-flex items-center gap-1.5">
43+
<Calendar className="h-3.5 w-3.5 opacity-80" />
44+
<span>{project.year}</span>
45+
</span>
46+
<span className="inline-flex items-center gap-1.5">
47+
<User className="h-3.5 w-3.5 opacity-80" />
48+
<span className="truncate max-w-[12rem]" title={project.role}>
49+
{project.role}
50+
</span>
51+
</span>
52+
</Reveal>
53+
<Reveal as="p" className="text-sm text-text-secondary mt-2">
3354
{project.blurb}
3455
</Reveal>
3556
<div className="mt-3 flex flex-wrap gap-2">
36-
{project.tech.map((t) => (
37-
<span
38-
key={t}
39-
className="text-[10px] uppercase tracking-wide font-mono bg-white/5 text-text-secondary rounded-full px-2 py-0.5"
40-
>
41-
{t}
42-
</span>
57+
{project.tech.slice(0, 5).map((t) => (
58+
<TechBadge key={t} name={t} size="sm" />
4359
))}
60+
{project.tech.length > 5 && (
61+
<span className="inline-flex items-center rounded-full px-2.5 py-1 text-xs shadow-sm bg-white/10 text-text-secondary">
62+
+{project.tech.length - 5} more
63+
</span>
64+
)}
4465
</div>
4566
</div>
4667
</Link>

components/ProjectsGrid.tsx

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
"use client";
12
import ProjectCard, { Project } from "./ProjectCard";
23
import { Reveal, RevealContainer } from "./animation/Reveal";
4+
import { AnimatePresence, LayoutGroup } from "framer-motion";
5+
import { useMemo, useState } from "react";
36

47
const projects: Project[] = [
58
{
@@ -8,6 +11,9 @@ const projects: Project[] = [
811
blurb: "Cross-platform Flutter scheduling app with web and marketing site.",
912
image: "/images/nextime/nextime.jpg",
1013
tech: ["Flutter", "Dart", "NextJS", "React", "TailwindCSS", "Vite", "Git"],
14+
year: 2024,
15+
role: "Lead Mobile Developer",
16+
categories: ["Mobile Apps", "Web Apps"],
1117
},
1218
{
1319
slug: "zebekov",
@@ -25,6 +31,9 @@ const projects: Project[] = [
2531
"Git",
2632
"Nginx",
2733
],
34+
year: 2023,
35+
role: "Full-Stack Developer",
36+
categories: ["Web Apps"],
2837
},
2938
{
3039
slug: "court-vision",
@@ -41,17 +50,34 @@ const projects: Project[] = [
4150
"Git",
4251
"Nginx",
4352
],
53+
year: 2025,
54+
role: "Full-Stack & AI/ML Engineer",
55+
categories: ["AI/ML", "Web Apps"],
4456
},
4557
{
4658
slug: "course-creator",
4759
title: "Course Creator - Coddy, Inc.",
4860
blurb: "10 creator-led courses on HTML, CSS, C++, Python and more.",
4961
image: "/images/course-creator/course-creator.jpg",
5062
tech: ["HTML", "CSS", "C++", "Python", "Git"],
63+
year: 2021,
64+
role: "Course Creator",
65+
categories: ["Web Apps"],
5166
},
5267
];
5368

5469
export default function ProjectsGrid() {
70+
const TABS = ["All Projects", "Web Apps", "Mobile Apps", "AI/ML"] as const;
71+
type Tab = (typeof TABS)[number];
72+
const [activeTab, setActiveTab] = useState<Tab>("All Projects");
73+
74+
const filtered = useMemo(() => {
75+
if (activeTab === "All Projects") return projects;
76+
return projects.filter((p) =>
77+
p.categories.includes(activeTab as Exclude<Tab, "All Projects">)
78+
);
79+
}, [activeTab]);
80+
5581
return (
5682
<section
5783
id="projects"
@@ -72,17 +98,56 @@ export default function ProjectsGrid() {
7298
development, web applications, AI, and educational content creation.
7399
</Reveal>
74100
</RevealContainer>
75-
<RevealContainer
76-
as="div"
77-
className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"
78-
stagger={0.1}
79-
>
80-
{projects.map((p) => (
81-
<Reveal key={p.slug} as="div">
82-
<ProjectCard project={p} />
83-
</Reveal>
84-
))}
101+
{/* Tabs */}
102+
<RevealContainer as="div" className="mb-6 sm:mb-8" stagger={0.03}>
103+
<div className="flex justify-center">
104+
<div className="inline-flex rounded-full bg-white/5 p-1">
105+
{TABS.map((tab) => {
106+
const isActive = tab === activeTab;
107+
return (
108+
<button
109+
key={tab}
110+
onClick={() => setActiveTab(tab)}
111+
className={`px-3.5 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm rounded-full transition-colors ${
112+
isActive
113+
? "bg-accent text-white"
114+
: "text-text-secondary hover:text-white"
115+
}`}
116+
>
117+
{tab}
118+
</button>
119+
);
120+
})}
121+
</div>
122+
</div>
85123
</RevealContainer>
124+
{/* Responsive grid: 3 cols when possible, center when 1-2 items. Layout + presence for smooth tab transitions */}
125+
<LayoutGroup>
126+
<RevealContainer
127+
as="div"
128+
className={[
129+
"grid gap-6 mx-auto w-full",
130+
filtered.length >= 3
131+
? "sm:grid-cols-2 lg:grid-cols-3 lg:max-w-[1120px]"
132+
: filtered.length === 2
133+
? "sm:grid-cols-2 lg:grid-cols-2 lg:max-w-[740px]"
134+
: "sm:grid-cols-1 lg:grid-cols-1 lg:max-w-[360px]",
135+
].join(" ")}
136+
stagger={0.08}
137+
layout
138+
transition={{
139+
layout: { type: "spring", duration: 0.45, bounce: 0.08 },
140+
}}
141+
>
142+
<AnimatePresence mode="popLayout">
143+
{filtered.map((p) => (
144+
<Reveal key={p.slug} as="div" layout>
145+
<ProjectCard project={p} />
146+
</Reveal>
147+
))}
148+
</AnimatePresence>
149+
</RevealContainer>
150+
</LayoutGroup>
86151
</section>
87152
);
88153
}

components/TechBadge.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ const TECHS: Record<string, TechMeta> = {
3333
simpleIcon: "tailwindcss",
3434
},
3535

36+
// Build Tools
37+
vite: {
38+
label: "Vite",
39+
color: "#646CFF",
40+
text: "light",
41+
simpleIcon: "vite",
42+
},
43+
3644
// Mobile
3745
flutter: {
3846
label: "Flutter",

components/animation/Reveal.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import { motion, Variants, useReducedMotion } from "framer-motion";
55
type CommonProps = {
66
children: ReactNode;
77
className?: string;
8-
/** Delay (seconds) applied to this element or to children via delayChildren */
98
delay?: number;
10-
/** Animation duration in seconds */
119
duration?: number;
12-
/** Once: animate only the first time it enters the viewport */
1310
once?: boolean;
14-
/** If true, animates on mount immediately (good for above-the-fold). If false, animates when in view. */
1511
immediate?: boolean;
1612
};
1713

@@ -35,6 +31,7 @@ function getVariants(
3531
opacity: 1,
3632
transition: { duration: Math.min(0.2, duration), delay: 0 },
3733
},
34+
exit: { opacity: 0, transition: { duration: 0.15 } },
3835
};
3936
}
4037
return {
@@ -45,6 +42,12 @@ function getVariants(
4542
x: 0,
4643
transition: { duration, ease: "easeOut", delay },
4744
},
45+
exit: {
46+
opacity: 0,
47+
y: offsetY,
48+
x: offsetX,
49+
transition: { duration: Math.min(0.35, duration), ease: "easeInOut" },
50+
},
4851
};
4952
}
5053

@@ -106,6 +109,8 @@ export function Reveal({
106109
animate={immediate ? "show" : undefined}
107110
whileInView={immediate ? undefined : "show"}
108111
viewport={immediate ? undefined : { once, amount: 0.2 }}
112+
layout={rest.layout}
113+
transition={rest.transition}
109114
{...rest}
110115
>
111116
{children}
@@ -161,6 +166,8 @@ export function RevealContainer({
161166
animate={immediate ? "show" : undefined}
162167
whileInView={immediate ? undefined : "show"}
163168
viewport={immediate ? undefined : { once, amount: 0.15 }}
169+
layout={rest.layout}
170+
transition={rest.transition}
164171
{...rest}
165172
>
166173
{children}

0 commit comments

Comments
 (0)