feat(ui): add staggered entry animations using framer-motion and fix typography nesting

This commit is contained in:
Poyraz Avsever
2026-03-30 11:30:12 +03:00
parent a23665ee9e
commit 87f6585a78
5 changed files with 170 additions and 95 deletions
+32 -27
View File
@@ -1,9 +1,10 @@
"use client";
"use client";
import Image from "next/image";
import Link from "next/link";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "poyraz-ui/molecules";
import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper";
import { BOOKMARKS } from "@/data/bookmarks";
import { certificates } from "@/data/certificates";
import { EDUCATION } from "@/data/education";
@@ -25,21 +26,23 @@ export function AboutContent() {
</Card>
<div>
<div className="grid gap-2">
<StaggerContainer className="grid gap-2">
{EDUCATION.map((item) => (
<Card key={item.id} className="rounded-sm border-border p-3">
<Typography variant="large" className="text-base leading-tight">
{item.title}
</Typography>
<Typography variant="small" className="mt-0.5 text-muted-foreground">
{item.institution}
</Typography>
<Typography variant="small" className="mt-1 text-red-600">
{item.period}
</Typography>
</Card>
<StaggerItem key={item.id}>
<Card className="rounded-sm border-border p-3">
<Typography variant="large" className="text-base leading-tight">
{item.title}
</Typography>
<Typography variant="small" className="mt-0.5 text-muted-foreground">
{item.institution}
</Typography>
<Typography variant="small" className="mt-1 text-red-600">
{item.period}
</Typography>
</Card>
</StaggerItem>
))}
</div>
</StaggerContainer>
</div>
</div>
@@ -106,21 +109,23 @@ export function AboutContent() {
</Sheet>
<div className="h-full">
<div className="grid gap-2">
<StaggerContainer className="grid gap-2">
{EXPERIENCE.map((item) => (
<Card key={item.id} className="rounded-sm border-border p-3">
<Typography variant="large" className="text-base leading-tight">
{item.role}
</Typography>
<Typography variant="small" className="mt-0.5 text-muted-foreground">
{item.company}
</Typography>
<Typography variant="small" className="mt-1 text-red-600">
{item.period}
</Typography>
</Card>
<StaggerItem key={item.id}>
<Card className="rounded-sm border-border p-3">
<Typography variant="large" className="text-base leading-tight">
{item.role}
</Typography>
<Typography variant="small" className="mt-0.5 text-muted-foreground">
{item.company}
</Typography>
<Typography variant="small" className="mt-1 text-red-600">
{item.period}
</Typography>
</Card>
</StaggerItem>
))}
</div>
</StaggerContainer>
</div>
</div>
+18 -14
View File
@@ -1,3 +1,5 @@
"use client";
import Link from "next/link";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import {
@@ -10,6 +12,7 @@ import {
PaginationNext,
PaginationPrevious,
} from "poyraz-ui/molecules";
import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper";
import type { BlogPageData } from "@/data/blog";
type BlogContentProps = {
@@ -118,22 +121,23 @@ export function BlogContent({ data }: BlogContentProps) {
<div className="space-y-3">
{hasArticles ? (
<div className="grid gap-3 md:grid-cols-3">
<StaggerContainer className="grid gap-3 md:grid-cols-3">
{data.articles.map((post) => (
<ArticleCard
key={post.id}
image={post.image}
category={post.category}
title={post.title}
excerpt={post.excerpt}
date={post.date}
readTime={post.readTime}
href={post.href}
className="rounded-sm border-border [&_h3]:line-clamp-2 [&_h3]:min-h-[2.5rem]"
author={{ name: post.author, avatar: "/logo/logo.jpeg" }}
/>
<StaggerItem key={post.id}>
<ArticleCard
image={post.image}
category={post.category}
title={post.title}
excerpt={post.excerpt}
date={post.date}
readTime={post.readTime}
href={post.href}
className="rounded-sm border-border [&_h3]:line-clamp-2 [&_h3]:min-h-[2.5rem]"
author={{ name: post.author, avatar: "/logo/logo.jpeg" }}
/>
</StaggerItem>
))}
</div>
</StaggerContainer>
) : (
<Card className="rounded-sm border-border p-5">
<Typography variant="p" className="text-muted-foreground">
+43 -43
View File
@@ -4,6 +4,7 @@ import Image from "next/image";
import { useRef, useState } from "react";
import { Card, Typography } from "poyraz-ui/atoms";
import { NewsCard } from "poyraz-ui/molecules";
import { StaggerContainer, StaggerItem, FadeIn } from "@/components/motion-wrapper";
type HomeHeroProps = {
news: {
@@ -38,52 +39,51 @@ export function HomeHero({ news }: HomeHeroProps) {
return (
<section className="grid gap-3 md:h-65 md:grid-cols-2">
<div className="grid gap-2 md:grid-rows-3">
<StaggerContainer className="grid gap-2 md:grid-rows-3">
{news.map((item) => (
<NewsCard
key={item.id}
className="rounded-sm border-border md:h-full"
category={item.category}
title={item.title}
date={item.date}
image={item.image}
href={item.href}
/>
))}
</div>
<Card className="grid grid-cols-[112px_1fr] items-stretch gap-2 rounded-sm border-border sm:grid-cols-[168px_1fr] md:h-full">
<div className="flex flex-col justify-center gap-1 px-3 py-2">
<div className="flex items-center gap-1">
<Typography variant="h2" className="leading-tight">
Poyraz
</Typography>
<Typography variant="h2" secondaryFont className="leading-tight text-red-600">
{" "}
Avsever
</Typography>
</div>
<Typography variant="small" className="text-muted-foreground">
Teknolojiyi merak eden bir genç. Yazılım geliştirme, yapay zeka ve teknoloji dünyasındaki gelişmeleri takip ediyor.
</Typography>
</div>
<div className="relative h-full overflow-hidden">
<div
className="relative h-full w-full"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Image
src={frame === 1 ? "/images/hero1.png" : "/images/hero2.png"}
alt="Poyraz Avsever"
width={240}
height={240}
className="absolute right-0 bottom-0 h-auto w-20 object-contain md:w-48"
<StaggerItem key={item.id}>
<NewsCard
className="rounded-sm border-border md:h-full"
category={item.category}
title={item.title}
date={item.date}
image={item.image}
href={item.href}
/>
</StaggerItem>
))}
</StaggerContainer>
<FadeIn delay={0.15}>
<Card className="grid grid-cols-[112px_1fr] items-stretch gap-2 rounded-sm border-border sm:grid-cols-[168px_1fr] md:h-full">
<div className="flex flex-col justify-center gap-1 px-3 py-2">
<div className="flex items-center gap-1">
<Typography variant="h2" className="leading-tight">
Poyraz <span className="font-secondary text-red-600">Avsever</span>
</Typography>
</div>
<Typography variant="small" className="text-muted-foreground">
Teknolojiyi merak eden bir genç. Yazılım geliştirme, yapay zeka ve teknoloji dünyasındaki gelişmeleri takip ediyor.
</Typography>
</div>
</div>
</Card>
<div className="relative h-full overflow-hidden">
<div
className="relative h-full w-full"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Image
src={frame === 1 ? "/images/hero1.png" : "/images/hero2.png"}
alt="Poyraz Avsever"
width={240}
height={240}
className="absolute right-0 bottom-0 h-auto w-20 object-contain md:w-48"
/>
</div>
</div>
</Card>
</FadeIn>
</section>
);
}
+64
View File
@@ -0,0 +1,64 @@
"use client";
import { motion } from "framer-motion";
import type { ReactNode } from "react";
type FadeInProps = {
children: ReactNode;
className?: string;
delay?: number;
};
export function FadeIn({ children, className, delay = 0 }: FadeInProps) {
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay, ease: "easeOut" }}
className={className}
>
{children}
</motion.div>
);
}
type StaggerContainerProps = {
children: ReactNode;
className?: string;
stagger?: number;
};
export function StaggerContainer({ children, className, stagger = 0.06 }: StaggerContainerProps) {
return (
<motion.div
initial="hidden"
animate="visible"
variants={{
hidden: {},
visible: { transition: { staggerChildren: stagger } },
}}
className={className}
>
{children}
</motion.div>
);
}
type StaggerItemProps = {
children: ReactNode;
className?: string;
};
export function StaggerItem({ children, className }: StaggerItemProps) {
return (
<motion.div
variants={{
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: { duration: 0.35, ease: "easeOut" } },
}}
className={className}
>
{children}
</motion.div>
);
}
+13 -11
View File
@@ -3,6 +3,7 @@ import Link from "next/link";
import { Icon } from "@iconify/react";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import { ImageCard } from "poyraz-ui/molecules";
import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper";
import { FIGMA_TEMPLATES, MOBILE_APPS, WEB_APPS, type ProjectItem } from "@/data/projects";
import { getGithubRepos, getNpmPackages } from "@/lib/project-feeds";
@@ -43,19 +44,20 @@ function ProjectSection({ title, items }: { title: string; items: ProjectItem[]
<Typography variant="large" className="text-base">
{title}
</Typography>
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
<StaggerContainer className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{items.map((item) => (
<ImageCard
key={item.id}
image={item.image}
title={item.title}
description={item.description}
badge={item.badge}
href={item.href}
className="aspect-square rounded-sm border-border"
/>
<StaggerItem key={item.id}>
<ImageCard
image={item.image}
title={item.title}
description={item.description}
badge={item.badge}
href={item.href}
className="aspect-square rounded-sm border-border"
/>
</StaggerItem>
))}
</div>
</StaggerContainer>
</section>
);
}