-
Notifications
You must be signed in to change notification settings - Fork 44
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
[박상준] Week13 #419
The head ref may contain hidden characters: "part3-\uBC15\uC0C1\uC900-week13"
[박상준] Week13 #419
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"presets": ["next/babel"], | ||
"plugins": [["styled-components", { "ssr": true }]] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import React, { ReactElement, useEffect, useState } from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
|
||
const ModalPortal = ({ children }: { children: ReactElement }) => { | ||
const [mounted, setMounted] = useState<boolean>(false); | ||
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null); | ||
|
||
useEffect(() => { | ||
setPortalElement(document.getElementById('modal')); | ||
}, []); | ||
|
||
useEffect(() => { | ||
setMounted(true); | ||
return () => setMounted(false); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
{mounted && portalElement ? createPortal(children, portalElement) : null} | ||
</> | ||
); | ||
}; | ||
|
||
export default ModalPortal; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import styled from 'styled-components'; | ||
import { ButtonProps } from './Button'; | ||
|
||
const buttonSize = { | ||
xs: '4.8', | ||
sm: '10', | ||
md: '28', | ||
lg: '40', | ||
}; | ||
|
||
export const Cta = styled.span<ButtonProps>` | ||
cursor: pointer; | ||
text-decoration: none; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
border-radius: 0.8rem; | ||
background: var(--Gradient-purpleblue-to-skyblue); | ||
color: var(--Gray-cta); | ||
padding: 1.6rem 2rem; | ||
font-family: Pretendard; | ||
font-size: 1.6rem; | ||
font-style: normal; | ||
font-weight: 700; | ||
line-height: normal; | ||
width: ${({ size }) => buttonSize[size]}rem; | ||
position: relative; | ||
|
||
&:hover { | ||
opacity: 0.8; | ||
} | ||
|
||
@media (max-width: 768px) { | ||
font-size: 1.4rem; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { ButtonHTMLAttributes } from 'react'; | ||
import * as S from './Button.styled'; | ||
|
||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { | ||
size: 'xs' | 'sm' | 'md' | 'lg'; | ||
} | ||
|
||
export function Button({ children, size }: ButtonProps) { | ||
return ( | ||
<> | ||
<S.Cta size={size}>{children}</S.Cta> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import styled from 'styled-components'; | ||
|
||
export const EmptyImg = styled.div` | ||
height: 100%; | ||
background-color: var(--EmptyArea); | ||
border-radius: 1.5rem 1.5rem 0 0; | ||
display: flex; | ||
justify-content: center; | ||
flex-direction: column; | ||
align-items: center; | ||
|
||
img { | ||
opacity: 0.2; | ||
width: 13.3rem; | ||
height: 2.4rem; | ||
} | ||
`; | ||
|
||
export const ItemImg = styled.div<{ image: string }>` | ||
height: 100%; | ||
background-image: url(${(props) => props.image}); | ||
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. 이 속성으로 이미지를 설정하게 되면 접근성 면에서 좋지 않습니다. |
||
border-radius: 1.5rem 1.5rem 0 0; | ||
background-size: cover; | ||
background-position: center; | ||
|
||
&:hover { | ||
background-size: 130%; | ||
} | ||
`; | ||
|
||
export const ItemCard = styled.div` | ||
width: 34rem; | ||
height: 33.4rem; | ||
display: flex; | ||
flex-direction: column; | ||
box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.08); | ||
border-radius: 1.5rem; | ||
text-decoration: none; | ||
color: #000; | ||
position: relative; | ||
font-size: 1.6rem; | ||
|
||
&:hover { | ||
background-color: var(--Background); | ||
} | ||
|
||
@media (max-width: 768px) { | ||
font-size: 1.4rem; | ||
} | ||
`; | ||
|
||
export const StarIcon = styled.img` | ||
width: 3.4rem; | ||
height: 3rem; | ||
flex-shrink: 0; | ||
position: absolute; | ||
top: 1.5rem; | ||
right: 1.5rem; | ||
z-index: 10; | ||
`; | ||
|
||
export const ItemInfo = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
padding: 1.5rem 2rem; | ||
width: 100%; | ||
height: 13.5rem; | ||
gap: 1rem; | ||
position: relative; | ||
`; | ||
|
||
export const KebabIcon = styled.img` | ||
cursor: pointer; | ||
width: 2.1rem; | ||
height: 1.7rem; | ||
flex-shrink: 0; | ||
position: absolute; | ||
right: 2rem; | ||
top: 1.5rem; | ||
`; | ||
|
||
export const ItemDate = styled.p` | ||
color: var(--Description); | ||
font-size: 1.3rem; | ||
`; | ||
|
||
export const ItemDescription = styled.p` | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
|
||
a { | ||
text-decoration: none; | ||
color: #000; | ||
} | ||
`; | ||
|
||
export const ItemFullDate = styled.p` | ||
font-size: 1.4rem; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { useEffect, useRef, useState } from 'react'; | ||
import { changeDate, calculateDate } from '../../util/util'; | ||
import * as S from './Card.styled'; | ||
import KebabMenu from '../KebabMenu/KebabMenu'; | ||
import { Link } from '../../hooks/useGetFolder'; | ||
import Image from 'next/image'; | ||
|
||
function Card({ item }: { item: Link }) { | ||
const [createdAt, setCreatedAt] = useState({ time: 0, result: '' }); | ||
const [fullDate, setFullDate] = useState(''); | ||
const { image_source } = item; | ||
const [kebabView, setKebaView] = useState(false); | ||
const [like, setLike] = useState(false); | ||
const kebabRef = useRef<HTMLObjectElement>(null); | ||
|
||
const { url, description } = item; | ||
|
||
const createdText = `${createdAt.time} ${createdAt.result} ago`; | ||
|
||
useEffect(() => { | ||
const nowDate = new Date(); | ||
let createdate = new Date(item.created_at); | ||
const date = (Number(nowDate) - Number(createdate)) / 1000; | ||
setCreatedAt(calculateDate(date)); | ||
setFullDate(changeDate(createdate)); | ||
}, [item]); | ||
|
||
useEffect(() => { | ||
function handleClickOutside(e: any) { | ||
if ( | ||
kebabView && | ||
kebabRef.current && | ||
!kebabRef.current.contains(e.target) | ||
) { | ||
setKebaView(false); | ||
} | ||
} | ||
|
||
document.addEventListener('mousedown', handleClickOutside); | ||
return () => { | ||
document.removeEventListener('mousedown', handleClickOutside); | ||
}; | ||
}, [kebabView]); | ||
Comment on lines
+28
to
+43
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. kebabView 와 이 로직을 커스텀 훅으로 분리하면 어떨까요? |
||
|
||
return ( | ||
<S.ItemCard> | ||
<S.StarIcon | ||
src={like ? '/full_star.svg' : '/star.svg'} | ||
alt="별 이미지" | ||
onClick={() => { | ||
setLike(!like); | ||
}} | ||
/> | ||
{image_source ? ( | ||
<S.ItemImg image={image_source} /> | ||
) : ( | ||
<S.EmptyImg> | ||
<Image src="/logo.svg" alt="빈 이미지" width={133} height={24} /> | ||
</S.EmptyImg> | ||
)} | ||
<S.ItemInfo> | ||
<S.KebabIcon | ||
src="/kebab.svg" | ||
alt="kebabIcon" | ||
onClick={() => setKebaView(!kebabView)} | ||
/> | ||
<S.ItemDate>{createdText}</S.ItemDate> | ||
<S.ItemDescription> | ||
<a href={url} target="_blank" rel="noreferrer"> | ||
{description ? description : url} | ||
</a> | ||
</S.ItemDescription> | ||
<S.ItemFullDate>{fullDate}</S.ItemFullDate> | ||
</S.ItemInfo> | ||
{kebabView && <KebabMenu menuRef={kebabRef} />} | ||
</S.ItemCard> | ||
); | ||
} | ||
|
||
export default Card; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { ReactNode } from 'react'; | ||
import styled from 'styled-components'; | ||
|
||
const Container = styled.div<{ $empty: number }>` | ||
gap: 2rem; | ||
display: grid; | ||
grid-template-columns: ${(props) => | ||
props.$empty > 0 ? 'repeat(3, 1fr)' : 'none'}; | ||
margin: 0 auto; | ||
position: relative; | ||
|
||
@media (max-width: 1199px) { | ||
grid-template-columns: ${(props) => (props.$empty > 0 ? '1fr 1fr' : '1fr')}; | ||
} | ||
|
||
@media (max-width: 767px) { | ||
grid-template-columns: 1fr; | ||
} | ||
`; | ||
|
||
function ContentsContainer({ | ||
children, | ||
content, | ||
}: { | ||
children: ReactNode; | ||
content: number; | ||
}) { | ||
return <Container $empty={content}>{children}</Container>; | ||
} | ||
|
||
export default ContentsContainer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import styled from 'styled-components'; | ||
|
||
export const FolderName = styled.span<{ $select: string | boolean }>` | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
padding: 0.8rem 1.2rem; | ||
border-radius: 0.5rem; | ||
border: 1px solid var(--Primary); | ||
background-color: ${(props) => | ||
props.$select === 'select' ? 'var(--Primary)' : '#fff'}; | ||
cursor: pointer; | ||
height: 3.5rem; | ||
font-size: 1.6rem; | ||
white-space: nowrap; | ||
color: ${(props) => (props.$select === 'select' ? '#fff' : '#000')}; | ||
|
||
@media (max-width: 768px) { | ||
font-size: 1.2rem; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import React from 'react'; | ||
import * as S from './FolderButton.styled'; | ||
import { Folder } from '@/hooks/useGetFolderList'; | ||
|
||
function FolderButton({ | ||
item, | ||
setFolderId, | ||
setFolderName, | ||
isSelected, | ||
handleMenuClick, | ||
index, | ||
}: { | ||
item: Folder; | ||
setFolderId: React.Dispatch<React.SetStateAction<number>>; | ||
setFolderName: React.Dispatch<React.SetStateAction<string>>; | ||
Comment on lines
+14
to
+15
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.
|
||
isSelected: string; | ||
handleMenuClick: (index: number) => void; | ||
index: number; | ||
}) { | ||
const changeFolder = () => { | ||
setFolderId(item.id); | ||
setFolderName(item.name); | ||
handleMenuClick(index); | ||
}; | ||
|
||
return ( | ||
<S.FolderName onClick={changeFolder} $select={isSelected}> | ||
{item.name} | ||
</S.FolderName> | ||
); | ||
} | ||
|
||
export default FolderButton; |
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.
불필요한 프래그먼트가 있네요!