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

[박상준] Week13 #419

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
4 changes: 4 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true }]]
}
24 changes: 24 additions & 0 deletions Portal.tsx
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;
36 changes: 36 additions & 0 deletions components/Button/Button.styled.ts
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;
}
`;
14 changes: 14 additions & 0 deletions components/Button/Button.tsx
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 (
<>
Copy link
Collaborator

Choose a reason for hiding this comment

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

불필요한 프래그먼트가 있네요!

<S.Cta size={size}>{children}</S.Cta>
</>
);
}
100 changes: 100 additions & 0 deletions components/Card/Card.styled.ts
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});
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 속성으로 이미지를 설정하게 되면 접근성 면에서 좋지 않습니다.
카드 이미지 정보가 크게 의미있진 않다 생각되지만 특별한 이유가 있지 않으면 이미지 태그의 src 속성을 사용하길 권장드립니다.
가능하면 EmptyImg 와 ItemImg 를 합쳐서 하나의 컴포넌트로 관리하면 좋을 것 같아요.

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;
`;
80 changes: 80 additions & 0 deletions components/Card/Card.tsx
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
31 changes: 31 additions & 0 deletions components/ContentsContainer.tsx
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;
21 changes: 21 additions & 0 deletions components/FolderButton/FolderButton.styled.ts
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;
}
`;
33 changes: 33 additions & 0 deletions components/FolderButton/FolderButton.tsx
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

onSelect: (folder: Folder) => void;
하나로 관리할 수 있을 것 같아요~

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;
Loading
Loading