Skip to content

Commit d1086eb

Browse files
committed
feat: refactor MusicSheet component into CareerCards with enhanced card animations and data structure
1 parent caf9317 commit d1086eb

File tree

5 files changed

+164
-104
lines changed

5 files changed

+164
-104
lines changed

public/things/dev.png

1.64 MB
Loading

public/things/nurse.png

1.49 MB
Loading

public/things/photographer.png

1.61 MB
Loading

public/things/waiter.png

1.58 MB
Loading
Lines changed: 164 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,191 @@
11
import { cn } from "@/lib/utils";
22
import { motion } from "motion/react";
3-
import {
4-
Dispatch,
5-
SetStateAction,
6-
useState,
7-
useCallback,
8-
useMemo,
9-
} from "react";
3+
import { useState } from "react";
104

11-
const MusicSheet = () => {
12-
const [active, setActive] = useState(false);
13-
const [hovered, setHovered] = useState(-1);
5+
interface CardData {
6+
id: number;
7+
title: string;
8+
subtitle: string;
9+
description: string;
10+
image: string;
11+
salary: {
12+
amount: string;
13+
className: string;
14+
};
15+
}
1416

15-
const items = useMemo(
16-
() =>
17-
[
18-
{ content: "Nothing 1", angle: 10 },
19-
{ content: "Nothing 2", angle: 15 },
20-
{ content: "Nothing 3", angle: -10 },
21-
{ content: "Nothing 4", angle: -5 },
22-
].map((item, idx) => ({
23-
...item,
24-
id: idx + 1,
25-
})),
26-
[]
27-
);
17+
const INITIAL_CARDS = [
18+
{
19+
id: 1,
20+
title: "Developer",
21+
subtitle: "Software Engineer",
22+
description:
23+
"I'm a software engineer with a passion for building web and mobile applications.",
24+
image: "/things/dev.png",
25+
salary: {
26+
amount: "100,000",
27+
className: "text-green-500 bg-green-500/10 border border-green-500",
28+
},
29+
},
30+
{
31+
id: 2,
32+
title: "Nurse",
33+
subtitle: "Medical Assistant",
34+
description: "I'm a nurse with a passion for helping people.",
35+
image: "/things/nurse.png",
36+
salary: {
37+
amount: "50,000",
38+
className: "text-blue-500 bg-blue-500/10 border border-blue-500",
39+
},
40+
},
41+
{
42+
id: 3,
43+
title: "Photographer",
44+
subtitle: "Camera Man",
45+
description: "I'm a photographer with a passion for capturing moments.",
46+
image: "/things/photographer.png",
47+
salary: {
48+
amount: "30,000",
49+
className: "text-yellow-500 bg-yellow-500/10 border border-yellow-500",
50+
},
51+
},
52+
{
53+
id: 4,
54+
title: "Waiter",
55+
subtitle: "Restaurant Staff",
56+
description: "I'm a waiter with a passion for serving people.",
57+
image: "/things/waiter.png",
58+
salary: {
59+
amount: "20,000",
60+
className: "text-blue-500 bg-blue-500/10 border border-blue-500",
61+
},
62+
},
63+
] as const;
64+
65+
const ANIMATION_CONFIG = {
66+
stacked: {
67+
rotate: (index: number) => -5 * (index + 1),
68+
y: 0,
69+
},
70+
revealed: {
71+
rotate: (index: number) => {
72+
const angles = [-10, -5, 5, 10];
73+
return angles[index] || 0;
74+
},
75+
y: (index: number, total: number) => {
76+
if (index === 0) return 20;
77+
if (index === total - 1) return 20;
78+
return 0;
79+
},
80+
},
81+
};
2882

29-
const handleMouseEnter = useCallback(() => setActive(true), []);
30-
const handleMouseLeave = useCallback(() => setActive(false), []);
83+
export default function CareerCards() {
84+
const [isRevealed, setIsRevealed] = useState(false);
85+
const [hoveredId, setHoveredId] = useState<number | null>(null);
3186

3287
return (
3388
<div
3489
className="relative size-full flex items-center justify-center rounded-xl bg-background"
35-
onMouseEnter={handleMouseEnter}
36-
onMouseLeave={handleMouseLeave}
90+
onMouseEnter={() => setIsRevealed(true)}
91+
onMouseLeave={() => {
92+
setIsRevealed(false);
93+
setHoveredId(null);
94+
}}
3795
>
38-
{active ? (
39-
<ReleasedPlayer
40-
items={items}
41-
hovered={hovered}
42-
setHovered={setHovered}
43-
/>
44-
) : (
45-
<Stacked items={items} hovered={hovered} setHovered={setHovered} />
46-
)}
96+
<div
97+
className={cn("relative", isRevealed ? "w-full h-full" : "w-56 h-72")}
98+
>
99+
{INITIAL_CARDS.map((card, index) => (
100+
<Card
101+
key={card.id}
102+
data={card}
103+
index={index}
104+
total={INITIAL_CARDS.length}
105+
isRevealed={isRevealed}
106+
isHovered={hoveredId === card.id}
107+
onHover={(id) => setHoveredId(id)}
108+
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
109+
/>
110+
))}
111+
</div>
47112
</div>
48113
);
49-
};
114+
}
50115

51-
type DisplayProps = {
52-
hovered: number;
53-
setHovered: Dispatch<SetStateAction<number>>;
54-
items: { id: number; content: string; angle: number }[];
55-
};
56-
57-
const Stacked = ({ items, hovered, setHovered }: DisplayProps) => (
58-
<div className="size-40 flex items-center justify-center">
59-
{items.map((item) => (
60-
<Item
61-
key={item.id}
62-
item={item}
63-
hovered={hovered}
64-
setHovered={setHovered}
65-
className="absolute"
66-
/>
67-
))}
68-
</div>
69-
);
70-
71-
const ReleasedPlayer = ({ items, hovered, setHovered }: DisplayProps) => (
72-
<div className="size-full flex items-center justify-center relative">
73-
{items.map((item) => (
74-
<Item
75-
key={item.id}
76-
item={item}
77-
hovered={hovered}
78-
setHovered={setHovered}
79-
/>
80-
))}
81-
</div>
82-
);
83-
84-
type ItemProps = {
85-
item: { id: number; content: string; angle: number };
116+
interface CardProps {
117+
data: CardData;
118+
index: number;
119+
total: number;
120+
isRevealed: boolean;
121+
isHovered: boolean;
122+
onHover: (id: number | null) => void;
86123
className?: string;
87-
hovered: number;
88-
setHovered: Dispatch<SetStateAction<number>>;
89-
};
124+
}
90125

91-
const Item = ({ item, hovered, setHovered, className }: ItemProps) => {
92-
const handleHover = useCallback(
93-
(id: number) => {
94-
setHovered((prev) => (prev !== id ? id : prev));
95-
},
96-
[setHovered]
97-
);
126+
function Card({
127+
data,
128+
index,
129+
total,
130+
isRevealed,
131+
isHovered,
132+
onHover,
133+
className,
134+
}: CardProps) {
135+
const config = isRevealed
136+
? ANIMATION_CONFIG.revealed
137+
: ANIMATION_CONFIG.stacked;
138+
const rotateValue = config.rotate(index);
139+
const yOffset =
140+
typeof config.y === "function" ? config.y(index, total) : config.y;
98141

99142
return (
100143
<motion.div
144+
layoutId={`career-card-${data.id}`}
101145
className={cn(
102-
"size-56 border rounded-xl flex items-center justify-center text-4xl font-semibold bg-muted",
103-
"relative",
146+
"w-80 h-96 bg-muted rounded-2xl shadow-lg overflow-hidden cursor-pointer",
104147
className
105148
)}
106-
onMouseEnter={() => handleHover(item.id)}
107-
onMouseLeave={() => handleHover(-1)}
108-
layoutId={`sheet-player-${item.id}`}
109149
animate={{
110-
rotate: hovered === item.id ? 0 : item.angle,
111-
zIndex: hovered === item.id ? 100 : 0,
112-
boxShadow:
113-
hovered === item.id ? "0 0 10px 0 rgba(0, 0, 0, 0.5)" : "none",
150+
rotate: rotateValue,
151+
y: yOffset,
152+
x: isRevealed ? index * -30 : 0,
153+
scale: isHovered ? 1.02 : 1,
154+
zIndex: isHovered ? 50 : total - index,
155+
}}
156+
onMouseEnter={() => onHover(data.id)}
157+
onMouseLeave={() => onHover(null)}
158+
transition={{
159+
type: "spring",
160+
stiffness: 200,
161+
damping: 20,
114162
}}
115-
transition={{ type: "spring", stiffness: 200, damping: 20 }}
116163
>
117-
{item.id}
164+
<div className="size-full bg-background p-4 flex flex-col gap-4">
165+
<div className="relative aspect-square rounded-lg overflow-hidden bg-muted">
166+
<img
167+
src={data.image}
168+
alt={data.title}
169+
className="size-full object-cover"
170+
/>
171+
<div
172+
className={cn(
173+
"absolute top-2 right-2 px-2 py-1 rounded-lg text-sm font-medium",
174+
data.salary.className
175+
)}
176+
>
177+
${data.salary.amount}
178+
</div>
179+
</div>
118180

119-
{hovered === item.id && (
120-
<motion.div
121-
layoutId={`player-id-${item.id}`}
122-
className="bg-blue-500 w-56 h-48 absolute -top-[100%] flex items-center justify-center text-2xl rounded-xl"
123-
>
124-
{item.content}
125-
</motion.div>
126-
)}
181+
<div className="flex-1 flex flex-col">
182+
<h3 className="text-2xl font-bold">{data.title}</h3>
183+
<p className="text-muted-foreground">{data.subtitle}</p>
184+
<p className="text-sm text-muted-foreground/80 mt-2 line-clamp-3">
185+
{data.description}
186+
</p>
187+
</div>
188+
</div>
127189
</motion.div>
128190
);
129-
};
130-
131-
export default MusicSheet;
191+
}

0 commit comments

Comments
 (0)