Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
db45b99
Revert "[최제원] Sprint4 (#155)"
CJewon Nov 18, 2024
2fd73b2
react npm start 오류로 인한 npm install 및 react 개발환경 셋팅
CJewon Nov 20, 2024
6c30b66
header 추가 및 GET 리퀘스트를 받아 itemList 정렬
CJewon Nov 23, 2024
66f6832
클래스 이름 추가 및 CSS 추가
CJewon Nov 23, 2024
9c47467
파일 구조 정리 및 상품 정렬 쿼리 생성
CJewon Nov 25, 2024
c7cb73c
최신순, 인기순 드롭아웃 html 구현과 검색 input 기능 html 구현
CJewon Nov 26, 2024
c8e22ef
form 태그 내부에 있는 태그들의 css 추가 및 수정
CJewon Nov 28, 2024
b4b9ce3
Merge branch 'React-최제원' of https://github.com/codeit-bootcamp-fronte…
CJewon Nov 28, 2024
944208c
스프린트 미션5 미완성으로 코드잇에서 제공한 스프린트 미션5 완성본을 표본으로 커밋
CJewon Nov 28, 2024
7104975
addItemPage html과 css 추가
CJewon Nov 30, 2024
702b080
이미지 삭제 기능 추가
CJewon Dec 2, 2024
ad38edb
Merge branch 'React-최제원' of https://github.com/codeit-bootcamp-fronte…
CJewon Dec 4, 2024
fc914ca
input 안에 들어가있는 value값 삭제
CJewon Dec 4, 2024
94d4274
sprint6 templete code copy and commit
CJewon Dec 4, 2024
37dba11
상품 상세페이지 html 추가 및 api 연결과 간단한 css 작업도중 git push
CJewon Dec 6, 2024
48e9d67
ItemDetailPage css 작업완료
CJewon Dec 8, 2024
63114f1
user information 추가 및 useNavigate를 이용하여 목록으로 돌아가기 기능 구현
CJewon Dec 9, 2024
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
17,002 changes: 15,386 additions & 1,616 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "1-weekly-mission",
"name": "panda-market-react",
"version": "0.1.0",
"private": true,
"dependencies": {
Expand All @@ -8,8 +8,9 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.0.1",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"styled-components": "^6.1.13",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import MarketPage from "./pages/MarketPage/MarketPage";
import AddItemPage from "./pages/AddItemPage/AddItemPage";
import CommunityFeedPage from "./pages/CommunityFeedPage/CommunityFeedPage";
import Header from "./components/Layout/Header";

import ItemDetailPage from "./pages/ItemDetailPage/ItemDetailPage";
function App() {
return (
<BrowserRouter>
Expand All @@ -20,6 +20,7 @@ function App() {
<Route path="items" element={<MarketPage />} />
<Route path="additem" element={<AddItemPage />} />
<Route path="community" element={<CommunityFeedPage />} />
<Route path="items/:productId" element={<ItemDetailPage />}></Route>
</Routes>
</div>
</BrowserRouter>
Expand Down
32 changes: 32 additions & 0 deletions src/api/itemApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,35 @@ export async function getProducts(params = {}) {
throw error;
}
}

export async function getProductsById(productId) {
try {
const response = await fetch(
`https://panda-market-api.vercel.app/products/${productId}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("Failed to fetch product by ID:", error);
throw error;
Comment on lines +31 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍👍

}
}

export async function getProductsComments({ productId, limit = 5 }) {
try {
const response = await fetch(
`https://panda-market-api.vercel.app/products/${productId}/comments?limit=${limit}`
);
if (!response.ok) {
throw new Error(`HTTP error : ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("Failed to fetch product by comments", error);
throw error;
}
}
Binary file added src/assets/images/icons/Img_inquiry_empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_back.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_heart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_kebab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/images/icons/ic_plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/images/icons/ic_x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions src/components/Layout/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import Logo from "../../assets/images/logo/logo.svg";
import { Link, NavLink } from "react-router-dom";
import { Link, NavLink, useLocation } from "react-router-dom";
import "./Header.css";

// react-router-dom의 NavLink를 이용하면 활성화된 네비게이션 항목을 하이라이트해줄 수 있어요!
Expand All @@ -9,6 +9,8 @@ function getLinkStyle({ isActive }) {
}

function Header() {
const location = useLocation(); // 현재 경로 정보

return (
<header className="globalHeader">
<div className="headerLeft">
Expand All @@ -24,7 +26,16 @@ function Header() {
</NavLink>
</li>
<li>
<NavLink to="/items" style={getLinkStyle}>
{/* React Router v6 이전 버전에서는 NavLink `isActive` prop으로 바로 스타일 정보를 넣어줄 수 있었지만, 최신 버전에서는 className 또는 style을 이용해야 해요 */}
{/* /additem 페이지에서도 네이게이션의 '중고마켓' 링크 하이라이트 */}
<NavLink
to="/items"
style={({ isActive }) =>
location.pathname === "/additem" || isActive
? { color: "var(--blue)" }
: {}
}
>
중고마켓
</NavLink>
</li>
Expand Down
27 changes: 27 additions & 0 deletions src/components/UI/DeleteButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import styled from "styled-components";
import { ReactComponent as CloseIcon } from "../../assets/images/icons/ic_x.svg";

const Button = styled.button`
background-color: ${({ theme }) => theme.colors.gray[0]};
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍👍👍

width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
&:hover {
background-color: ${({ theme }) => theme.colors.blue[0]};
}
`;

function DeleteButton({ onClick, label }) {
return (
<Button aria-label={`${label} 삭제`} onClick={onClick}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Type도 명시되면 더 좋겠네요 ㅎ

<CloseIcon />
</Button>
);
}

export default DeleteButton;
33 changes: 33 additions & 0 deletions src/components/UI/DropdownMenu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.sortButtonWrapper {
position: relative;
}

.sortDropdownTriggerButton {
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 9px;
margin-left: 8px;
}

.dropdownMenu {
position: absolute;
top: 110%;
right: 0;
background: #fff;
border-radius: 8px;
border: 1px solid #e5e7eb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 99;
}

.dropdownItem {
padding: 12px 44px;
border-bottom: 1px solid #e5e7eb;
font-size: 16px;
color: #1f2937;
cursor: pointer;
}

.dropdownItem:last-child {
border-bottom: none;
}
43 changes: 43 additions & 0 deletions src/components/UI/DropdownMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState } from "react";
import "./DropdownMenu.css";
import { ReactComponent as SortIcon } from "../../assets/images/icons/ic_sort.svg";

function DropdownMenu({ onSortSelection }) {
const [isDropdownVisible, setIsDropdownVisible] = useState(false);

const toggleDropdown = () => {
setIsDropdownVisible(!isDropdownVisible);
};

return (
<div className="sortButtonWrapper">
<button className="sortDropdownTriggerButton" onClick={toggleDropdown}>
<SortIcon />
</button>

{isDropdownVisible && (
<div className="dropdownMenu">
<div
className="dropdownItem"
onClick={() => {
onSortSelection("recent");
setIsDropdownVisible(false);
}}
>
최신순
</div>
<div
className="dropdownItem"
onClick={() => {
onSortSelection("favorite");
setIsDropdownVisible(false);
}}
>
인기순
</div>
</div>
)}
</div>
);
}
export default DropdownMenu;
122 changes: 122 additions & 0 deletions src/components/UI/ImageUpload.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState } from "react";
import { Label } from "./InputItem";
import styled, { css } from "styled-components";
import { ReactComponent as PlusIcon } from "../../assets/images/icons/ic_plus.svg";
import DeleteButton from "./DeleteButton";

const ImageUploadContainer = styled.div`
display: flex;
gap: 8px;

@media ${({ theme }) => theme.mediaQuery.tablet} {
gap: 18px;
}

@media ${({ theme }) => theme.mediaQuery.desktop} {
gap: 24px;
}
`;

const squareStyles = css`
// 작은 화면에서는 max-width가 되기 전까지는 UploadButton과 ImagePreview가 각각 gap을 포함해 컨테이너 너비의 절반을 차지하도록 함
width: calc(50% - 4px);
max-width: 200px;
aspect-ratio: 1 / 1; // 정사각형 비율 유지
border-radius: 12px;

@media ${({ theme }) => theme.mediaQuery.tablet} {
width: 162px;
}

@media ${({ theme }) => theme.mediaQuery.desktop} {
width: 282px;
}
`;

// file input과 연관 짓기 위해 버튼이 대신 label로 설정
const UploadButton = styled.label`
background-color: ${({ theme }) => theme.colors.gray[1]};
color: ${({ theme }) => theme.colors.gray[0]};
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 16px;
cursor: pointer; // 버튼이 아닌 label을 사용한 경우 별도로 추가해 주세요

&:hover {
background-color: ${({ theme }) => theme.colors.gray[2]};
}

${squareStyles}
`;

const ImagePreview = styled.div`
background-image: url(${({ src }) => src});
background-size: cover;
background-position: center;
position: relative; // DeleteButton 포지셔닝을 위해 추가

${squareStyles}
`;

const DeleteButtonWrapper = styled.div`
position: absolute;
top: 12px;
right: 12px;
`;

// 브라우저 기본 '파일 선택' 버튼 대신 커스텀 버튼을 사용하기 위해 file input을 숨김 처리
const HiddenFileInput = styled.input`
display: none;
`;

function ImageUpload({ title }) {
const [imagePreviewUrl, setImagePreviewUrl] = useState("");

const handleImageChange = (event) => {
const file = event.target.files[0];
if (file) {
// 미리보기 주소 값(Object URL) 생성
const imageUrl = URL.createObjectURL(file);
setImagePreviewUrl(imageUrl);
}
};

const handleDelete = () => {
setImagePreviewUrl(""); // 미리보기 URL 리셋
};

return (
<div>
{title && <Label>{title}</Label>}

<ImageUploadContainer>
{/* HiddenFileInput의 id와 label의 htmlFor 값을 매칭해 주세요 */}
<UploadButton htmlFor="image-upload">
<PlusIcon />
이미지 등록
</UploadButton>

<HiddenFileInput
id="image-upload"
type="file"
onChange={handleImageChange}
accept="image/*" // 이미지 파일만 업로드 가능하도록 제한
/>

{/* 업로드된 이미지가 있으면 썸네일 렌더링 */}
{imagePreviewUrl && (
<ImagePreview src={imagePreviewUrl}>
<DeleteButtonWrapper>
<DeleteButton onClick={handleDelete} label="이미지 파일" />
</DeleteButtonWrapper>
</ImagePreview>
)}
</ImageUploadContainer>
</div>
);
}

export default ImageUpload;
Loading
Loading