-
Notifications
You must be signed in to change notification settings - Fork 2
design: 온보딩 페이지 퍼블리싱 #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
360973b
223950a
c3ecfd2
77c21db
4f3b0cf
7a3ca58
cfbc00f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import React from 'react'; | ||
| import Spinner from './components/Spinner'; | ||
|
|
||
| const SetZipCode = ({ | ||
| setIsZipCodeSet, | ||
| }: { | ||
| setIsZipCodeSet: React.Dispatch<React.SetStateAction<Boolean>>; | ||
| }) => { | ||
| const DUMMY_ZIPCODE = '122A2'; | ||
|
|
||
| return ( | ||
| <> | ||
| <header className="flex flex-col items-center"> | ||
| <h1 className="message-header body-b mb-2">우편번호를 설정하고 있습니다.</h1> | ||
| <p className="caption-sb text-gray-60">우편번호란?</p> | ||
| <p className="caption-sb text-gray-60">사용자님이 편지를 주고 받는 주소입니다.</p> | ||
| </header> | ||
| <section className="flex gap-2"> | ||
| {DUMMY_ZIPCODE.split('').map((char, index) => ( | ||
| <Spinner key={index} target={`${char}`} index={index}></Spinner> | ||
| ))} | ||
| </section> | ||
| <button | ||
| type="button" | ||
| className="primary-btn body-m w-full py-2" | ||
| onClick={() => setIsZipCodeSet(true)} | ||
| > | ||
| 다음으로 | ||
| </button> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default SetZipCode; |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뭔가 엄청난 싸움을 하신 것 같은데요...!!!! 수고하셨습니다
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 휘유~ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import envelope from '@/assets/images/envelope-pink.png'; | ||
| import envelopeFront from '@/assets/images/envelope-pink-front.png'; | ||
| import envelopeTop from '@/assets/images/envelope-pink-back-top.png'; | ||
|
|
||
| import { useState, useRef, useEffect } from 'react'; | ||
| import { twMerge } from 'tailwind-merge'; | ||
| import { Link } from 'react-router'; | ||
|
|
||
| export default function UserInteraction() { | ||
| const imgRef = useRef<HTMLImageElement>(null); | ||
| const [imgPos, setImgPos] = useState<{ top: number; width: number }>({ top: 0, width: 0 }); | ||
| const [imgToBottom, setImgToBottom] = useState<Boolean>(false); | ||
|
|
||
| const [startAnimation, setStartAnimation] = useState<Boolean>(false); | ||
| const [openAnimation, setOpenAnimation] = useState<Boolean>(false); | ||
| const [letterOutAnimation, setLetterOutAnimation] = useState<Boolean>(false); | ||
| const [envelopeOut, setEnvelopeOut] = useState<Boolean>(false); | ||
| const [finishAnimation, setFinishAnimation] = useState<Boolean>(false); | ||
|
|
||
| const handleLetterClick = () => { | ||
| if (imgRef.current) { | ||
| const rect = imgRef.current.getBoundingClientRect(); | ||
| setImgPos({ top: rect.top, width: rect.width }); | ||
| } | ||
| setStartAnimation(true); | ||
| setTimeout(() => { | ||
| setImgToBottom(true); | ||
| }, 1000); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (imgToBottom) { | ||
| setTimeout(() => { | ||
| setOpenAnimation(true); | ||
| }, 1000); | ||
| } | ||
| }, [imgToBottom]); | ||
|
|
||
| useEffect(() => { | ||
| if (openAnimation) { | ||
| setTimeout(() => { | ||
| setLetterOutAnimation(true); | ||
| }, 2000); | ||
| } | ||
| }, [openAnimation]); | ||
|
|
||
| useEffect(() => { | ||
| if (letterOutAnimation) { | ||
| setTimeout(() => { | ||
| setEnvelopeOut(true); | ||
| }, 2000); | ||
| } | ||
| }, [letterOutAnimation]); | ||
|
|
||
| useEffect(() => { | ||
| if (envelopeOut) { | ||
| setTimeout(() => { | ||
| setFinishAnimation(true); | ||
| }, 2000); | ||
| } | ||
| }, [envelopeOut]); | ||
| if (startAnimation === false) { | ||
| return ( | ||
| <> | ||
| <header className="flex flex-col items-center"> | ||
| <h1 className="message-header body-b mb-2">이제 편지를 보내러 가볼까요?</h1> | ||
| </header> | ||
| <section className="mt-25 flex w-full grow flex-col place-items-center items-center px-10"> | ||
| <p className="comment caption-m animate-float mb-8">편지를 눌러보세요!</p> | ||
| <img | ||
| role="button" | ||
| ref={imgRef} | ||
| src={envelope} | ||
| alt="분홍색 편지지" | ||
| className="h-auto w-full rounded transition-transform duration-1000 ease-in-out hover:scale-105" | ||
| onClick={handleLetterClick} | ||
| /> | ||
| </section> | ||
| </> | ||
| ); | ||
| } else { | ||
| return ( | ||
| <> | ||
| <img | ||
| src={envelopeFront} | ||
| alt="분홍색 편지지" | ||
| className={twMerge( | ||
| `z-30 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`, | ||
| imgToBottom && 'translate-y-full', | ||
| envelopeOut && 'animate-envelopeOut', | ||
| )} | ||
| style={{ | ||
| top: `${imgPos.top}px`, | ||
| position: 'absolute', | ||
| width: `${imgPos.width}px`, | ||
| }} | ||
| /> | ||
| {letterOutAnimation && ( | ||
| <div | ||
| className="animate-expandScale to-gray-5 z-20 max-w-[600px] rounded bg-linear-to-b from-white" | ||
| style={{ | ||
| width: `${imgPos.width - imgPos.width * 0.1}px`, | ||
| bottom: `${imgPos.top - 0.7 * imgPos.top}px`, | ||
| top: `${imgPos.top - 0.5 * imgPos.top}px`, | ||
| position: 'absolute', | ||
| }} | ||
| ></div> | ||
| )} | ||
| {openAnimation && ( | ||
| <img | ||
| src={envelopeTop} | ||
| alt="" | ||
| className={twMerge( | ||
| `z-10 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`, | ||
| imgToBottom && 'translate-y-full', | ||
| openAnimation && 'animate-openEnvelope', | ||
| envelopeOut && 'animate-envelopeOut', | ||
| )} | ||
| style={{ | ||
| top: `${imgPos.top}px`, | ||
| position: 'absolute', | ||
| width: `${imgPos.width}px`, | ||
| transformOrigin: 'bottom', | ||
| }} | ||
| /> | ||
| )} | ||
| <img | ||
| src={envelope} | ||
| alt="분홍색 편지지" | ||
| className={twMerge( | ||
| `z-0 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`, | ||
| imgToBottom && 'translate-y-full', | ||
| envelopeOut && 'animate-envelopeOut', | ||
| )} | ||
| style={{ | ||
| top: `${imgPos.top}px`, | ||
| position: 'absolute', | ||
| width: `${imgPos.width}px`, | ||
| }} | ||
| /> | ||
| {/* TODO: 편지지 링크 */} | ||
| {finishAnimation && <Link to={'/'}></Link>} | ||
| </> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import { ELEMENTS } from '../constants/index'; | ||
|
|
||
| interface SpinnerProps { | ||
| target: string; | ||
| index: number; | ||
| } | ||
|
|
||
| const Spinner = ({ target, index }: SpinnerProps) => { | ||
| const newArr = ELEMENTS.filter((item) => item !== target); | ||
| const TARGET_ARR = [target, ...newArr.sort(() => Math.random() - 0.5), target]; | ||
| const SPEED = 100 + 10 * index; | ||
| const [position, setPosition] = useState(0); | ||
| const [isRunning, setIsRunning] = useState(true); | ||
| let LETTER_HEIGHT = 40; | ||
| const animationFrameRef = useRef<number | null>(null); | ||
|
|
||
| // calculate full height of the cycle | ||
| const containerRef = useRef<HTMLDivElement>(null); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. containerRef 선언은 되어 있는데 연결되어 있지 않은 것 같아요
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오호.. 일단 연결해 두었습니다! 웹에서는 차이가 크게 없어서 빼도 될 것 같은데, 모바일에서 어떨지 모르겠어서 일단 뺼지 둘지는 TODO로 남겨 두었습니당 |
||
| useEffect(() => { | ||
| if (containerRef.current) { | ||
| const letter = containerRef.current.querySelector('p'); | ||
| if (letter) { | ||
| LETTER_HEIGHT = letter.getBoundingClientRect().height; | ||
| } | ||
| } | ||
| }); | ||
|
||
| const FULL_ROTATION = -TARGET_ARR.length * LETTER_HEIGHT; | ||
|
|
||
| useEffect(() => { | ||
| if (!isRunning) return; | ||
|
|
||
| let lastTime = performance.now(); | ||
| const frameRate = 1000 / 60; | ||
|
|
||
| const animate = (time: number) => { | ||
| const deltaTime = time - lastTime; | ||
| if (deltaTime >= frameRate) { | ||
| setPosition((prev) => { | ||
| let newPos = prev - LETTER_HEIGHT * (deltaTime / SPEED); | ||
|
|
||
| if (newPos < FULL_ROTATION) { | ||
| newPos = 0; | ||
| setIsRunning(false); | ||
| return newPos; | ||
| } | ||
| return newPos; | ||
| }); | ||
| lastTime = time; | ||
| } | ||
|
|
||
| animationFrameRef.current = requestAnimationFrame(animate); | ||
| }; | ||
|
|
||
| animationFrameRef.current = requestAnimationFrame(animate); | ||
|
Comment on lines
+56
to
+59
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거 문제 없는 게 맞을까요? animate 안에 animate를 호출하고 있어서요!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵넵! 보통 requestAnimationFrame은 이런식으로 사용한다고 합니다! requestAnimationFrame에 등록된 콜백함수들을 비동기로 호출하기 때문에 상관 없어요! https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하!! 블로그 내용보고 살짝 겁먹었지만 시간날 때 천천히 읽어볼게요!!! |
||
|
|
||
| return () => { | ||
| if (animationFrameRef.current) { | ||
| cancelAnimationFrame(animationFrameRef.current); | ||
| } | ||
| }; | ||
| }, [isRunning]); | ||
|
|
||
| return ( | ||
| <div | ||
| 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" | ||
| style={{ willChange: 'transform' }} | ||
| > | ||
| <div | ||
| className="text-center transition-transform duration-100 ease-linear" | ||
| style={{ transform: `translateY(${position}px)`, transitionDuration: '500ms' }} | ||
| > | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인라인 스타일 duration이랑 tailwind duration이 충돌나고 있는 것 같습니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 확인 감사합니당 |
||
| {TARGET_ARR.map((item, index) => { | ||
| return ( | ||
| <p key={index} className="h1-b"> | ||
| {item} | ||
| </p> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Spinner; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| export const ELEMENTS = [ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| 6, | ||
| 7, | ||
| 8, | ||
| 9, | ||
| 0, | ||
| 'A', | ||
| 'B', | ||
| 'C', | ||
| 'D', | ||
| 'E', | ||
| 'F', | ||
| 'G', | ||
| 'H', | ||
| 'I', | ||
| 'J', | ||
| 'K', | ||
| 'L', | ||
| 'M', | ||
| 'N', | ||
| 'O', | ||
| 'P', | ||
| 'Q', | ||
| 'R', | ||
| 'S', | ||
| 'T', | ||
| 'U', | ||
| 'V', | ||
| 'W', | ||
| 'X', | ||
| 'Y', | ||
| 'Z', | ||
| ]; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,19 @@ | ||||||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||||||
| import SetZipCode from './SetZipCode'; | ||||||||||||||||||||||
| import UserInteraction from './UserInteraction'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const OnboardingPage = () => { | ||||||||||||||||||||||
| return <div>OnboardingPage</div>; | ||||||||||||||||||||||
| const [isZipCodeSet, setIsZipCodeSet] = useState<Boolean>(false); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <main className="inset-0 mx-5 mt-20 mb-[1.875rem] flex grow flex-col items-center justify-between overflow-hidden"> | ||||||||||||||||||||||
| {isZipCodeSet === false ? ( | ||||||||||||||||||||||
| <SetZipCode setIsZipCodeSet={setIsZipCodeSet} /> | ||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||
| <UserInteraction /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
||||||||||||||||||||||
| {isZipCodeSet === false ? ( | |
| <SetZipCode setIsZipCodeSet={setIsZipCodeSet} /> | |
| ) : ( | |
| <UserInteraction /> | |
| )} | |
| {isZipCodeSet ? ( | |
| <UserInteraction /> | |
| ) : ( | |
| <SetZipCode setIsZipCodeSet={setIsZipCodeSet} /> | |
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수정했습니당
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지원님이 closed-letter.png라는 이름으로 에셋 추가해주셨는데 겹치는 것 같아요! 둘 중 하나로 통일해야할 것 같습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제 이미지 삭제 후, 지원님 이미지로 통일했습니다!
envelope-pink-front -> opened-letter-front
envelope-pink -> closed-letter