1import { Box } from '@mui/system';
2import { AnimatePresence, motion } from 'framer-motion';
3import { useEffect, useState } from 'react';
4
5export interface LikeButtonProps {
6 count?: number;
7 isLiked?: boolean;
8 onClick?: () => void;
9}
10
11const transition = {
12 type: 'spring',
13 stiffness: 600,
14 damping: 100,
15 duration: 0.5,
16};
17
18const LikeButton = (props: LikeButtonProps) => {
19 const { onClick, count = 0, isLiked: isLikedProp = false } = props;
20 const [shouldAnimate, setShouldAnimate] = useState(false);
21 const [isLiked, setIsLiked] = useState(isLikedProp);
22
23 const handleClick = () => {
24 if (!isLiked) {
25 setShouldAnimate(prevState => !prevState);
26 }
27 setIsLiked(prevState => !prevState);
28 onClick?.();
29 };
30
31 useEffect(() => {
32 if (shouldAnimate) {
33 let tid = setTimeout(() => {
34 setShouldAnimate(prevState => !prevState);
35 }, 520);
36
37 return () => {
38 clearTimeout(tid);
39 };
40 }
41 }, [shouldAnimate]);
42
43 return (
44 <Box
45 component={motion.div}
46 layout
47 sx={{
48 height: 40,
49 background: 'linear-gradient(rgb(35 35 35) 0%, rgb(28 28 28) 100%)',
50 borderRadius: '12px',
51 display: 'flex',
52 alignItems: 'center',
53 gap: '16px',
54 cursor: 'pointer',
55 transition: 'background 0.2s ease-out',
56 boxShadow:
57 'rgba(255, 255, 255, 0.02) 1px 1px 0px 0px inset, rgba(255, 255, 255, 0.02) -1px -1px 0px 0px inset, rgba(255, 255, 255, 0.02) 1px -1px 0px 0px inset, rgba(255, 255, 255, 0.02) -1px 1px 0px 0px inset, rgba(255, 255, 255, 0.05) 0px 1px 0px 0px inset, rgba(0, 0, 0, 0.175) 0px 4px 8px 0px',
58 '&:hover': {
59 background: 'linear-gradient(rgb(35 35 35) 0%, rgb(35 35 35) 100%)',
60 },
61 userSelect: 'none',
62 }}
63 onClick={handleClick}
64 >
65 <Box
66 sx={{
67 pl: '12px',
68 display: 'flex',
69 alignItems: 'center',
70 gap: '16px',
71 }}
72 >
73 <Box
74 sx={{ position: 'relative', width: 20, height: 20, mt: '6px' }}
75 tabIndex={-1}
76 >
77 <Box
78 component={motion.div}
79 animate={shouldAnimate ? { y: -6, x: -2, rotate: -2 } : {}}
80 transition={{
81 type: 'spring',
82 stiffness: 800,
83 damping: 100,
84 duration: 0.5,
85 }}
86 sx={{
87 position: 'absolute',
88 zIndex: 2,
89 left: '2px',
90 bottom: '2px',
91 display: 'flex',
92 }}
93 >
94 <LikeIcon active={isLiked} />
95 </Box>
96 <Box
97 component={motion.div}
98 animate={shouldAnimate ? { y: -8, x: 1, rotate: 2 } : {}}
99 transition={{
100 type: 'spring',
101 stiffness: 600,
102 damping: 100,
103 duration: 0.5,
104 delay: 0.05,
105 }}
106 sx={{
107 position: 'absolute',
108 zIndex: 1,
109 bottom: '8px',
110 right: '-5px',
111 display: 'flex',
112 }}
113 >
114 <LikeIcon active={isLiked} />
115 </Box>
116 <AnimatePresence>{shouldAnimate && <Confetti />}</AnimatePresence>
117 </Box>
118 <Box sx={{ color: 'hsl(0 0% 99%)', fontWeight: 500 }}>
119 {isLiked ? 'Liked' : 'Like'}
120 </Box>
121 </Box>
122 <Box sx={{ py: '6px', height: '100%' }}>
123 <Box
124 sx={{
125 width: '1px',
126 height: '100%',
127 background: 'rgba(255, 255, 255, 0.05)',
128 }}
129 />
130 </Box>
131 <Box
132 sx={{
133 color: 'rgba(255, 255, 255, 0.45)',
134 fontWeight: 500,
135 py: '9px',
136 pr: '16px',
137 position: 'relative',
138 display: 'flex',
139 alignItems: 'center',
140 flexDirection: 'column',
141 justifyContent: 'center',
142 overflow: 'hidden',
143 }}
144 component={motion.div}
145 layout
146 >
147 {}
148 <Box sx={{ visibility: 'hidden', height: 0, opacity: 0 }}>20</Box>
149 <Box sx={{ visibility: 'hidden', height: 0, opacity: 0 }}>21</Box>
150 <Box
151 component={motion.span}
152 sx={{
153 position: isLiked ? 'absolute' : 'relative',
154 opacity: isLiked ? 0 : 1,
155 top: isLiked ? -22 : 0,
156 }}
157 animate={{
158 opacity: isLiked ? 0 : 1,
159 top: isLiked ? -22 : 0,
160 }}
161 transition={{
162 type: 'spring',
163 damping: 60,
164 stiffness: 500,
165 duration: 0.3,
166 ease: [0.45, 0, 0.55, 1],
167 }}
168 >
169 {count}
170 </Box>
171 <Box
172 component={motion.span}
173 sx={{
174 position: isLiked ? 'relative' : 'absolute',
175 opacity: isLiked ? 1 : 0,
176 bottom: isLiked ? 0 : -22,
177 }}
178 animate={{
179 color: 'hsl(0 0% 99%)',
180 opacity: isLiked ? 1 : 0,
181 bottom: isLiked ? 0 : -22,
182 }}
183 transition={{
184 type: 'spring',
185 damping: 60,
186 stiffness: 500,
187 duration: 0.3,
188 ease: [0.45, 0, 0.55, 1],
189 }}
190 >
191 {count + 1}
192 </Box>
193 </Box>
194 </Box>
195 );
196};
197
198const LikeIcon = ({ active }: { active?: boolean }) => (
199 <svg
200 xmlns="http://www.w3.org/2000/svg"
201 width="18"
202 height="18"
203 stroke={active ? 'hsl(0 0% 99%)' : '#8c8c8c'}
204 stroke-linecap="round"
205 stroke-linejoin="round"
206 stroke-width="2.5"
207 viewBox="0 0 25 25"
208 fill={active ? 'hsl(0 0% 99%)' : 'rgb(35, 35, 35)'}
209 style={{
210 filter:
211 'drop-shadow(2px 0 0 rgb(35 35 35)) drop-shadow(-2px 0 0 rgb(35 35 35)) drop-shadow(0 2px 0 rgb(35 35 35)) drop-shadow(0 -2px 0 rgb(35 35 35))',
212 }}
213 >
214 <path d="M7 10v12M15 5.88L14 10h5.83a2 2 0 011.92 2.56l-2.33 8A2 2 0 0117.5 22H4a2 2 0 01-2-2v-8a2 2 0 012-2h2.76a2 2 0 001.79-1.11L12 2a3.13 3.13 0 013 3.88z"></path>
215 </svg>
216);
217
218const Confetti = () => {
219 const variants = {
220 initial: {
221 opacity: 0,
222 scale: 0,
223 },
224 animate: (e: any) => {
225 return {
226 opacity: 1,
227 scale: 1,
228 x: e.animateX,
229 y: e.animateY,
230 rotate: e.animateRotate ?? 0,
231 };
232 },
233 exit: (e: any) => {
234 var t;
235 return {
236 opacity: 0.5,
237 scale: 0,
238 x: e.exitX,
239 y: e.exitY,
240 rotate: e.exitRotate ?? 0,
241 };
242 },
243 };
244 return (
245 <>
246 <Box
247 component={motion.div}
248 variants={variants}
249 initial="initial"
250 animate="animate"
251 exit="exit"
252 custom={{
253 animateX: 8,
254 animateY: -12,
255 exitX: 18,
256 exitY: -15,
257 }}
258 transition={{
259 ...transition,
260 stiffness: 600,
261 }}
262 sx={{
263 position: 'absolute',
264 background: 'hsl(0 0% 50%)',
265 width: 6,
266 height: 6,
267 borderRadius: '50%',
268 bottom: -14,
269 right: 1,
270 }}
271 />
272 <Box
273 component={motion.div}
274 variants={variants}
275 initial="initial"
276 animate="animate"
277 exit="exit"
278 custom={{
279 animateX: 7,
280 animateY: -14,
281 exitX: 27,
282 exitY: -10,
283 }}
284 transition={{
285 ...transition,
286 stiffness: 500,
287 }}
288 sx={{
289 position: 'absolute',
290 background: 'hsl(0 0% 50%)',
291 width: 8,
292 height: 8,
293 borderRadius: '50%',
294 bottom: -4,
295 right: -10,
296 }}
297 />
298 <Box
299 component={motion.div}
300 variants={variants}
301 initial="initial"
302 animate="animate"
303 exit="exit"
304 custom={{
305 animateX: 8,
306 animateY: -26,
307 exitX: 13,
308 exitY: -22,
309 }}
310 transition={{
311 ...transition,
312 stiffness: 800,
313 }}
314 sx={{
315 position: 'absolute',
316 background: 'hsl(0 0% 50%)',
317 width: 6,
318 height: 6,
319 borderRadius: '50%',
320 top: 0,
321 right: -1,
322 }}
323 />
324 <Box
325 component={motion.div}
326 variants={variants}
327 initial="initial"
328 animate="animate"
329 exit="exit"
330 custom={{
331 animateX: -26,
332 animateY: -14,
333 exitX: -30,
334 exitY: -17,
335 }}
336 transition={{
337 ...transition,
338 stiffness: 600,
339 }}
340 sx={{
341 position: 'absolute',
342 background: 'hsl(0 0% 50%)',
343 width: 6,
344 height: 6,
345 borderRadius: '50%',
346 left: 12,
347 top: 8,
348 }}
349 />
350 <Box
351 component={motion.div}
352 variants={variants}
353 initial="initial"
354 animate="animate"
355 exit="exit"
356 custom={{
357 animateX: -12,
358 animateY: -22,
359 exitX: -16,
360 exitY: -17,
361 animateRotate: -30,
362 exitRotate: -70,
363 }}
364 transition={{
365 ...transition,
366 stiffness: 600,
367 }}
368 sx={{
369 position: 'absolute',
370 left: 3,
371 top: -15,
372 display: 'flex',
373 }}
374 >
375 <StarIcon />
376 </Box>
377 <Box
378 component={motion.div}
379 variants={variants}
380 initial="initial"
381 animate="animate"
382 exit="exit"
383 custom={{
384 animateX: 21,
385 animateY: -7,
386 exitX: 26,
387 exitY: -5,
388 animateRotate: 30,
389 exitRotate: 70,
390 }}
391 transition={{
392 ...transition,
393 stiffness: 600,
394 }}
395 sx={{
396 position: 'absolute',
397 left: 10,
398 top: -10,
399 display: 'flex',
400 }}
401 >
402 <StarIcon />
403 </Box>
404 </>
405 );
406};
407
408const StarIcon = () => (
409 <svg
410 xmlns="http://www.w3.org/2000/svg"
411 width="14"
412 height="14"
413 viewBox="0 0 24 24"
414 fill="hsl(0 0% 50%)"
415 stroke-width="2"
416 stroke-linecap="round"
417 stroke-linejoin="round"
418 >
419 <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
420 </svg>
421);
422
423export default LikeButton;