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

Next 엄성민 - 스프린트 미션 8 #55

Open
wants to merge 26 commits into
base: next-엄성민
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 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 .github/pull-request-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
### 기본

- [x]
<<<<<<< HEAD
- [테스트s]
=======
- []
>>>>>>> upstream/next-엄성민
- []

### 심화
Expand Down
41 changes: 41 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ _위 이미지는 판다마켓의 대표 이미지입니다._ 📸
- **스프린트 미션 8부터** 시작하는 프론트엔드 내용을 포함하고 있어요.
- 만약 스프린트 미션 9부터 프론트엔드 코드를 React가 아닌 Next로 구현하고 싶다면 next 브랜치를 사용해요.

<<<<<<< HEAD
> _스프린트 미션 내 백엔드 요구사항은 [백엔드 레포지토리](https://github.com/codeit-sprint-fullstack/4-sprint-mission-be)의 브랜치에서 관리해주세요_
=======
> _스프린트 미션 내 백엔드 요구사항은 [백엔드 레포지토리](https://github.com/codeit-sprint-fullstack/2-Sprint-mission-Be)의 브랜치에서 관리해주세요_
>>>>>>> upstream/next-엄성민

---

Expand Down
95 changes: 95 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import axios from "axios";

// const baseURL = "http://localhost:3100";
const baseURL = "https://four-sprint-mission-be-1.onrender.com";

const client = axios.create({
baseURL,
});

// 게시글 관련 api
const getArticles = async (keyword = "", order = "recent") => {
const url = `/article?keyword=${keyword}&orderBy=${order}`;
const response = await client.get(url);
eomsung marked this conversation as resolved.
Show resolved Hide resolved
const data = response.data;
return data;
};

const getArticle = async (id) => {
const url = `/article/${id}`;
const response = await client.get(url);
const data = response.data;
return data;
};

const createArticle = async (content) => {
eomsung marked this conversation as resolved.
Show resolved Hide resolved
const url = `/article`;
const response = await client.post(url, content);
const data = response.data;
return data;
};

const deleteArticle = async (id) => {
const url = `/article/${id}`;
const response = await client.delete(url);
const data = response.data;
return data;
};

const patchArticle = async (id, content) => {
const url = `/article/${id}`;
const response = await client.patch(url, content);
const data = response.data;
return data;
};

const createComment = async (id, content) => {
const url = `/article/${id}/comment`;
const response = await client.post(url, { content });
const data = response.data;
return data;
};

const getComments = async (id) => {
const url = `article/${id}/comments`;
const response = await client.get(url);
const data = response.data;
return data;
};

const deleteComment = async (id) => {
const url = `article/${id}/comment`;
const response = await client.delete(url);
const data = response.data;
return data;
};

const patchComment = async (id, content) => {
const url = `article/${id}/comment`;
const response = await client.patch(url, { content });
const data = response.data;
return data;
};

// 상품 관련 api
const getProduct = async () => {
const url = "/products";
const response = await client.get(url);
const data = response.data;
return data;
};

const api = {
getArticles,
getArticle,
createArticle,
deleteArticle,
patchArticle,
createComment,
getComments,
deleteComment,
patchComment,
getProduct,
};

export default api;
Empty file.
Empty file.
41 changes: 41 additions & 0 deletions app/(root)/_components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Link from "next/link";
import React from "react";
import icFacebook from "@/assets/svg/ic_facebook.svg";
import icInstagram from "@/assets/svg/ic_instagram.svg";
import icYoutube from "@/assets/svg/ic_youtube.svg";
import icTwitter from "@/assets/svg/ic_twitter.svg";
import Image from "next/image";
function Footer() {
return (
<div className="relative translate-y-full w-full h-[160px] bg-black flex justify-center items-center">
<div className="flex flex-col md:flex-row gap-3 md:gap-0 justify-between items-center xl:max-w-[1120px] md:max-w-[696px] w-full mt-8 ">
<p className="text-[#9CA3AF]">©codeit - 2024 </p>

<span className="flex text-[#E5E7EB] gap-[30px]">
<Link className="" href="/">
Privacy Policy
</Link>
<Link className="" href="/">
FAQ
</Link>
</span>
<span className="flex gap-3">
<a className="" href="https://www.facebook.com/">
<Image src={icFacebook} alt="facebookIcon" />
</a>
<a className="icon" href="https://www.twitter.com/">
<Image src={icTwitter} alt="twitterIcon" />
</a>
<a className="icon" href="https://www.youtube.com/">
<Image src={icYoutube} alt="youtubeIcon" />
</a>
<a className="icon" href="https://www.instagram.com/">
<Image src={icInstagram} alt="instagramIcon" />
</a>
</span>
</div>
</div>
);
}

export default Footer;
52 changes: 52 additions & 0 deletions app/(root)/_components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";
import React from "react";
import Image from "next/image";
import Logo from "@/assets/svg/pandaLogo.svg";
import Button from "@/components/Button";
import Link from "next/link";
import { usePathname } from "next/navigation";

function Header() {
const pathName = usePathname();
const showMenu = pathName === "/" ? false : true;
return (
<header className=" flex justify-center w-full min-w-max h-[70px] xl:px-[200px] px-[30px] border-b border-[#DFDFDF] sticky box-border top-0 z-10 bg-white">
<div className="flex justify-between items-center w-full ">
<div className="flex items-center text-nowrap">
<Link className="mr-4 " href="/">
<Image src={Logo.src} width={153} height={51} alt="logo" />
</Link>
{showMenu && (
eomsung marked this conversation as resolved.
Show resolved Hide resolved
<>
<Link
className={`px-[15px] py-[21px] text-[18px] ${
pathName === "/freeBoard" ? "text-Blue" : "text-[#4B5563]"
} font-bold`}
href="/freeBoard"
>
자유게시판
</Link>
<Link
className={`px-[15px] py-[21px] text-[18px] ${
pathName === "/market" ? "text-Blue" : "text-[#4B5563]"
} font-bold`}
href="/market"
>
중고마켓
</Link>
</>
)}
</div>

<Button
color="blue"
className="px-[23px] py-[23px] rounded-lg w-[128px] h-[48px] "
eomsung marked this conversation as resolved.
Show resolved Hide resolved
>
로그인
</Button>
</div>
</header>
);
}

export default Header;
104 changes: 104 additions & 0 deletions app/(root)/freeBoard/[articleId]/_components/ArticleComment.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"use client";
import api from "@/api";
import EditDeleteDropdown from "./EditDeleteDropdown";
import profile from "@/assets/svg/ic_profile.svg";
import Button from "@/components/Button";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
const ArticleComment = ({ comment }) => {
eomsung marked this conversation as resolved.
Show resolved Hide resolved
const [edit, setEdit] = useState(false);
const [updatedComment, setUpdatedComment] = useState(comment.content);
const router = useRouter();
const [timeDiff, setTimeDiff] = useState("");
const [disabled, setDisabled] = useState(true);
const [isActive, setIsActive] = useState("inactive");
useEffect(() => {
eomsung marked this conversation as resolved.
Show resolved Hide resolved
if (updatedComment === "") {
setDisabled(true);
setIsActive("inactive");
} else {
setDisabled(false);
setIsActive("active");
}
}, [updatedComment]);
useEffect(() => {
const calculateTimeDiff = () => {
eomsung marked this conversation as resolved.
Show resolved Hide resolved
const createDate = new Date(comment.updatedAt).getTime();
const currentDate = new Date().getTime();

const seconds = Math.floor((currentDate - createDate) / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

if (seconds < 60) {
setTimeDiff(`${seconds}초 전`);
} else if (minutes < 60) {
setTimeDiff(`${minutes}분 전`);
} else if (hours < 24) {
setTimeDiff(`${hours}시간 전`);
} else {
setTimeDiff(`${days}일 전`);
}
};

calculateTimeDiff();
}, [comment.updatedAt]);

const handlebuttonclick = async () => {
try {
api.patchComment(comment.id, updatedComment);
setEdit(false);
router.refresh();
} catch (e) {
console.error(e);
}
};

return (
<div className=" bg-[#FCFCFC] border-b border-[#E5E7EB] pb-3 flex flex-col gap-6">
<div className="flex justify-between items-center">
{edit ? (
<textarea
value={updatedComment}
onChange={(e) => setUpdatedComment(e.target.value)}
className="resize-none w-full px-3 py-2 bg-[#F3F4F6] rounded-xl"
></textarea>
) : (
<p> {comment.content}</p>
)}

<EditDeleteDropdown
type="comment"
commentId={comment.id}
onEdit={() => setEdit(true)}
/>
</div>
<div className="flex justify-between">
<div className="flex gap-2">
<Image src={profile.src} width={32} height={32} alt="profile" />
<div className="flex flex-col gap-1 text-[12px]">
<p className=" text-[#4B5563]">총명한 판다</p>
<p className="text-[#9CA3AF]">{timeDiff}</p>
</div>
</div>

{edit ? (
<Button
className="rounded-lg px-4"
onClick={handlebuttonclick}
disabled={disabled}
isActive={isActive}
>
저장하기
</Button>
) : (
""
)}
</div>
</div>
);
};

export default ArticleComment;
31 changes: 31 additions & 0 deletions app/(root)/freeBoard/[articleId]/_components/ArticleComments.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";
import Image from "next/image";
import empty from "@/assets/svg/img_empty.svg";
import ArticleComment from "./ArticleComment";

function ArticleComments({ comments }) {
const existComments = comments.length !== 0;
return (
<>
{existComments ? (
<div className="flex flex-col w-full gap-10">
{comments.map((comment) => {
return (
<div key={comment.id}>
<ArticleComment comment={comment} />
</div>
);
})}
</div>
) : (
<div className="flex flex-col">
<Image src={empty.src} width={140} height={140} alt="emptyIamge" />
<p>아직 댓글이 없어요,</p>
<p>지금 댓글을 달아보세요!</p>
</div>
)}
</>
);
}

export default ArticleComments;
Loading