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 #374

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
26 changes: 4 additions & 22 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
node_modules
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
dist
dist-ssr
*.local
329 changes: 9 additions & 320 deletions index.html

Large diffs are not rendered by default.

19,618 changes: 2,866 additions & 16,752 deletions package-lock.json

Large diffs are not rendered by default.

52 changes: 17 additions & 35 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,41 +1,23 @@
{
"name": "1-weekly-mission",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"react-toastify": "^10.0.5",
"web-vitals": "^2.1.4"
},
"name": "5-Weekly-Mission-ts",
"version": "0.0.0",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"dev": "vite",
"build": "tsc && vite build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"dependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"devDependencies": {
"@types/moment": "^2.13.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.25",
"@types/react-router-dom": "^5.3.3",
"typescript": "^4.1.2",
"vite": "^1.0.0-rc.13",
"vite-plugin-react": "^4.0.0"
}
}
17 changes: 17 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; // Routes를 추가해야 합니다.
import Folder from "./folder";
import Shared from "./shared";

function App() {
return (
<Router>
<Routes>
<Route path='/shared' element={<Shared />} />
<Route path='/folder' element={<Folder />} />
</Routes>
</Router>
);
}

export default App;
68 changes: 68 additions & 0 deletions src/Components/Article/Article.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Article.jsx
import React from "react";
import { useState } from "react";
import link_icon from "../../assets/link.png";
import styles from "./Article.module.css";
import Modal from "../Modal/Modal";

function Article() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [linkInput, setLinkInput] = useState("");

const handleOpenModal = () => {
if (!linkInput.trim()) {
alert("링크를 입력해주세요.");
return;
}
setIsModalOpen(true);
};

const handleCloseModal = () => {
setIsModalOpen(false);
};

const handleSubmit = (linkInput: string) => {
setLinkInput(""); // 입력값 초기화
handleCloseModal();
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLinkInput(e.target.value);
};

if (isModalOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}

return (
<div className={styles.folder_info_container}>
<div className={styles.search_div}>
<img src={link_icon} width={30} height={30} alt='link icon' />
<input
className={styles.search_input}
placeholder='링크를 추가해 보세요'
value={linkInput}
onChange={handleChange}
/>
<button className={styles.btn} onClick={handleOpenModal}>
추가하기
</button>
{isModalOpen && (
<Modal
title='폴더에 추가'
subtitle={linkInput}
list
btnText='추가하기'
btnColor='submit'
onClose={handleCloseModal}
onSubmit={() => handleSubmit(linkInput)}
/>
)}
</div>
</div>
);
}

export default Article;
199 changes: 199 additions & 0 deletions src/Components/Cards/Cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React from "react";
import { Link } from "react-router-dom";
import { useState, useRef, useEffect } from "react";
import moment from "moment";
import thumbnail from "../../assets/thumbnail.svg";
import dot from "../../assets/dot.svg";
import star from "../../assets/star_empty.png";
import formatDate from "../../utils/formatDate";
import styles from "./Cards.module.css";
import Modal from "../Modal/Modal";

function Cards({ items }) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

아래처럼 props에 대한 타입들도 지정해주면 좋을 것 같아요.

Suggested change
function Cards({ items }) {
interface CardsProps {
items : Card[]
}
function Cards({ items } : CardsProps) {

const [popoverIndex, setPopoverIndex] = useState(null); // 각 카드의 index
const popoverRef = useRef(null);

const handleKebabClick = (index) => {
setPopoverIndex(index);
};

const handleClosePopover = (e) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이벤트 타입들도 지정해주면 좋을 것 같습니다.

if (!popoverRef.current) return;

if (!popoverRef.current.contains(e.target)) {
setPopoverIndex(null);
}
};

const [modalType, setModalType] = useState(null);

const openModal = (type) => {
setModalType(type);
};

const closeModal = () => {
setModalType(null);
};

const handleSubmit = () => {
closeModal();
};

// 모달 오픈 시 스크롤 막기
useEffect(() => {
if (modalType) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
}, [modalType]);

const MINUTES = 60;
const HOURS = 24;
const DAYS = 30;
const MONTHS = 12;

const generateTimeText = (createdAt) => {
const timeDiff = moment().diff(moment(createdAt), "minutes");

if (timeDiff < 2) {
return "1 minute ago";
}
if (timeDiff <= MINUTES - 1) {
return `${timeDiff} minutes ago`;
}
if (timeDiff < MINUTES * HOURS) {
const hours = Math.floor(timeDiff / MINUTES);
return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
}
if (timeDiff <= MINUTES * HOURS * DAYS) {
const days = Math.floor(timeDiff / (MINUTES * HOURS));
return days === 1 ? "1 day ago" : `${days} days ago`;
}
if (timeDiff <= MINUTES * HOURS * DAYS * MONTHS) {
const months = Math.floor(timeDiff / (MINUTES * HOURS * DAYS));
return months === 1 ? "1 month ago" : `${months} months ago`;
}
const years = Math.floor(timeDiff / (MINUTES * HOURS * DAYS * MONTHS));
return years === 1 ? "1 year ago" : `${years} years ago`;
};

return (
<>
{items && items.length > 0 ? (
<div className={styles.card_grid_container} onClick={handleClosePopover}>
{items.map((link, index) => (
<div key={link.id} className={styles.card}>
{link.showStar && (
<div className={styles.star}>
<img src={star} width={34} height={34} alt='star' />
</div>
)}
<Link to={link.url} target='_blank'>
<div className={styles.card_img_div}>
{link.image_source ? (
<img
src={link.image_source}
className={styles.card_img}
alt={link.title}
width={450}
height={350}
/>
) : (
<img
src={thumbnail}
className={styles.card_img}
alt=''
width={450}
height={350}
/>
)}
</div>
</Link>
<div className={styles.card_info}>
<div className={styles.card_info_top}>
<p className={styles.card_info_time}>
{generateTimeText(link.created_at)}
</p>
<img
src={dot}
className={styles.dot_menu_button}
alt='dot'
tabIndex={0}
onClick={() => handleKebabClick(index)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleKebabClick(index);
}
}}
/>
{popoverIndex === index && (
<div className={styles.popover} ref={popoverRef}>
<div
role='button'
tabIndex={0}
className={styles.popover_content}
onClick={() => openModal("deleteLink")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
openModal("deleteLink");
}
}}
>
삭제하기
</div>
{modalType === "deleteLink" && (
<Modal
title='링크 삭제'
subtitle={link.url}
btnColor='delete'
btnText='삭제하기'
onClose={closeModal}
onSubmit={handleSubmit}
/>
)}
<div
role='button'
tabIndex={0}
className={styles.popover_content}
onClick={() => openModal("add")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
openModal("add");
}
}}
>
폴더에 추가
</div>
{modalType === "add" && (
<Modal
title='폴더에 추가'
subtitle={link.url}
list
btnText='추가하기'
btnColor='submit'
onClose={closeModal}
onSubmit={handleSubmit}
/>
)}
</div>
)}
</div>
<Link to={link.url} target='_blank'>
<p className={styles.card_info_body}>{link.description}</p>
</Link>
<p className={styles.card_info_date}>
{formatDate(link.created_at)}
</p>
</div>
</div>
))}
</div>
) : (
<div className={styles.no_links_message}>저장된 링크가 없습니다.</div>
)}
</>
);
}

export default Cards;
Loading
Loading