-
Notifications
You must be signed in to change notification settings - Fork 1
Feat: 온보딩 UI #10
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
base: develop
Are you sure you want to change the base?
Feat: 온보딩 UI #10
Changes from 7 commits
d9d9659
81222b9
f4a9e4a
4f7e2c5
de71818
b2bcc6d
74cb410
9243742
2499166
f522b7d
d80c9e7
8c7048d
3b7dc44
a4d14e0
1483d0c
3e4517f
f0e0b67
2b1e02d
059fed1
6317409
14cfb75
f1559ac
142e82d
58563bc
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,52 @@ | ||
| "use client"; | ||
|
|
||
| import { useRouter } from "next/navigation"; | ||
|
|
||
| import React from "react"; | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| import OnboardingStep from "../../components/onboarding/OnboardingStep"; | ||
| import ProgressDots from "../../components/onboarding/ProgressDots"; | ||
| import { | ||
| ONBOARDING_STEPS, | ||
| OnboardingStepName, | ||
| } from "../../config/onboardingSteps"; | ||
| import { useFunnel } from "../../hooks/useFunnel"; | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const OnboardingPage = () => { | ||
| const router = useRouter(); | ||
| const { Funnel, Step, setStep, currentStep } = useFunnel(ONBOARDING_STEPS[0]); | ||
|
|
||
| const currentIndex = ONBOARDING_STEPS.indexOf( | ||
| currentStep as OnboardingStepName, | ||
| ); | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const isLast = currentIndex === ONBOARDING_STEPS.length - 1; | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const goNext = () => { | ||
| const next = ONBOARDING_STEPS[currentIndex + 1]; | ||
|
|
||
| if (!next) { | ||
| localStorage.setItem("onboardingDone", "true"); | ||
| router.push("/onboarding/profile"); | ||
| return; | ||
| } | ||
| setStep(next); | ||
| }; | ||
|
|
||
| return ( | ||
| <main className="flex min-h-dvh justify-center"> | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <div className="mt-[13px] flex h-full w-[440px] flex-col"> | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <ProgressDots total={ONBOARDING_STEPS.length} current={currentIndex} /> | ||
|
|
||
| <Funnel> | ||
| {ONBOARDING_STEPS.map(name => ( | ||
| <Step key={name} name={name}> | ||
| <OnboardingStep stepName={name} isLast={isLast} onNext={goNext} /> | ||
| </Step> | ||
| ))} | ||
| </Funnel> | ||
| </div> | ||
| </main> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingPage; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import OnboardingProfileClient from "@/components/onboarding/OnboardingProfile"; | ||
|
|
||
| const OnboardingProfilePage = () => { | ||
| return <OnboardingProfileClient />; | ||
| }; | ||
| export default OnboardingProfilePage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| export default function Home() { | ||
| return <div>홈화면</div>; | ||
| import { redirect } from "next/navigation"; | ||
|
|
||
| export default function RootPage() { | ||
| redirect("/onboarding"); | ||
| } | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useRef, useState } from "react"; | ||
|
|
||
| interface NameInputProps { | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| } | ||
|
|
||
| const PLACEHOLDER = "이름을 입력해주세요"; | ||
|
|
||
| const INITIAL_WIDTH = 204; | ||
| const NameInput = ({ value, onChange }: NameInputProps) => { | ||
|
Member
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. 컴포넌트에서는 export default형식이 아니라 export const 형식으로 사용해주세요 |
||
| const spanRef = useRef<HTMLSpanElement>(null); | ||
| const [inputWidth, setInputWidth] = useState<number>(INITIAL_WIDTH); | ||
|
|
||
| useEffect(() => { | ||
| if (!spanRef.current) return; | ||
|
|
||
| const spanWidth = spanRef.current.offsetWidth; | ||
| setInputWidth(spanWidth + 36); | ||
| }, [value]); | ||
|
|
||
| return ( | ||
| <div className="flex justify-center"> | ||
| <span ref={spanRef} className="text-d3 invisible absolute whitespace-pre"> | ||
| {value || PLACEHOLDER} | ||
| </span> | ||
|
|
||
| <input | ||
| type="text" | ||
| value={value} | ||
| onChange={e => onChange(e.target.value)} | ||
| placeholder={PLACEHOLDER} | ||
| style={{ width: inputWidth }} | ||
| className="border-mint-01 text-d3 text-neutral-01 placeholder:text-neutral-07 rounded-2xl border px-4 py-2 text-center transition-[width] duration-150 ease-out outline-none" | ||
| /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default NameInput; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
|
|
||
| import AlertModal from "@/components/common/AlertModal"; | ||
| import FullButton from "@/components/common/FullButton"; | ||
| import NameInput from "@/components/common/NameInput"; | ||
| import ProfileImagePicker from "@/components/onboarding/ProfileImagePicker"; | ||
| import UploadAlert from "@/components/onboarding/UploadAlert"; | ||
|
|
||
| import { useProfileImageUpload } from "@/hooks/useProfileImageUpload"; | ||
|
|
||
| const OnboardingProfileClient = () => { | ||
| const [name, setName] = useState(""); | ||
| const [isUploadOpen, setIsUploadOpen] = useState(false); | ||
|
|
||
| const isNameValid = name.trim().length > 0; | ||
|
|
||
| const { profileImage, isLoading, uploadImage, resetImage } = | ||
| useProfileImageUpload(); | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <> | ||
| <main className="flex min-h-dvh justify-center"> | ||
| <div className="mt-[13px] flex h-full w-[440px] flex-col"> | ||
| <section className="mt-[123px] px-4 py-2.5"> | ||
| <p className="text-d2 text-neutral-02 whitespace-pre-line"> | ||
| 가족들에게 보여줄{"\n"}내 프로필을 만들어주세요 | ||
| </p> | ||
| </section> | ||
| <section className="mt-[86px] flex flex-col items-center"> | ||
| <ProfileImagePicker | ||
| imageUrl={profileImage} | ||
| onClick={() => setIsUploadOpen(true)} | ||
| /> | ||
| <div className="mt-4 w-full px-[118px]"> | ||
| <NameInput value={name} onChange={setName} /> | ||
| </div> | ||
| </section> | ||
|
Comment on lines
+56
to
+64
Member
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. 여기는 flex구조라서 mt-4로 간격 넣는 것보다는 gap-4로 간격 넣는게 더 좋을 것 같습니다 |
||
|
|
||
| <section className="mt-[210px] mb-[42px] px-4 py-2.5"> | ||
| <FullButton isActive={isNameValid}>프로필생성하기</FullButton> | ||
| </section> | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </div> | ||
| </main> | ||
| <UploadAlert | ||
| isOpen={isUploadOpen} | ||
| onClose={() => setIsUploadOpen(false)} | ||
| onSelectAlbum={file => { | ||
| uploadImage(file); | ||
| setIsUploadOpen(false); | ||
| }} | ||
| onSelectDefault={() => { | ||
| resetImage(); | ||
| setIsUploadOpen(false); | ||
| }} | ||
| /> | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| <AlertModal | ||
| isOpen={isLoading} | ||
| title="로딩중" | ||
| confirmLabel="취소하기" | ||
| isLoading | ||
| loadingImageSrc="/loading.gif" | ||
| backdrop="blur" | ||
| onClose={() => {}} | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /> | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingProfileClient; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import Image from "next/image"; | ||
|
|
||
| import FullButton from "@/components/common/FullButton"; | ||
|
|
||
| import type { OnboardingStepName } from "../../config/onboardingSteps"; | ||
| import { onboardingContents } from "../../config/onboardingSteps"; | ||
|
|
||
| interface Props { | ||
| stepName: OnboardingStepName; | ||
| isLast: boolean; | ||
| onNext: () => void; | ||
| } | ||
|
|
||
| const OnboardingStep = ({ stepName, isLast, onNext }: Props) => { | ||
| const { title, background } = onboardingContents[stepName]; | ||
|
|
||
| return ( | ||
| <main className="relative flex h-screen flex-col bg-white pt-[558px]"> | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| {background.type === "image" && ( | ||
| <Image | ||
| src={background.src} | ||
| alt="" | ||
| fill | ||
| priority | ||
| className="pointer-events-none -translate-y-[110px] object-cover object-top" | ||
| /> | ||
| )} | ||
| {background.type === "class" && ( | ||
| <div | ||
| className={`pointer-events-none absolute -translate-y-[110px] ${background.className}`} | ||
| aria-hidden | ||
| /> | ||
| )} | ||
|
|
||
| <section className="relative z-10 px-4 py-2.5"> | ||
| <p className="text-d2 text-neutral-02 whitespace-pre-line">{title}</p> | ||
| </section> | ||
|
|
||
| <section className="relative z-10 px-4 pt-[23px] pb-[78px]"> | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <FullButton type="button" onClick={onNext} isActive> | ||
| {isLast ? "시작하기" : "다음"} | ||
| </FullButton> | ||
| </section> | ||
| </main> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingStep; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| "use client"; | ||
|
|
||
| import ProfileIcon from "@/assets/onboarding_profile.svg"; | ||
| import PhotoIcon from "@/assets/photo.svg"; | ||
|
|
||
| type ProfileImagePickerProps = { | ||
| imageUrl: string | null; | ||
| onClick: () => void; | ||
| }; | ||
|
|
||
| const ProfileImagePicker = ({ imageUrl, onClick }: ProfileImagePickerProps) => { | ||
| return ( | ||
| <div | ||
| className="relative flex h-[160px] w-[160px] cursor-pointer items-center justify-center" | ||
|
Member
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. 여기부분에서 h-40 w-40이렇게 사용해줘도 좋을 것 같아요 |
||
| onClick={onClick} | ||
| > | ||
| {imageUrl ? ( | ||
| <img | ||
| src={imageUrl} | ||
| alt="프로필 이미지" | ||
| className="h-full w-full rounded-[36px] object-cover" | ||
| /> | ||
| ) : ( | ||
| <ProfileIcon /> | ||
| )} | ||
|
|
||
| <button | ||
| type="button" | ||
| className="absolute right-[16px] bottom-[16px] h-[32px] w-[32px]" | ||
| > | ||
| <PhotoIcon /> | ||
|
Member
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. button에 아이콘 크기를 지정하는게 아니라 아이콘 내부에 h-8 w-8을 적용해야할 것 같습니다. |
||
| </button> | ||
idid10 marked this conversation as resolved.
Show resolved
Hide resolved
idid10 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ProfileImagePicker; | ||
Uh oh!
There was an error while loading. Please reload this page.