Skip to content

Commit d926a62

Browse files
committed
add funny animations
1 parent 4da779f commit d926a62

File tree

15 files changed

+1103
-2
lines changed

15 files changed

+1103
-2
lines changed

services/ahhachul.com/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"react-lazy-load-image-component": "^1.6.0",
6464
"react-loading-skeleton": "^3.4.0",
6565
"react-router-dom": "^6.21.3",
66+
"react-use-measure": "^2.1.1",
6667
"react-zoom-pan-pinch": "3.6.0",
6768
"suspend-react": "^0.1.3",
6869
"swiper": "^11.1.15",

services/ahhachul.com/src/pages/lost-found/ui/Page/Detail.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ArticleDetailErrorFallback } from 'features/articles/ui/ArticleDetailEr
66
import { QueryErrorBoundary } from 'entities/app-errors/ui/QueryErrorBoundary';
77
import { Loading } from 'entities/app-loaders/ui/Loading';
88
import LostFoundCommentTextField from '../_common/LostFoundCommentTextField';
9+
import { renderRightEllipsis } from 'widgets/layout-header';
910

1011
const LostFoundArticleDetail = React.lazy(
1112
() => import('../_common/LostFoundArticleDetail/LostFoundArticleDetail'),
@@ -23,12 +24,13 @@ const LostFoundDetail: ActivityComponentType<WithArticleId> = ({
2324
};
2425

2526
return (
26-
<Layout>
27+
<Layout appBar={{ renderRight: renderRightEllipsis }}>
2728
<QueryErrorBoundary errorFallback={ArticleDetailErrorFallback}>
2829
<Suspense fallback={<Loading opacity={1} />}>
2930
<LostFoundArticleDetail articleId={articleId} />
3031
</Suspense>
3132
</QueryErrorBoundary>
33+
3234
<LostFoundCommentTextField
3335
articleId={articleId}
3436
handleHitBottom={handleHitBottom}

services/ahhachul.com/src/shared/lib/hooks/useIsDeferred.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useTimeout } from './useTimeout';
44
export const useIsDeferred = (ms?: number) => {
55
const [isDeferred, setIsDeferred] = useState(false);
66

7-
useTimeout(() => setIsDeferred(true), ms ?? 500);
7+
useTimeout(() => setIsDeferred(true), ms ?? 1000);
88

99
return { isDeferred };
1010
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { AnimatePresence, motion } from 'framer-motion';
3+
4+
export function AnimatedState({
5+
children,
6+
className,
7+
state,
8+
}: {
9+
children: React.ReactNode;
10+
className?: string;
11+
state: string;
12+
}) {
13+
const variants = {
14+
initial: { opacity: 0, y: -25 },
15+
visible: { opacity: 1, y: 0 },
16+
exit: { opacity: 0, y: 25 },
17+
};
18+
19+
return (
20+
<AnimatePresence mode="popLayout" initial={false}>
21+
<motion.div
22+
key={state}
23+
initial="initial"
24+
animate="visible"
25+
exit="exit"
26+
variants={variants}
27+
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
28+
className={className}
29+
>
30+
{children}
31+
</motion.div>
32+
</AnimatePresence>
33+
);
34+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { useState } from 'react';
2+
import { AnimatePresence, motion } from 'framer-motion';
3+
import { CheckIcon, CopyIcon } from '@radix-ui/react-icons';
4+
import styled from '@emotion/styled';
5+
6+
export function CopyButton() {
7+
const variants = {
8+
hidden: { opacity: 0, scale: 0.5 },
9+
visible: { opacity: 1, scale: 1 },
10+
};
11+
12+
const [copied, setCopied] = useState(false);
13+
14+
const copy = () => {
15+
setCopied(true);
16+
setTimeout(() => setCopied(false), 1000);
17+
};
18+
19+
return (
20+
<StyledButton type="button" aria-label="Copy code snippet" onClick={copy}>
21+
<AnimatePresence mode="wait" initial={false}>
22+
{copied ? (
23+
<IconWrapper
24+
key="checkmark"
25+
variants={variants}
26+
initial="hidden"
27+
animate="visible"
28+
exit="hidden"
29+
>
30+
<CheckIcon />
31+
</IconWrapper>
32+
) : (
33+
<IconWrapper
34+
key="copy"
35+
variants={variants}
36+
initial="hidden"
37+
animate="visible"
38+
exit="hidden"
39+
>
40+
<CopyIcon />
41+
</IconWrapper>
42+
)}
43+
</AnimatePresence>
44+
</StyledButton>
45+
);
46+
}
47+
48+
const StyledButton = styled.button`
49+
display: flex;
50+
width: 48px;
51+
height: 48px;
52+
align-items: center;
53+
justify-content: center;
54+
border-radius: 6px;
55+
background-color: #f9fafb;
56+
color: #1f2937;
57+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
58+
transition:
59+
background-color 0.2s,
60+
color 0.2s;
61+
62+
&:hover {
63+
background-color: #f3f4f6;
64+
color: #111827;
65+
}
66+
`;
67+
68+
const IconWrapper = styled(motion.span)`
69+
svg {
70+
width: 24px;
71+
height: 24px;
72+
}
73+
`;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useState } from 'react';
2+
import { SymbolIcon } from '@radix-ui/react-icons';
3+
import { AnimatedState } from '../AnimatedState/AnimatedState';
4+
import styled from '@emotion/styled';
5+
import { keyframes } from '@emotion/react';
6+
7+
export const SmoothButton = () => {
8+
const buttonCopy = {
9+
idle: 'Send me a login link',
10+
loading: <SpinningIcon />,
11+
success: 'Login link sent!',
12+
};
13+
14+
const [buttonState, setButtonState] =
15+
useState<keyof typeof buttonCopy>('idle');
16+
17+
return (
18+
<StyledButton
19+
type="button"
20+
disabled={buttonState !== 'idle'}
21+
onClick={() => {
22+
if (buttonState === 'success') return;
23+
24+
setButtonState('loading');
25+
26+
setTimeout(() => {
27+
setButtonState('success');
28+
}, 1750);
29+
30+
setTimeout(() => {
31+
setButtonState('idle');
32+
}, 3500);
33+
}}
34+
>
35+
<AnimatedStateWrapper state={buttonState}>
36+
{buttonCopy[buttonState]}
37+
</AnimatedStateWrapper>
38+
</StyledButton>
39+
);
40+
};
41+
42+
const spin = keyframes`
43+
from {
44+
transform: rotate(0deg);
45+
}
46+
to {
47+
transform: rotate(360deg);
48+
}
49+
`;
50+
51+
const StyledButton = styled.button`
52+
position: relative;
53+
height: 32px;
54+
width: 144px;
55+
overflow: hidden;
56+
border-radius: 8px;
57+
background: linear-gradient(to bottom, #0ea5e9, #3b82f6);
58+
font-size: 14px;
59+
font-weight: 500;
60+
box-shadow:
61+
0 1px 3px 0 rgba(0, 0, 0, 0.1),
62+
0 1px 2px 0 rgba(0, 0, 0, 0.06);
63+
64+
&:disabled {
65+
opacity: 0.9;
66+
cursor: not-allowed;
67+
}
68+
`;
69+
70+
const AnimatedStateWrapper = styled(AnimatedState)`
71+
display: flex;
72+
width: 100%;
73+
align-items: center;
74+
justify-content: center;
75+
color: white;
76+
`;
77+
78+
const SpinningIcon = styled(SymbolIcon)`
79+
width: 16px;
80+
height: 16px;
81+
animation: ${spin} 1s linear infinite;
82+
`;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React, { useState } from 'react';
2+
import { AnimatePresence, motion } from 'framer-motion';
3+
import styled from '@emotion/styled';
4+
5+
export function ToggleBox() {
6+
const [isVisible, setIsVisible] = useState(true);
7+
8+
return (
9+
<>
10+
<Container>
11+
<AnimatePresence>
12+
{isVisible ? (
13+
<AnimatedBox
14+
initial={{ opacity: 0 }}
15+
animate={{ opacity: 1 }}
16+
exit={{ opacity: 0 }}
17+
/>
18+
) : null}
19+
</AnimatePresence>
20+
</Container>
21+
<ToggleButton type="button" onClick={() => setIsVisible((prev) => !prev)}>
22+
{isVisible ? 'Hide' : 'Show'}
23+
</ToggleButton>
24+
</>
25+
);
26+
}
27+
28+
const Container = styled.div`
29+
height: 48px;
30+
`;
31+
32+
const AnimatedBox = styled(motion.div)`
33+
width: 48px;
34+
height: 48px;
35+
border-radius: 12px;
36+
background-color: #eab308; // Tailwind's yellow-500
37+
`;
38+
39+
const ToggleButton = styled.button`
40+
border-radius: 8px;
41+
background-color: white;
42+
padding: 8px 16px;
43+
font-size: 14px;
44+
`;

0 commit comments

Comments
 (0)