Skip to content
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

ExpensePage 모바일 대응 #366

Merged
merged 3 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Outlet } from 'react-router-dom';

import { useMediaQuery } from '@hooks/common/useMediaQuery';
import { useResetError } from '@hooks/common/useResetError';

import Error from '@components/common/Error/Error';
Expand All @@ -10,6 +11,7 @@ import Header from '@components/layout/Header/Header';

const App = () => {
const { handleErrorReset } = useResetError();
useMediaQuery();

return (
<ErrorBoundary Fallback={Error} onReset={handleErrorReset}>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/assets/svg/error-image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion frontend/src/components/common/Error/Error.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ErrorImage from '@assets/svg/error-image.svg';
import { HTTP_ERROR_MESSAGE } from '@constants/api';
import { mediaQueryMobileState } from '@store/mediaQuery';
import { Box, Button, Flex, Heading, Text } from 'hang-log-design-system';
import { useRecoilValue } from 'recoil';

import { hasKeyInObject } from '@utils/typeGuard';

Expand All @@ -18,13 +20,14 @@ export interface ErrorProps {

const Error = ({ statusCode = 404, resetError }: ErrorProps) => {
const isHTTPError = hasKeyInObject(HTTP_ERROR_MESSAGE, statusCode);
const isMobile = useRecoilValue(mediaQueryMobileState);

if (!isHTTPError) return null;

return (
<Box>
<Flex styles={{ direction: 'column', align: 'center' }} css={containerStyling}>
<ErrorImage aria-label="에러 이미지" />
<ErrorImage width={isMobile ? '80%' : '476px'} aria-label="에러 이미지" />
<Heading css={headingStyling} size="small">
{HTTP_ERROR_MESSAGE[statusCode].HEADING}
</Heading>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { Theme } from 'hang-log-design-system';
export const sectionStyling = css({
position: 'relative',
width: '100%',
padding: `${Theme.spacer.spacing4} 0`,
marginBottom: Theme.spacer.spacing5,
padding: `${Theme.spacer.spacing4} 0`,

'@media screen and (max-width: 600px)': {
marginBottom: Theme.spacer.spacing3,
},
});

export const titleStyling = css({
Expand All @@ -15,4 +19,23 @@ export const titleStyling = css({

export const badgeWrapperStyling = css({
width: '60%',
minHeight: '24px',
marginBottom: Theme.spacer.spacing2,

overflowX: 'scroll',
whiteSpace: 'nowrap',
'-ms-overflow-style': 'none',
scrollbarWidth: 'none',

'& > span': {
marginRight: Theme.spacer.spacing1,
},

'::-webkit-scrollbar': {
display: 'none',
},

'@media screen and (max-width: 600px)': {
width: 'calc(100vw - 220px)',
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { mediaQueryMobileState } from '@store/mediaQuery';
import type { ExpenseData } from '@type/expense';
import { Badge, Flex, Heading, Text, Theme } from 'hang-log-design-system';
import { Badge, Box, Heading, Text } from 'hang-log-design-system';
import { useRecoilValue } from 'recoil';

import { formatDate } from '@utils/formatter';

Expand All @@ -15,14 +17,16 @@ interface ExpenseInformationProps
}

const ExpenseInformation = ({ ...information }: ExpenseInformationProps) => {
const isMobile = useRecoilValue(mediaQueryMobileState);

return (
<header css={sectionStyling}>
<Flex styles={{ gap: Theme.spacer.spacing1, wrap: 'wrap' }} css={badgeWrapperStyling}>
<Box css={badgeWrapperStyling}>
{information.cities.map(({ id, name }) => (
<Badge key={id}>{name}</Badge>
))}
</Flex>
<Heading css={titleStyling} size="large">
</Box>
<Heading css={titleStyling} size={isMobile ? 'medium' : 'large'}>
{information.title}
</Heading>
<Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export const containerStyling = css({
width: '50vw',
marginTop: '140px',
padding: '0 50px',

'@media screen and (max-width: 600px)': {
marginTop: Theme.spacer.spacing0,

width: '100vw',
padding: `0 ${Theme.spacer.spacing4}`,
},
});

export const toggleGroupStyling = css({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { EXPENSE_LIST_FILTERS } from '@constants/expense';
import { Toggle, ToggleGroup, useSelect } from 'hang-log-design-system';
import { mediaQueryMobileState } from '@store/mediaQuery';
import { Flex, Heading, Toggle, ToggleGroup, useSelect } from 'hang-log-design-system';
import { useRecoilValue } from 'recoil';

import ExpenseCategories from '@components/expense/ExpenseCategories/ExpenseCategories';
import ExpenseDates from '@components/expense/ExpenseDates/ExpenseDates';
Expand All @@ -13,28 +15,33 @@ interface ExpenseListProps {
}

const ExpenseListSection = ({ tripId }: ExpenseListProps) => {
const isMobile = useRecoilValue(mediaQueryMobileState);

const { selected: selectedFilter, handleSelectClick: handleFilterSelectClick } = useSelect(
EXPENSE_LIST_FILTERS.DAY_LOG
);

return (
<section css={containerStyling}>
<ToggleGroup css={toggleGroupStyling}>
<Toggle
text={EXPENSE_LIST_FILTERS.DAY_LOG}
toggleId={EXPENSE_LIST_FILTERS.DAY_LOG}
selectedId={selectedFilter}
changeSelect={handleFilterSelectClick}
aria-label="날짜 필터"
/>
<Toggle
text={EXPENSE_LIST_FILTERS.CATEGORY}
toggleId={EXPENSE_LIST_FILTERS.CATEGORY}
selectedId={selectedFilter}
changeSelect={handleFilterSelectClick}
aria-label="카테고리 필터"
/>
</ToggleGroup>
<Flex styles={{ justify: isMobile ? 'space-between' : 'flex-end' }}>
{isMobile && <Heading size="xSmall">경비 상세 정보</Heading>}
<ToggleGroup css={toggleGroupStyling}>
<Toggle
text={EXPENSE_LIST_FILTERS.DAY_LOG}
toggleId={EXPENSE_LIST_FILTERS.DAY_LOG}
selectedId={selectedFilter}
changeSelect={handleFilterSelectClick}
aria-label="날짜 필터"
/>
<Toggle
text={EXPENSE_LIST_FILTERS.CATEGORY}
toggleId={EXPENSE_LIST_FILTERS.CATEGORY}
selectedId={selectedFilter}
changeSelect={handleFilterSelectClick}
aria-label="카테고리 필터"
/>
</ToggleGroup>
</Flex>
{selectedFilter === EXPENSE_LIST_FILTERS.DAY_LOG ? (
<ExpenseDates tripId={tripId} />
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { mediaQueryMobileState } from '@store/mediaQuery';
import { Flex, Skeleton, Theme } from 'hang-log-design-system';
import { useRecoilValue } from 'recoil';

import ExpenseListSkeleton from '@components/expense/ExpenseList/ExpenseListSkeleton';
import {
Expand All @@ -7,9 +9,14 @@ import {
} from '@components/expense/ExpenseListSection/ExpenseListSection.style';

const ExpenseListSectionSkeleton = () => {
const isMobile = useRecoilValue(mediaQueryMobileState);

return (
<section css={containerStyling}>
<Skeleton width="125px" height="38px" css={toggleGroupStyling} />
<Flex styles={{ justify: isMobile ? 'space-between' : 'flex-end' }}>
{isMobile && <Skeleton width="150px" height="38px" />}
<Skeleton width="125px" height="38px" css={toggleGroupStyling} />
</Flex>
<Skeleton width="100%" height="48px" />
<ExpenseListSkeleton />
<Flex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const containerStyling = css({

width: '50vw',
padding: '0 50px',

'@media screen and (max-width: 600px)': {
width: '100vw',
padding: `0 ${Theme.spacer.spacing4}`,
},
});

export const totalAmountStyling = css({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { EXPENSE_CHART_COLORS } from '@constants/expense';
import { CURRENCY_ICON, DEFAULT_CURRENCY } from '@constants/trip';
import { mediaQueryMobileState } from '@store/mediaQuery';
import { Box, Heading } from 'hang-log-design-system';
import { useRecoilValue } from 'recoil';

import { formatNumberToMoney } from '@utils/formatter';

Expand All @@ -20,6 +22,8 @@ interface TotalExpenseSectionProps {
}

const TotalExpenseSection = ({ tripId }: TotalExpenseSectionProps) => {
const isMobile = useRecoilValue(mediaQueryMobileState);

const { expenseData } = useExpense(tripId);

const chartData = expenseData.categories.reduce<Segment[]>((acc, curr) => {
Expand All @@ -45,7 +49,7 @@ const TotalExpenseSection = ({ tripId }: TotalExpenseSectionProps) => {
endDate={expenseData.endDate}
cities={expenseData.cities}
/>
<Heading size="small" css={totalAmountStyling}>
<Heading size={isMobile ? 'xSmall' : 'small'} css={totalAmountStyling}>
총 경비 :{' '}
<span>
{CURRENCY_ICON[DEFAULT_CURRENCY]}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/layout/Footer/Footer.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const containerStyling = css({

backgroundColor: Theme.color.white,

'@media screen and (max-width: 600px)': {
height: '124px',
},

'& *': {
color: Theme.color.gray600,
},
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/layout/Header/Header.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const headerStyling = css({
backgroundColor: Theme.color.white,
padding: `${Theme.spacer.spacing4} 50px`,

'@media screen and (max-width: 600px)': {
padding: `${Theme.spacer.spacing3} ${Theme.spacer.spacing4}`,
},

'& > *': {
cursor: 'pointer',
},
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/constants/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const TRIP_ITEM_ADD_MAX_IMAGE_UPLOAD_COUNT = 5;
export const EXPENSE_CATEGORY_INFORMATION_SKELETON_LENGTH = 6;

export const EXPENSE_LIST_SKELETON_LENGTH = 5;

export const MOBILE_MEDIA_QUERY_SIZE = '(max-width: 600px)';
37 changes: 37 additions & 0 deletions frontend/src/hooks/common/useMediaQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { MOBILE_MEDIA_QUERY_SIZE } from '@constants/ui';
import { mediaQueryMobileState, viewportWidthState } from '@store/mediaQuery';
import { useCallback, useEffect, useRef } from 'react';
import { useSetRecoilState } from 'recoil';

export const useMediaQuery = () => {
const setViewportWidth = useSetRecoilState(viewportWidthState);
const setIsMobile = useSetRecoilState(mediaQueryMobileState);
const mediaQueryRef = useRef<MediaQueryList | null>(null);

const handleWindowResize = useCallback(() => {
setIsMobile(window.matchMedia(MOBILE_MEDIA_QUERY_SIZE).matches);
setViewportWidth(window.innerWidth);
}, [setIsMobile, setViewportWidth]);

const handleViewportWidthChange = useCallback(() => {
setViewportWidth(window.innerWidth);
}, [setViewportWidth]);

useEffect(() => {
setIsMobile(window.matchMedia(MOBILE_MEDIA_QUERY_SIZE).matches);
setViewportWidth(window.innerWidth);
}, [setIsMobile, setViewportWidth]);

useEffect(() => {
const mediaQueryList = window.matchMedia(MOBILE_MEDIA_QUERY_SIZE);
mediaQueryRef.current = mediaQueryList;

mediaQueryRef.current.addEventListener('change', handleWindowResize);
window.addEventListener('resize', handleViewportWidthChange);

return () => {
mediaQueryRef.current?.removeEventListener('change', handleWindowResize);
window.removeEventListener('resize', handleViewportWidthChange);
};
}, [handleWindowResize, handleViewportWidthChange]);
};
10 changes: 10 additions & 0 deletions frontend/src/pages/ExpensePage/ExpensePage.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ export const containerStyling = css({
minHeight: `calc(100vh - 81px - ${Theme.spacer.spacing6})`,

marginBottom: Theme.spacer.spacing6,

'@media screen and (max-width: 600px)': {
flexDirection: 'column',
gap: Theme.spacer.spacing5,
},
});

export const dividerStyling = css({
width: `calc(100vw - ${Theme.spacer.spacing6})`,
margin: `0 ${Theme.spacer.spacing4}`,
});
10 changes: 8 additions & 2 deletions frontend/src/pages/ExpensePage/ExpensePage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Flex } from 'hang-log-design-system';
import { mediaQueryMobileState } from '@store/mediaQuery';
import { Divider, Flex } from 'hang-log-design-system';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';

import { useExpenseQuery } from '@hooks/api/useExpenseQuery';

import { containerStyling } from '@pages/ExpensePage/ExpensePage.style';
import { containerStyling, dividerStyling } from '@pages/ExpensePage/ExpensePage.style';

import ExpenseListSection from '@components/expense/ExpenseListSection/ExpenseListSection';
import TotalExpenseSection from '@components/expense/TotalExpenseSection/TotalExpenseSection';
Expand All @@ -12,11 +14,15 @@ const ExpensePage = () => {
const { tripId } = useParams();

if (!tripId) throw new Error('존재하지 않는 tripId 입니다.');

const isMobile = useRecoilValue(mediaQueryMobileState);

const { expenseData } = useExpenseQuery(Number(tripId));

return (
<Flex css={containerStyling}>
<TotalExpenseSection tripId={expenseData.id} />
{isMobile && <Divider css={dividerStyling} />}
<ExpenseListSection tripId={expenseData.id} />
</Flex>
);
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/pages/ExpensePage/ExpensePageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { Flex } from 'hang-log-design-system';
import { mediaQueryMobileState } from '@store/mediaQuery';
import { Divider, Flex } from 'hang-log-design-system';
import { useRecoilValue } from 'recoil';

import { containerStyling } from '@pages/ExpensePage/ExpensePage.style';
import { containerStyling, dividerStyling } from '@pages/ExpensePage/ExpensePage.style';

import ExpenseListSectionSkeleton from '@components/expense/ExpenseListSection/ExpenseListSectionSkeleton';
import TotalExpenseSectionSkeleton from '@components/expense/TotalExpenseSection/TotalExpenseSectionSkeleton';

const ExpensePageSkeleton = () => {
const isMobile = useRecoilValue(mediaQueryMobileState);

return (
<Flex css={containerStyling}>
<TotalExpenseSectionSkeleton />
{isMobile && <Divider css={dividerStyling} />}
<ExpenseListSectionSkeleton />
</Flex>
);
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/NotFoundPage/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { PATH } from '@constants/path';
import { useNavigate } from 'react-router-dom';

import { useMediaQuery } from '@hooks/common/useMediaQuery';

import Error from '@components/common/Error/Error';

const NotFoundPage = () => {
const navigate = useNavigate();

useMediaQuery();

return <Error resetError={() => navigate(PATH.ROOT)} />;
};

Expand Down
Loading