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

[허우림] week12 #365

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
28 changes: 27 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
{
"extends": "next/core-web-vitals"
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"],
"parserOptions": {
"project": "./tsconfig.json",
"createDefaultProgram": true
},
"env": {
"browser": true,
"node": true,
"es6": true
},
"ignorePatterns": ["node_modules/"],
"extends": [
"airbnb",
"airbnb-typescript",
"airbnb/hooks",
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": ["warn", { "extensions": [".ts", ".tsx"] }],
"no-useless-catch": "off"
}
}
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}
114 changes: 114 additions & 0 deletions components/common/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useContext, useEffect, useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { calCreatedAt, calCreatedDates } from '../../utils/date';
import { KebabContextProvider } from '../../contexts/KebabContext';
import CardContext from '../../contexts/CardContext';
import NoImg from '@/public/assets/icons/card/card_no-img.svg';
import kebab from '@/public/assets/icons/card/kebab.svg';
import CardInfo from './CardInfo';
import styles from '@/styles/card/card.module.css';
interface Props {
id: string;
createdAt: string;
url: string;
title: string;
description: string;
imageSource: string;
}

export default function Card({ link }: { link: Props }) {
const { setLinkInfo } = useContext(CardContext);
const { id, createdAt, url, title, description, imageSource } = link;
const [mins, setMins] = useState('');
const [createdDates, setCreatedDates] = useState({
Copy link
Collaborator

Choose a reason for hiding this comment

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

year, month, day는 createdAt으로부터 파생된 값이네요.
그렇다면 별도의 state 없이 아래와 같이 단순히 함수 호출로 처리할 수 있습니다.

const [year, month, day] = calCreatedDates(createdAt);

year: '',
month: '',
day: '',
});
const [isHovered, setIsHovered] = useState(false);

const handleSetLinkInfo = () => {
setLinkInfo({
id: id,
createdAt: createdAt,
url: url,
title: title,
description: description,
imageSource: imageSource,
});
};

const getCreatedDates = () => {
const [year, month, day] = calCreatedDates(createdAt);
setCreatedDates((prev) => ({
...prev,
year: year,
month: month,
day: day,
}));
};

const getCreatedAt = () => {
setMins(calCreatedAt(createdDates));
};

useEffect(() => {
handleSetLinkInfo();
}, []);

useEffect(() => {
getCreatedDates();
}, [createdAt]);

useEffect(() => {
getCreatedAt();
}, [createdDates]);

return (
<>
<KebabContextProvider>
<div className={styles.flexWrapper} id={`card-${id}`}>
<div className={styles.cardImgWrapper}>
<Link target="_blank" href={url}>
{imageSource ? (
<Image
className={
isHovered
? `${styles.cardImg} ${styles.grow}`
: styles.cardImg
}
width={500}
height={253}
src={imageSource}
alt={`${title}-img`}
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
/>
) : (
<Image
className={styles.cardImg}
width={500}
height={253}
src={NoImg}
alt={`${title}-img`}
/>
)}
</Link>
</div>
<CardInfo
mins={mins}
imgSrc={kebab}
title={title}
description={description}
createdDates={createdDates}
/>
</div>
</KebabContextProvider>
</>
);
}
59 changes: 59 additions & 0 deletions components/common/CardInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Image from 'next/image';
import { useContext, useReducer } from 'react';
import Dropdown from './folderPage/Dropdown';
import KebabContext from '../../contexts/KebabContext';
import styles from '@/styles/card/card.module.css';
import { useRouter } from 'next/router';

interface Props {
mins: string;
imgSrc: string;
title: string;
description: string;
createdDates: {
year: string;
month: string;
day: string;
};
}

export default function CardInfo({
mins,
imgSrc,
title,
description,
createdDates,
}: Props) {
const { setIsKebabClicked, isKebabClicked } = useContext(KebabContext);
Copy link
Collaborator

Choose a reason for hiding this comment

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

각 케밥 버튼의 상태는 CardInfo 컴포넌트 내부에서만 관리되고 있네요.
전역적으로 공유가 필요한 성격의 데이터도 아니고, 컴포넌트 내부 상태로 관리하는 편이 데이터 흐름을 더 쉽게 파악할 수 있을 것 같습니다.

예시:

const [isKebabClicked, setIsKebabClicked] = useContext(false);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

그러네요! 한 컴포넌트 안에서만 쓰는 state인데 context를 사용할 필요가 없었네요!
그런데 멘토님 예시코드에서 useContext가 아니라 useState 사용하라고 말씀하시는 거 맞을까요?

const router = useRouter();

const handleKebabClick = () => {
if (router.pathname !== '/') {
setIsKebabClicked((prev: boolean) => !prev);
}
};

return (
<div className={styles.cardInfoContainer}>
<div className={styles.timesAgoWrapper}>
<div className={styles.mins}>{mins}</div>
<Image
width={21}
height={17}
src={imgSrc}
alt="kebab"
onClick={handleKebabClick}
style={router.pathname !== '/' ? { cursor: 'pointer' } : undefined}
/>
{isKebabClicked && <Dropdown />}
</div>
<div>
<div className={styles.title}>{title}</div>
<div className={styles.description}>{description}</div>
</div>
<div className={styles.created}>
{createdDates.year}. {createdDates.month}. {createdDates.day}
</div>
</div>
);
}
30 changes: 30 additions & 0 deletions components/common/CardWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CardContextProvider } from '../../contexts/CardContext';
import Card from './Card';
import styles from '@/styles/card/cardWrapper.module.css';

interface Props {
links: {
id: string;
createdAt: string;
url: string;
title: string;
imageSource: string;
description: string;
}[];
}

export default function CardWrapper({ links }: Props) {
return (
<div className={styles.wrapper}>
{links.map((link) => {
return (
<div key={link.id}>
<CardContextProvider>
<Card link={link} />
</CardContextProvider>
</div>
);
})}
</div>
);
}
37 changes: 37 additions & 0 deletions components/common/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Sns from './Sns';
import FooterSnsDatas from './FooterSnsDatas';
import styles from '@/styles/footer/footer.module.css';
import Link from 'next/link';

export default function Footer() {
const getThisYear = () => {
const date = new Date();
const thisYear = date.getFullYear();
return thisYear;
};

return (
<>
<div className={styles.wrapper}>
<span className={styles.codeit}>©codeit - {getThisYear()}</span>
<div className={styles.info}>
<span className={styles.privacy}>
<Link href="/privacy">Privacy Policy</Link>
</span>
<span className={styles.faq}>
<Link href="/faq">FAQ</Link>
</span>
</div>
<div className={styles.sns}>
{FooterSnsDatas.map((data) => {
return (
<div key={`sns-${data.id}`}>
<Sns footerSnsData={data} />
</div>
);
})}
</div>
</div>
</>
);
}
23 changes: 23 additions & 0 deletions components/common/FooterSnsDatas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import facebook from '/public/assets/icons/footer/facebook.png';
import twitter from '/public/assets/icons/footer/twitter.png';
import instagram from '/public/assets/icons/footer/instagram.png';
import youtube from '/public/assets/icons/footer/youtube.png';

export const FooterSnsDatas = [
{ id: 1, name: 'facebook', url: 'https://www.facebook.com', img: facebook },
{ id: 2, name: 'twitter', url: 'https://www.twitter.com', img: twitter },
{
id: 3,
name: 'youtube',
url: 'https://www.youtube.com',
img: youtube,
},
{
id: 4,
name: 'instagram',
url: 'https://www.instagram.com',
img: instagram,
},
];

export default FooterSnsDatas;
45 changes: 45 additions & 0 deletions components/common/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Image from 'next/image';
import LoginButton from './LoginButton';
import Logo from './Logo';
import styles from '@/styles/header/header.module.css';

interface Props {
profileDatas: {
id: number;
created_at?: string;
name: string;
image_source: string;
email: string;
auth_id?: string;
};
}

export default function Header({ profileDatas }: Props) {
const {
name = 'defaultName',
image_source = 'https://images.unsplash.com/photo-1701600713610-0f724c65168d?q=80&w=1074&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
email = '[email protected]',
} = profileDatas;

return (
<div className={styles.header}>
<div className={styles.nav}>
<Logo />
{name ? (
<div className={styles.profile}>
<Image
className={styles.profileImg}
src={image_source}
width={28}
height={28}
alt={name}
/>
<p className={styles.profileName}>{email}</p>
</div>
) : (
<LoginButton />
)}
</div>
</div>
);
}
14 changes: 14 additions & 0 deletions components/common/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styles from '@/styles/header/header.module.css';
import Link from 'next/link';

export default function LoginButton() {
return (
<>
<Link href="/signin">
<button className={`${styles.loginBtn} ${styles.button}`}>
로그인
</button>
</Link>
</>
);
}
Loading