diff --git a/app/analyzers/clarity.tsx b/app/analyzers/clarity.tsx new file mode 100644 index 00000000..b1cb5879 --- /dev/null +++ b/app/analyzers/clarity.tsx @@ -0,0 +1,15 @@ +import Script from 'next/script'; + +export const Clarity = () => { + return ( + + ); +}; diff --git a/app/analyzers/index.ts b/app/analyzers/index.ts new file mode 100644 index 00000000..5b96b769 --- /dev/null +++ b/app/analyzers/index.ts @@ -0,0 +1 @@ +export * from './clarity'; diff --git a/app/join/finish/page.tsx b/app/join/finish/page.tsx index 2f0a32d0..4e638076 100644 --- a/app/join/finish/page.tsx +++ b/app/join/finish/page.tsx @@ -14,8 +14,8 @@ export default function Page() { const router = useRouter(); const authInfo = useAtomValue(AuthInfoAtom); - const handleGoToMain = () => { - router.push('/'); + const handleStartClick = () => { + router.push('/on-boarding'); }; return ( @@ -41,7 +41,7 @@ export default function Page() { buttonType="primary" size="large" className={buttonStyles} - onClick={handleGoToMain} + onClick={handleStartClick} /> diff --git a/app/layout.tsx b/app/layout.tsx index 5a564d0a..601841ff 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,6 +9,7 @@ import MetaTagImage from '@/public/images/meta-tag.png'; import { css } from '@/styled-system/css'; import { pretendard } from '@/styles/font'; +import { Clarity } from './analyzers'; import ReactQueryProvider from './providers/ReactQueryProvider'; export const metadata: Metadata = { @@ -62,6 +63,9 @@ export default function RootLayout({ }>) { return ( + + + diff --git a/app/on-boarding/page.tsx b/app/on-boarding/page.tsx new file mode 100644 index 00000000..9826376e --- /dev/null +++ b/app/on-boarding/page.tsx @@ -0,0 +1,101 @@ +'use client'; + +import '../../features/on-boarding/steps.css'; + +import Link from 'next/link'; +import { cloneElement, ReactElement } from 'react'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + +import { Button } from '@/components/atoms'; +import { BackButton, HeaderBar } from '@/components/molecules'; +import { + ProgressIndicator, + SkipButton, + Steps, + stepsIntroduce, + useProgressIndicator, +} from '@/features/on-boarding'; +import { css } from '@/styled-system/css'; + +export default function OnBoarding() { + const { step, handlers } = useProgressIndicator(); + return ( +
+ + + + + + + + + {[ + { + component: ( + + ), + key: 'skipButton', + }, + ]} + + + { + return cloneElement(child, { + classNames: 'fade', + }); + }} + > + + + + +
+ {step < stepsIntroduce.length - 1 ? ( +
+
+ ); +} + +const layoutStyles = { + total: css({ + display: 'flex', + justifyContent: 'center', + backgroundColor: 'background.gray', + height: '100dvh', + }), + button: css({ + w: 'full', + maxWidth: 'maxWidth', + position: 'fixed', + bottom: 'calc(15px + env(safe-area-inset-bottom))', + padding: '0 20px', + }), +}; diff --git a/components/molecules/header-bar/header-bar.tsx b/components/molecules/header-bar/header-bar.tsx index a2d1cb13..dd35ac7d 100644 --- a/components/molecules/header-bar/header-bar.tsx +++ b/components/molecules/header-bar/header-bar.tsx @@ -9,34 +9,36 @@ import { import { css, cx } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; -import { leftIconStyles } from './style'; - -interface LeftContentProps { +interface CommonProps { children?: ReactNode; className?: string; } -function LeftContent({ children, className }: LeftContentProps) { - return
{children}
; +function LeftContent({ children, className }: CommonProps) { + return ( +
{children}
+ ); } -interface TitleProps { - children?: ReactNode; - className?: string; +function Title({ children, className }: CommonProps) { + return ( +

{children}

+ ); } -function Title({ children, className }: TitleProps) { - return

{children}

; +function CenterContent({ children, className }: CommonProps) { + return ( +
{children}
+ ); } -interface RightContentProps { +interface RightContentProps extends Pick { children?: { component: ReactNode; key: string | number }[]; - className?: string; } function RightContent({ children, className }: RightContentProps) { return ( -
+
{children?.map((object) => (
{object.component}
))} @@ -61,14 +63,24 @@ const getChildrenArray = ( interface HeaderBarProps { children?: ReactNode; className?: string; + innerClassName?: string; } -function HeaderBarLayout({ children, className }: HeaderBarProps) { +function HeaderBarLayout({ + children, + className, + innerClassName, +}: HeaderBarProps) { const leftContent = getChildrenArray( children, 1, ().type as FunctionComponent, ); + const centerContent = getChildrenArray( + children, + 1, + ().type as FunctionComponent, + ); const title = getChildrenArray( children, 1, @@ -82,9 +94,10 @@ function HeaderBarLayout({ children, className }: HeaderBarProps) { return ( <>
-
-
+
+
{leftContent} + {centerContent} {title} {rightContent}
@@ -96,6 +109,7 @@ function HeaderBarLayout({ children, className }: HeaderBarProps) { export const HeaderBar = Object.assign(HeaderBarLayout, { LeftContent, Title, + CenterContent, RightContent, }); @@ -111,23 +125,27 @@ const layoutStyles = { backgroundColor: 'white', zIndex: 200, }), - content: css({ + totalContent: css({ position: 'relative', display: 'flex', alignItems: 'center', w: 'full', }), - rightIcon: flex({ + leftContent: flex({ + position: 'absolute', + alignItems: 'center', + left: '12px', + }), + centerContent: flex({ + justifyContent: 'center', + alignItems: 'center', + w: 'full', + textStyle: 'heading6', + fontWeight: 500, + }), + rightContent: flex({ position: 'absolute', right: '12px', gap: '24px', }), }; - -const titleStyles = flex({ - justifyContent: 'center', - alignItems: 'center', - w: 'full', - textStyle: 'heading6', - fontWeight: 500, -}); diff --git a/components/molecules/header-bar/index.ts b/components/molecules/header-bar/index.ts index 4c8b4524..46d5758a 100644 --- a/components/molecules/header-bar/index.ts +++ b/components/molecules/header-bar/index.ts @@ -3,4 +3,3 @@ export * from './header-bar'; export * from './header-bar-notification-button'; export * from './header-bar-setting-button'; export * from './header-logo-button'; -export * from './style'; diff --git a/components/molecules/header-bar/style.ts b/components/molecules/header-bar/style.ts deleted file mode 100644 index 7142adde..00000000 --- a/components/molecules/header-bar/style.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { flex } from '@/styled-system/patterns'; - -export const leftIconStyles = flex({ - position: 'absolute', - alignItems: 'center', - left: '12px', -}); diff --git a/features/on-boarding/components/atoms/index.ts b/features/on-boarding/components/atoms/index.ts new file mode 100644 index 00000000..b6813351 --- /dev/null +++ b/features/on-boarding/components/atoms/index.ts @@ -0,0 +1 @@ +export * from './skip-button'; diff --git a/features/on-boarding/components/atoms/skip-button.tsx b/features/on-boarding/components/atoms/skip-button.tsx new file mode 100644 index 00000000..256b2f5e --- /dev/null +++ b/features/on-boarding/components/atoms/skip-button.tsx @@ -0,0 +1,20 @@ +import { css } from '@/styled-system/css'; + +interface SkipButtonProps { + label: string; + onClick: () => void; +} + +export function SkipButton({ label, onClick }: SkipButtonProps) { + return ( + + ); +} + +const skipButtonStyles = css({ + textStyle: 'label1.normal', + color: 'text.alternative', + fontWeight: 500, +}); diff --git a/features/on-boarding/components/index.ts b/features/on-boarding/components/index.ts new file mode 100644 index 00000000..f85714f5 --- /dev/null +++ b/features/on-boarding/components/index.ts @@ -0,0 +1,2 @@ +export * from './atoms'; +export * from './molecules'; diff --git a/features/on-boarding/components/molecules/index.ts b/features/on-boarding/components/molecules/index.ts new file mode 100644 index 00000000..44622b31 --- /dev/null +++ b/features/on-boarding/components/molecules/index.ts @@ -0,0 +1,2 @@ +export * from './progress-indicator'; +export * from './steps'; diff --git a/features/on-boarding/components/molecules/progress-indicator.tsx b/features/on-boarding/components/molecules/progress-indicator.tsx new file mode 100644 index 00000000..ff6fd9b6 --- /dev/null +++ b/features/on-boarding/components/molecules/progress-indicator.tsx @@ -0,0 +1,40 @@ +import { css, cva } from '@/styled-system/css'; +import { flex } from '@/styled-system/patterns'; + +import { stepsIntroduce } from '../../constants'; + +interface ProgressIndicatorProps { + step: number; +} + +export function ProgressIndicator({ step }: ProgressIndicatorProps) { + return ( +
+ {Array.from({ length: stepsIntroduce.length }, (_, i) => ( + + ))} +
+ ); +} + +interface IndicatorDotProps { + selected: boolean; +} + +function IndicatorDot({ selected }: IndicatorDotProps) { + return
; +} + +const progressIndicatorStyles = flex({ + gap: '6px', +}); + +const indicatorDotStyles = cva({ + base: { display: 'flex', width: '6px', height: '6px', borderRadius: 'full' }, + variants: { + selected: { + true: { backgroundColor: 'blue.70' }, + false: { backgroundColor: 'line.neutral' }, + }, + }, +}); diff --git a/features/on-boarding/components/molecules/steps.tsx b/features/on-boarding/components/molecules/steps.tsx new file mode 100644 index 00000000..9d482cc7 --- /dev/null +++ b/features/on-boarding/components/molecules/steps.tsx @@ -0,0 +1,76 @@ +import { Fragment } from 'react'; + +import { Image } from '@/components/atoms'; +import { css } from '@/styled-system/css'; +import { flex } from '@/styled-system/patterns'; + +import { stepsIntroduce } from '../../constants'; + +interface StepsProps { + current: number; +} + +export function Steps({ current }: StepsProps) { + const getHighlightedText = (text: string, highlightTexts: string[]) => { + const regex = new RegExp(`(${highlightTexts.join('|')})`, 'g'); + const parts = text.split(regex); + + return parts.map((part, index) => + highlightTexts.includes(part) ? ( + + {part} + + ) : ( + {part} + ), + ); + }; + + return ( +
+

+ {getHighlightedText( + stepsIntroduce[current].total, + stepsIntroduce[current].highlight, + )} +

+
+ 온보딩 이미지 +
+
+ ); +} + +const layout = { + total: flex({ + direction: 'column', + top: '87px', + height: 'calc(100dvh - 87px)', + position: 'absolute', + }), + image: css({ + position: 'relative', + w: 'full', + height: '68dvh', + }), +}; + +const textStyles = { + total: css({ + textStyle: 'heading3', + padding: '0 30px', + fontWeight: 600, + textAlign: 'center', + wordBreak: 'keep-all', + marginBottom: '6px', + }), + highlight: css({ + color: 'primary.swim.총거리.default', + }), +}; diff --git a/features/on-boarding/constants/index.ts b/features/on-boarding/constants/index.ts new file mode 100644 index 00000000..6cd3df46 --- /dev/null +++ b/features/on-boarding/constants/index.ts @@ -0,0 +1 @@ +export * from './steps-introduce'; diff --git a/features/on-boarding/constants/steps-introduce.ts b/features/on-boarding/constants/steps-introduce.ts new file mode 100644 index 00000000..669f32ae --- /dev/null +++ b/features/on-boarding/constants/steps-introduce.ts @@ -0,0 +1,39 @@ +import OnBoardingImage0 from '@/public/images/on-boarding/on-boarding-image0.png'; +import OnBoardingImage1 from '@/public/images/on-boarding/on-boarding-image1.png'; +import OnBoardingImage2 from '@/public/images/on-boarding/on-boarding-image2.png'; +import OnBoardingImage3 from '@/public/images/on-boarding/on-boarding-image3.png'; +import OnBoardingImage4 from '@/public/images/on-boarding/on-boarding-image4.png'; +import OnBoardingImage5 from '@/public/images/on-boarding/on-boarding-image5.png'; + +export const stepsIntroduce = [ + { + total: '캘린더에서 날짜를 클릭하면 수영을 기록할 수 있어요', + highlight: ['날짜를 클릭'], + image: OnBoardingImage0, + }, + { + total: '스마트 워치가 없어도 바퀴 수로 거리 기록이 가능해요', + highlight: ['바퀴 수'], + image: OnBoardingImage1, + }, + { + total: '기록한 수영 거리에 따라 물결 높이가 달라져요', + highlight: ['물결 높이'], + image: OnBoardingImage2, + }, + { + total: '영법별로 기록하면 색으로 구분되어 보여요', + highlight: ['영법별', '색'], + image: OnBoardingImage3, + }, + { + total: '캘린더와 타임라인으로 나의 실력 성장을 확인해요', + highlight: ['캘린더', '타임라인'], + image: OnBoardingImage4, + }, + { + total: '수영 친구를 팔로우하고 응원을 주고받아요', + highlight: ['수영 친구를 팔로우'], + image: OnBoardingImage5, + }, +]; diff --git a/features/on-boarding/hooks/index.ts b/features/on-boarding/hooks/index.ts new file mode 100644 index 00000000..afd0130d --- /dev/null +++ b/features/on-boarding/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-progress-indicator'; diff --git a/features/on-boarding/hooks/use-progress-indicator.tsx b/features/on-boarding/hooks/use-progress-indicator.tsx new file mode 100644 index 00000000..130de29b --- /dev/null +++ b/features/on-boarding/hooks/use-progress-indicator.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useState } from 'react'; + +export function useProgressIndicator() { + const [step, setStep] = useState(0); + + const next = () => { + setStep((prev) => (prev += 1)); + }; + + const skip = () => { + setStep(5); + }; + + return { step, handlers: { next, skip } }; +} diff --git a/features/on-boarding/index.ts b/features/on-boarding/index.ts new file mode 100644 index 00000000..51746607 --- /dev/null +++ b/features/on-boarding/index.ts @@ -0,0 +1,3 @@ +export * from './components'; +export * from './constants'; +export * from './hooks'; diff --git a/features/on-boarding/steps.css b/features/on-boarding/steps.css new file mode 100644 index 00000000..7ab1ce96 --- /dev/null +++ b/features/on-boarding/steps.css @@ -0,0 +1,14 @@ +.fade-enter { + opacity: 0; +} +.fade-enter-active { + opacity: 1; + transition: opacity 200ms ease-in; +} +.fade-exit { + opacity: 0; +} +.fade-exit-active { + opacity: 0; + transition: opacity 0 ease-in; +} diff --git a/public/images/on-boarding/on-boarding-image0.png b/public/images/on-boarding/on-boarding-image0.png new file mode 100644 index 00000000..b0177d37 Binary files /dev/null and b/public/images/on-boarding/on-boarding-image0.png differ diff --git a/public/images/on-boarding/on-boarding-image1.png b/public/images/on-boarding/on-boarding-image1.png new file mode 100644 index 00000000..3481aeb0 Binary files /dev/null and b/public/images/on-boarding/on-boarding-image1.png differ diff --git a/public/images/on-boarding/on-boarding-image2.png b/public/images/on-boarding/on-boarding-image2.png new file mode 100644 index 00000000..6c7b725f Binary files /dev/null and b/public/images/on-boarding/on-boarding-image2.png differ diff --git a/public/images/on-boarding/on-boarding-image3.png b/public/images/on-boarding/on-boarding-image3.png new file mode 100644 index 00000000..ae9a7de7 Binary files /dev/null and b/public/images/on-boarding/on-boarding-image3.png differ diff --git a/public/images/on-boarding/on-boarding-image4.png b/public/images/on-boarding/on-boarding-image4.png new file mode 100644 index 00000000..f48777fb Binary files /dev/null and b/public/images/on-boarding/on-boarding-image4.png differ diff --git a/public/images/on-boarding/on-boarding-image5.png b/public/images/on-boarding/on-boarding-image5.png new file mode 100644 index 00000000..09b5bfc2 Binary files /dev/null and b/public/images/on-boarding/on-boarding-image5.png differ