Skip to content

Commit

Permalink
feat: 가이드 페이지 구현 (#134)
Browse files Browse the repository at this point in the history
* feat: 페이지 구성 및 연결

* feat: Stepper 구현

* refactor: 상위 컴포넌트에서 훅 props로 받아쓰도록 변경

* feat: 온보딩 이미지 추가

* feat: step에 따른 내용 추가

* test: args 설정

* refactor: 리뷰 반영

* feat: 첫 방문시 온보딩 페이지로 리디렉션
  • Loading branch information
hae-on authored Jun 7, 2024
1 parent 5f9d76f commit 914bbb2
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 4 deletions.
Binary file added src/assets/onboarding-member.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/onboarding-recipe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/onboarding-review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/components/Common/Stepper/Stepper.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from '@storybook/react';

import Stepper from './Stepper';

const meta: Meta<typeof Stepper> = {
title: 'common/Stepper',
component: Stepper,
args: {
selectedStepper: '0',
},
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
35 changes: 35 additions & 0 deletions src/components/Common/Stepper/Stepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { MouseEventHandler } from 'react';

import { container, stepper, wrapper } from './stepper.css';

interface StepperProps {
selectedStepper: number;
handleStepperSelect: (selectedStepper: string) => void;
}

const Stepper = ({ selectedStepper, handleStepperSelect }: StepperProps) => {
const handleStepperClick: MouseEventHandler<HTMLButtonElement> = (event) => {
handleStepperSelect(event.currentTarget.value);
};

return (
<ul className={container}>
{Array.from({ length: 3 }, (_, index) => {
const isSelected = selectedStepper === index;

return (
<li key={index} className={wrapper}>
<button
className={isSelected ? stepper['active'] : stepper['default']}
type="button"
value={index}
onClick={handleStepperClick}
/>
</li>
);
})}
</ul>
);
};

export default Stepper;
22 changes: 22 additions & 0 deletions src/components/Common/Stepper/stepper.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { vars } from '@/styles/theme.css';
import { style, styleVariants } from '@vanilla-extract/css';

export const container = style({
display: 'flex',
gap: 10,
});

export const wrapper = style({
width: '33%',
});

export const stepperBase = style({
width: '100%',
height: 4,
cursor: 'pointer',
});

export const stepper = styleVariants({
active: [stepperBase, { background: vars.colors.primary }],
default: [stepperBase, { background: vars.colors.gray2 }],
});
2 changes: 2 additions & 0 deletions src/components/Common/Text/text.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const text = recipe({
info: { color: '#808080' },
disabled: { color: '#999999' },
white: { color: '#FFFFFF' },
yellow: { color: '#FFB017' },
},
size: {
caption4: { fontSize: '1.1rem' },
Expand All @@ -28,6 +29,7 @@ export const text = recipe({
regular: { fontWeight: 400 },
medium: { fontWeight: 500 },
semiBold: { fontWeight: 600 },
bold: { fontWeight: 700 },
},
},
defaultVariants: {
Expand Down
1 change: 1 addition & 0 deletions src/components/Common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export { default as TopBar } from './TopBar/TopBar';
export { default as StarRating } from './StarRating/StarRating';
export { default as ShowAllButton } from './ShowAllButton/ShowAllButton';
export { default as FormTextarea } from './FormTextarea/FormTextarea';
export { default as Stepper } from './Stepper/Stepper';
1 change: 1 addition & 0 deletions src/constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const PATH = {
RECIPE: '/recipes',
REVIEW: '/reviews',
LOGIN: '/login',
ONBOARDING: '/onboarding',
} as const;
31 changes: 31 additions & 0 deletions src/pages/OnboardingPage/OnboardingContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { contentWrapper, titleWrapper, descriptionText } from './onboardingPage.css';

import { Text } from '@/components/Common';

interface OnboardingContentProps {
title: string;
description: string;
image: string;
}

const OnboardingContent = ({ title, description, image }: OnboardingContentProps) => {
return (
<div className={contentWrapper}>
<div className={titleWrapper}>
<Text size="caption3" weight="bold" color="yellow">
{title}
</Text>
</div>
<div style={{ height: 17 }} />

<Text size="display1" weight="semiBold" color="sub" className={descriptionText}>
{description}
</Text>
<div style={{ height: 28 }} />

<img width={244} height={'100%'} src={image} alt="온보딩 예시" />
</div>
);
};

export default OnboardingContent;
57 changes: 57 additions & 0 deletions src/pages/OnboardingPage/OnboardingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useNavigate } from 'react-router-dom';

import OnboardingContent from './OnboardingContent';
import { container, link } from './onboardingPage.css';

import OnboardingMember from '@/assets/onboarding-member.png';
import OnboardingRecipe from '@/assets/onboarding-recipe.png';
import OnboardingReview from '@/assets/onboarding-review.png';
import { Stepper, Text } from '@/components/Common';
import { PATH } from '@/constants/path';
import { useTabMenu } from '@/hooks/common';
import { setLocalStorage } from '@/utils/localStorage';

const ONBOARDING_STEPS = [
{
title: '상품 조합',
description: '편의점 음식들을 이용한 \n다양한 조합을 확인할 수 있어요',
image: OnboardingRecipe,
},
{
title: '상품 리뷰',
description: '태그, 이미지, 별점을 통해 \n정확한 리뷰를 확인할 수 있어요',
image: OnboardingReview,
},
{ title: '마이페이지', description: '마이페이지에서 \n나만의 조합과 리뷰를 관리해요', image: OnboardingMember },
];

export const OnboardingPage = () => {
const { selectedTabMenu, handleTabMenuClick } = useTabMenu('0');
const selectedStepper = parseInt(selectedTabMenu);
const content = ONBOARDING_STEPS[selectedStepper];

const navigate = useNavigate();

const handleCompleteOnboarding = () => {
setLocalStorage('isRevisit', true);
navigate(PATH.HOME);
};

return (
<>
<section className={container}>
<Stepper selectedStepper={selectedStepper} handleStepperSelect={handleTabMenuClick} />
<div style={{ height: 50 }} />

<OnboardingContent title={content.title} description={content.description} image={content.image} />
</section>
<button className={link} onClick={handleCompleteOnboarding}>
<Text size="body" weight="bold" color="white">
시작하기
</Text>
</button>
</>
);
};

export default OnboardingPage;
36 changes: 36 additions & 0 deletions src/pages/OnboardingPage/onboardingPage.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { vars } from '@/styles/theme.css';
import { style } from '@vanilla-extract/css';

export const container = style({
height: '100%',
padding: '0 20px',
});

export const contentWrapper = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
});

export const titleWrapper = style({
width: 'fit-content',
padding: '6px 16px',
borderRadius: 40,
background: vars.colors.black,
});

export const link = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: 48,
padding: 16,
backgroundColor: vars.colors.primary,
});

export const descriptionText = style({
whiteSpace: 'pre-wrap',
textAlign: 'center',
});
7 changes: 5 additions & 2 deletions src/pages/ProductPage/ProductPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Spacing } from '@fun-eat/design-system';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { Suspense } from 'react';
import { useLocation } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';

import { categorySection, productSection } from './productPage.css';

Expand All @@ -17,6 +17,7 @@ import {
import { ProductPreviewList } from '@/components/Product';
import { CATEGORY_TYPE } from '@/constants';
import { PRODUCT_BANNER, STORE_BANNER } from '@/constants/image';
import { PATH } from '@/constants/path';
import { useTabMenu } from '@/hooks/common';
import { useCategoryQuery } from '@/hooks/queries/product';
import { vars } from '@/styles/theme.css';
Expand Down Expand Up @@ -55,7 +56,9 @@ export const ProductPage = () => {
</section>

{selectedTabMenu === TAB_MENUS[0].value ? (
<img src={PRODUCT_BANNER} width={'100%'} height={72} alt="상품 배너" />
<Link to={PATH.ONBOARDING}>
<img src={PRODUCT_BANNER} width={'100%'} height={72} alt="상품 배너" />
</Link>
) : (
<div style={{ height: '12px', backgroundColor: vars.colors.border.light }} aria-hidden />
)}
Expand Down
15 changes: 13 additions & 2 deletions src/router/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { Suspense, useEffect } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';

import { ErrorBoundary, ErrorComponent, Loading } from '@/components/Common';
import { Layout } from '@/components/Layout';
import { PATH } from '@/constants/path';
import { useRouteChangeTracker } from '@/hooks/common';
import { getLocalStorage } from '@/utils/localStorage';

interface AppProps {
hasLayout?: boolean;
}

const App = ({ hasLayout = false }: AppProps) => {
const { reset } = useQueryErrorResetBoundary();
const navigate = useNavigate();

useRouteChangeTracker();

useEffect(() => {
const isRevisit = getLocalStorage('isRevisit');

if (!isRevisit) {
navigate(PATH.ONBOARDING, { replace: true });
}
}, [navigate]);

if (!hasLayout) {
return (
<ErrorBoundary fallback={ErrorComponent} handleReset={reset}>
Expand Down
17 changes: 17 additions & 0 deletions src/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,23 @@ const router = createBrowserRouter([
},
],
},
// 온보딩 페이지
{
path: '/',
element: <App />,
errorElement: <NotFoundPage />,
children: [
{
path: `${PATH.ONBOARDING}`,
async lazy() {
const { OnboardingPage } = await import(
/* webpackChunkName: "OnboardingPage" */ '@/pages/OnboardingPage/OnboardingPage'
);
return { Component: OnboardingPage };
},
},
],
},
]);

export default router;

0 comments on commit 914bbb2

Please sign in to comment.