Skip to content

Commit 360973b

Browse files
committed
design: 온보딩 페이지 퍼블리싱
우편번호 페이지 퍼블리싱 완료 우편번호 애니메이션 구현 완료 애니메이션 페이지 퍼블리싱 완료 애니메이션 페이지 구현 중 관리자 편지는 컴포넌트 제작 후 라우팅 연결할 예정
1 parent 8c3c23c commit 360973b

File tree

11 files changed

+284
-2
lines changed

11 files changed

+284
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7-
"dev": "vite",
7+
"dev": "vite --host",
88
"build": "tsc -b && vite build",
99
"lint": "eslint .",
1010
"preview": "vite preview"
19.5 KB
Loading
203 KB
Loading
127 KB
Loading
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 Spinner from './components/Spinner';
3+
4+
const SetZipCode = ({
5+
setIsZipCodeSet,
6+
}: {
7+
setIsZipCodeSet: React.Dispatch<React.SetStateAction<Boolean>>;
8+
}) => {
9+
const DUMMY_ZIPCODE = '122A2';
10+
11+
return (
12+
<>
13+
<header className="flex flex-col items-center">
14+
<h1 className="message-header body-b mb-2">우편번호를 설정하고 있습니다.</h1>
15+
<p className="caption-sb text-gray-60">우편번호란?</p>
16+
<p className="caption-sb text-gray-60">사용자님이 편지를 주고 받는 주소입니다.</p>
17+
</header>
18+
<section className="flex gap-2">
19+
{DUMMY_ZIPCODE.split('').map((char, index) => (
20+
<Spinner key={index} target={`${char}`} index={index}></Spinner>
21+
))}
22+
</section>
23+
<button
24+
type="button"
25+
className="primary-btn body-m w-full py-2"
26+
onClick={() => setIsZipCodeSet(true)}
27+
>
28+
다음으로
29+
</button>
30+
</>
31+
);
32+
};
33+
34+
export default SetZipCode;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import envelope from '@/assets/images/envelope-pink.png';
2+
import envelopeFront from '@/assets/images/envelope-pink-front.png';
3+
import envelopeTop from '@/assets/images/envelope-pink-back-top.png';
4+
5+
import { useState, useRef, useEffect } from 'react';
6+
import { twMerge } from 'tailwind-merge';
7+
8+
export default function UserInteraction() {
9+
const [startAnimation, setStartAnimation] = useState<Boolean>(false);
10+
const imgRef = useRef<HTMLImageElement>(null);
11+
const [imgPos, setImgPos] = useState<{ top: number; width: number }>({ top: 0, width: 0 });
12+
const [imgToBottom, setImgToBottom] = useState<Boolean>(false);
13+
14+
const handleLetterClick = () => {
15+
if (imgRef.current) {
16+
const rect = imgRef.current.getBoundingClientRect();
17+
setImgPos({ top: rect.top, width: rect.width });
18+
}
19+
setStartAnimation(true);
20+
setTimeout(() => {
21+
setImgToBottom(true);
22+
}, 1000);
23+
};
24+
if (startAnimation === false) {
25+
return (
26+
<>
27+
<header className="flex flex-col items-center">
28+
<h1 className="message-header body-b mb-2">이제 편지를 보내러 가볼까요?</h1>
29+
</header>
30+
<section className="mt-25 flex w-full grow flex-col place-items-center items-center px-10">
31+
<p className="comment caption-m animate-float mb-8">편지를 눌러보세요!</p>
32+
<img
33+
role="button"
34+
ref={imgRef}
35+
src={envelope}
36+
alt="분홍색 편지지"
37+
className="h-auto w-full rounded transition-transform duration-1000 ease-in-out hover:scale-105"
38+
onClick={handleLetterClick}
39+
/>
40+
</section>
41+
</>
42+
);
43+
} else {
44+
return (
45+
<>
46+
<img
47+
src={envelopeFront}
48+
alt="분홍색 편지지"
49+
className={twMerge(
50+
`z-10 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
51+
imgToBottom && 'translate-y-full',
52+
)}
53+
style={{ top: `${imgPos.top}px`, position: 'absolute', width: `${imgPos.width}px` }}
54+
/>
55+
<img
56+
src={envelope}
57+
alt="분홍색 편지지"
58+
className={twMerge(
59+
`z-0 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
60+
imgToBottom && 'translate-y-full',
61+
)}
62+
style={{ top: `${imgPos.top}px`, position: 'absolute', width: `${imgPos.width}px` }}
63+
/>
64+
{/* <img
65+
src={envelopeTop}
66+
alt="분홍색 편지지"
67+
className={twMerge(
68+
`z-30 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
69+
imgToBottom && 'translate-y-full',
70+
)}
71+
style={{ top: `${imgPos.top}px`, position: 'absolute', width: `${imgPos.width}px` }}
72+
/> */}
73+
</>
74+
);
75+
}
76+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// import { forwardRef, useState, useRef, useCallback, useImperativeHandle } from 'react';
2+
3+
import { useEffect, useRef, useState } from 'react';
4+
import { ELEMENTS } from '../constants/index';
5+
6+
interface SpinnerProps {
7+
target: string;
8+
index: number;
9+
}
10+
11+
const Spinner = ({ target, index }: SpinnerProps) => {
12+
const newArr = ELEMENTS.filter((item) => item !== target);
13+
const TARGET_ARR = [target, ...newArr.sort(() => Math.random() - 0.5), target];
14+
const SPEED = 100 + 10 * index;
15+
const [position, setPosition] = useState(0);
16+
const [isRunning, setIsRunning] = useState(true);
17+
let LETTER_HEIGHT = 40;
18+
const animationFrameRef = useRef<number | null>(null);
19+
20+
// calculate full height of the cycle
21+
const containerRef = useRef<HTMLDivElement>(null);
22+
useEffect(() => {
23+
if (containerRef.current) {
24+
const letter = containerRef.current.querySelector('p');
25+
if (letter) {
26+
LETTER_HEIGHT = letter.getBoundingClientRect().height;
27+
}
28+
}
29+
});
30+
const FULL_ROTATION = -TARGET_ARR.length * LETTER_HEIGHT;
31+
32+
useEffect(() => {
33+
if (!isRunning) return;
34+
35+
let lastTime = performance.now();
36+
const frameRate = 1000 / 60;
37+
38+
const animate = (time: number) => {
39+
const deltaTime = time - lastTime;
40+
if (deltaTime >= frameRate) {
41+
setPosition((prev) => {
42+
let newPos = prev - LETTER_HEIGHT * (deltaTime / SPEED);
43+
44+
if (newPos < FULL_ROTATION) {
45+
newPos = 0;
46+
setIsRunning(false);
47+
return newPos;
48+
}
49+
return newPos;
50+
});
51+
lastTime = time;
52+
}
53+
54+
animationFrameRef.current = requestAnimationFrame(animate);
55+
};
56+
57+
animationFrameRef.current = requestAnimationFrame(animate);
58+
59+
return () => {
60+
if (animationFrameRef.current) {
61+
cancelAnimationFrame(animationFrameRef.current);
62+
}
63+
};
64+
}, [isRunning]);
65+
66+
return (
67+
<div
68+
className="bg-gray-10 flex h-13.5 w-10 -translate-y-20 flex-col items-center overflow-hidden rounded-sm inset-shadow-[0_4px_4px_0] inset-shadow-black/10"
69+
style={{ willChange: 'transform' }}
70+
>
71+
<div
72+
className="text-center transition-transform duration-100 ease-linear"
73+
style={{ transform: `translateY(${position}px)`, transitionDuration: '1000ms' }}
74+
>
75+
{TARGET_ARR.map((item, index) => {
76+
return (
77+
<p key={index} className="h1-b">
78+
{item}
79+
</p>
80+
);
81+
})}
82+
</div>
83+
</div>
84+
);
85+
};
86+
87+
export default Spinner;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export const ELEMENTS = [
2+
1,
3+
2,
4+
3,
5+
4,
6+
5,
7+
6,
8+
7,
9+
8,
10+
9,
11+
0,
12+
'A',
13+
'B',
14+
'C',
15+
'D',
16+
'E',
17+
'F',
18+
'G',
19+
'H',
20+
'I',
21+
'J',
22+
'K',
23+
'L',
24+
'M',
25+
'N',
26+
'O',
27+
'P',
28+
'Q',
29+
'R',
30+
'S',
31+
'T',
32+
'U',
33+
'V',
34+
'W',
35+
'X',
36+
'Y',
37+
'Z',
38+
];

src/pages/Onboarding/index.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1+
import { useState } from 'react';
2+
import SetZipCode from './SetZipCode';
3+
import UserInteraction from './UserInteraction';
4+
15
const OnboardingPage = () => {
2-
return <div>OnboardingPage</div>;
6+
const [isZipCodeSet, setIsZipCodeSet] = useState<Boolean>(false);
7+
8+
return (
9+
<main className="mx-5 mt-20 mb-[1.875rem] flex grow flex-col items-center justify-between">
10+
{isZipCodeSet === false ? (
11+
<SetZipCode setIsZipCodeSet={setIsZipCodeSet} />
12+
) : (
13+
<UserInteraction />
14+
)}
15+
</main>
16+
);
317
};
418

519
export default OnboardingPage;

src/styles/components.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@
2222
linear-gradient(180deg, #fad446 0%, #f8de8c 28%, #ffeab8 98.5%);
2323
background-blend-mode: overlay, normal, normal;
2424
}
25+
26+
.message-header {
27+
@apply text-gray-60 w-fit rounded-full bg-white px-6 py-4;
28+
}
2529
}

0 commit comments

Comments
 (0)