Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 35 additions & 0 deletions src/apis/feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import getQueryKey from '@/apis/getQueryKey';
import apiInstance from '@/apis/instance.api';
import { type FeedBaseType, type FeedItemType } from '@/apis/schema/feed';
import { useQuery, type UseQueryOptions } from '@tanstack/react-query';

type GetFeedMeResponse = Array<FeedItemType>;

type GetFeedByMemberIdResponse = Array<FeedBaseType>;

export const FEED_API = {
getFeedMe: async (): Promise<GetFeedMeResponse> => {
const { data } = await apiInstance.get('/feed/me');
return data;
},
getFeed: async (memberId: number): Promise<GetFeedByMemberIdResponse> => {
const { data } = await apiInstance.get(`/feed/${memberId}`);
return data;
},
};

export const useFeedMe = (options?: UseQueryOptions<GetFeedMeResponse>) => {
return useQuery<GetFeedMeResponse>({
...options,
queryKey: getQueryKey('feedMe'),
queryFn: FEED_API.getFeedMe,
});
};

export const useFeedByMemberId = (memberId: number, options?: UseQueryOptions<GetFeedByMemberIdResponse>) => {
return useQuery<GetFeedByMemberIdResponse>({
...options,
queryKey: getQueryKey('feed', { memberId }),
queryFn: () => FEED_API.getFeed(memberId),
});
};
5 changes: 5 additions & 0 deletions src/apis/getQueryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type QueryList = {
missionStack: {
missionId: string;
};

feedMe: undefined;
feed: {
memberId: number;
};
};

/**
Expand Down
25 changes: 15 additions & 10 deletions src/apis/schema/feed.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { type MissionType } from '@/apis/schema/mission';
import { type RecordType } from '@/apis/schema/record';
export interface FeedItemType extends FeedBaseType {
remark?: string;
nickname: string;
profileImage?: string;
memberId: number;
}

/**
* @description
* @param mission - 미션
* @param records - 미션 기록
*/
export interface FeedType {
mission: MissionType;
records: RecordType[];
export interface FeedBaseType {
missionId: number;
recordId: number;
name: string;
recordImageUrl: string;
duration: number;
sinceDay: number;
startedAt: string;
finishedAt: string;
}
155 changes: 155 additions & 0 deletions src/app/feed/FeedItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use client';

import Link from 'next/link';
import { type FeedItemType } from '@/apis/schema/feed';
import HistoryThumbnail from '@/app/record/[id]/detail/HistoryThumbnail';
import Thumbnail from '@/components/Thumbnail/Thumbnail';
import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog';
import { ROUTER } from '@/constants/router';
import { eventLogger } from '@/utils';
import { css } from '@styled-system/css';
import dayjs from 'dayjs';

function FeedItem({
sinceDay,
remark,
nickname,
memberId,
name,
profileImage,
recordImageUrl,
duration,
startedAt,
recordId,
}: FeedItemType) {
const handleClickFeedItem = () => {
eventLogger.logEvent(EVENT_LOG_CATEGORY.FEED, EVENT_LOG_NAME.FEED.CLICK_FEED);
};

const handleClickFollowProfile = () => {
eventLogger.logEvent(EVENT_LOG_CATEGORY.FEED, EVENT_LOG_NAME.FEED.CLICK_PROFILE);
};
return (
<li>
<Link href={ROUTER.PROFILE.DETAIL(memberId)} onClick={handleClickFollowProfile}>
<div className={profileWrapperCss}>
<Thumbnail size={'h24'} variant={'filled'} url={profileImage} />
<p>{nickname}</p>
</div>
</Link>
<Link href={ROUTER.RECORD.DETAIL.FOLLOW(recordId.toString())} onClick={handleClickFeedItem}>
<HistoryThumbnail imageUrl={recordImageUrl} missionDuration={duration} />
<div className={textWrapperCss}>
<p className={missionNameCss}>{name}</p>
{remark && <p className={remarkCss}>{remark}</p>}
<p className={captionCss}>
{sinceDay}일차 <div className={dotCss} /> {dayjs(startedAt).format('YYYY년 MM월 DD일')}
</p>
</div>
</Link>
</li>
);
}

export default FeedItem;

export const FeedSkeletonItem = () => {
return (
<li>
<div className={profileWrapperCss}>
<Thumbnail size={'h24'} variant={'filled'} url={null} />
<div
className={css(
{ ...skeletonTextCss },
{
width: '80px',
height: '20px',
},
)}
/>
</div>
<div className={profile} />
<div className={textWrapperCss}>
<div
className={css(
{ ...skeletonTextCss },
{
width: '80px',
height: '17px',
},
)}
/>
<div
className={css(
{ ...skeletonTextCss },
{
width: '130px',
height: '20px',
},
)}
/>
</div>
</li>
);
};

const profile = css({
animation: 'skeleton',
backgroundColor: 'bg.surface4',
width: '100%',
aspectRatio: '1 / 1',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이런게 있구나

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 로그인 화면도 요거랑 백그라운드 여백 색으로 여차저차 하면 더 자연스러워지지 않을까...

position: 'relative',
borderRadius: '22px',
overflow: 'hidden',
maxWidth: 'calc(475px - 32px)',
maxHeight: 'calc(475px - 32px)',
});

const skeletonTextCss = {
animation: 'skeleton',
backgroundColor: 'bg.surface4',
borderRadius: '12px',
};

const textWrapperCss = css({
display: 'flex',
gap: '8px',
flexDirection: 'column',

padding: '20px 4px',
});

const missionNameCss = css({
textStyle: 'body5',
color: 'gray.gray600',
});

const remarkCss = css({
textStyle: 'body2',
color: 'text.primary',
});

const captionCss = css({
textStyle: 'body3',
color: 'text.tertiary',
display: 'flex',
gap: '5px',
alignItems: 'center',
});

const dotCss = css({
width: '2px',
height: '2px',
borderRadius: '50%',
backgroundColor: 'icon.tertiary',
});

const profileWrapperCss = css({
display: 'flex',
alignItems: 'center',
padding: '16px 12px',
textStyle: 'body3',
color: 'text.primary',
gap: '8px',
cursor: 'pointer',
});
32 changes: 32 additions & 0 deletions src/app/feed/FeedList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';
import { useFeedMe } from '@/apis/feed';
import FeedItem, { FeedSkeletonItem } from '@/app/feed/FeedItem';
import { css } from '@styled-system/css';

function FeedList() {
const { data } = useFeedMe();
if (!data)
return (
<ul className={feedListCss}>
<FeedSkeletonItem />
<FeedSkeletonItem />
</ul>
);

return (
<ul className={feedListCss}>
{data.map((feed) => (
<FeedItem key={feed.recordId} {...feed} />
))}
</ul>
);
}

export default FeedList;

const feedListCss = css({
padding: '0 16px 132px 16px',
display: 'flex',
flexDirection: 'column',
gap: '32px',
});
15 changes: 15 additions & 0 deletions src/app/feed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import FeedList from '@/app/feed/FeedList';
import AppBar from '@/app/home/AppBar';
import AppBarBottom from '@/components/AppBarBottom/AppBarBottom';
import BottomDim from '@/components/BottomDim/BottomDim';

export default function FeedPage() {
return (
<>
<AppBar />
<FeedList />
<BottomDim />
<AppBarBottom />
</>
);
}
1 change: 1 addition & 0 deletions src/app/home/AppBar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import Icon from '@/components/Icon';
Expand Down
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import './globals.css';
export const metadata: Metadata = {
title: '10MM',
description: '10MM',
keywords: ['10mm', '10분만', '10분', '10MM', '10mm', '하루 10분', '10분 단위', '생환습관'],
openGraph: {
type: 'website',
url: 'https://www.10mm.today',
Expand Down
59 changes: 59 additions & 0 deletions src/app/mypage/FeedThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Image from 'next/image';
import MissionDuration from '@/app/record/[id]/detail/MissionDuration';
import { css } from '@styled-system/css';

function FeedThumbnail({ imageUrl, missionDuration }: { imageUrl: string; missionDuration: number }) {
return (
<div className={historyThumbnailWrapperCss}>
<div className={dimmedCss} />
<Image className={imageCss} width={365} height={365} src={imageUrl} alt={'피드 이미지'} />
<div className={positionCss}>
<MissionDuration duration={missionDuration} type={'profileFeed'} />
</div>
</div>
);
}

export default FeedThumbnail;

const dimmedCss = css({
position: 'absolute',
width: '100%',
height: '60px',
background:
'linear-gradient(0deg, rgba(27, 34, 51, 0.00) 0%, rgba(27, 34, 51, 0.01) 10%, rgba(27, 34, 51, 0.03) 19.79%, rgba(27, 34, 51, 0.07) 34.79%, rgba(27, 34, 51, 0.13) 56.25%, rgba(27, 34, 51, 0.20) 77.92%, rgba(27, 34, 51, 0.30) 100%)',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
});

const historyThumbnailWrapperCss = css({
width: '100%',
aspectRatio: '1 / 1',
position: 'relative',
borderRadius: '16px',
overflow: 'hidden',
maxWidth: 'calc(475px - 32px)',
maxHeight: 'calc(475px - 32px)',

'@media (max-width: 475px)': {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

maxWidth: 'calc(100vw - 32px)',
maxHeight: 'calc(100vw - 32px)',
},
});

const positionCss = css({
position: 'absolute',
top: '9px',
left: '8px',
zIndex: 2,
});

const imageCss = css({
width: '100%',
borderRadius: '22px',
objectFit: 'cover',
height: '100%',
});
Loading