-
Notifications
You must be signed in to change notification settings - Fork 57
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
The head ref may contain hidden characters: "part3-\uD5C8\uC6B0\uB9BC-week12"
[허우림] week12 #365
Changes from all commits
2ceb077
1e4c556
5afbc86
b473629
dfc568d
298f569
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"semi": true, | ||
"singleQuote": true, | ||
"tabWidth": 2, | ||
"trailingComma": "all" | ||
} |
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({ | ||
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> | ||
</> | ||
); | ||
} |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 각 케밥 버튼의 상태는 CardInfo 컴포넌트 내부에서만 관리되고 있네요. 예시: const [isKebabClicked, setIsKebabClicked] = useContext(false); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그러네요! 한 컴포넌트 안에서만 쓰는 |
||
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> | ||
); | ||
} |
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> | ||
); | ||
} |
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> | ||
</> | ||
); | ||
} |
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; |
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> | ||
); | ||
} |
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> | ||
</> | ||
); | ||
} |
There was a problem hiding this comment.
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 없이 아래와 같이 단순히 함수 호출로 처리할 수 있습니다.