Skip to content

Commit

Permalink
[김주동] sprint10 (#124)
Browse files Browse the repository at this point in the history
* chore: 머지 후 브랜치 삭제 github action 추가

* Initial commit from Create Next App

* fix: 머지 후 브랜치 삭제 github action 수정

* env: workflows 폴더로 이동

* First commit Sprint Mission5 on React

* First commit Sprint Mission5 on React (#78)

* Sprint mission 6 commit.

* Sprint Mission 6 Update 1

* update sprint mission 6

* Sprint mission 6 update commit.

* delete .bak files

* Update Comments

* modify ItemComment

* React 김주동 sprint7 (#97)

* First commit Sprint Mission5 on React

* Sprint mission 6 commit.

* Sprint Mission 6 Update 1

* update sprint mission 6

* Sprint mission 6 update commit.

* delete .bak files

* Update Comments

* modify ItemComment

* refactoring to typescript

* add LoginPage

* First commit

* REFACTOR: styled-components

* Merge branch 'Next-김주동' of https://github.com/joodongkim/10-Sprint-Mission into Next-김주동

* change from React.FC<type> to ({}:type)

* feat: replace interface to type

* refactoring for nextjs

* feat: sprint#10 first commit

* review: sprint#10

---------

Co-authored-by: withyj <withyj@codeit.kr>
Co-authored-by: hanseulhee <3021062@gmail.com>
  • Loading branch information
3 people authored Nov 5, 2024
1 parent 2afcec9 commit d0b2b5b
Show file tree
Hide file tree
Showing 71 changed files with 4,366 additions and 7,577 deletions.
3 changes: 0 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
Expand Down
50 changes: 50 additions & 0 deletions api/articleApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export async function getArticleDetail(articleId: number) {
if (!articleId) {
throw new Error("Invalid article ID");
}

try {
const response = await fetch(
`https://panda-market-api.vercel.app/articles/${articleId}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("Failed to fetch article detail:", error);
throw error;
}
}

export async function getArticleComments({
articleId,
limit = 10,
}: {
articleId: number;
limit?: number;
}) {
if (!articleId) {
throw new Error("Invalid article ID");
}

const params = {
limit: String(limit),
};

try {
const query = new URLSearchParams(params).toString();
const response = await fetch(
`https://panda-market-api.vercel.app/articles/${articleId}/comments?${query}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("Failed to fetch article comments:", error);
throw error;
}
}
8 changes: 8 additions & 0 deletions api/example.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
GET https://panda-market-api.vercel.app/articles

###

GET https://panda-market-api.vercel.app/articles?orderBy=like&pageSize=10&page=1

###

Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
import styled from "styled-components";
import {
FlexRowCentered,
LineDivider,
SectionHeader,
SectionTitle,
StyledLink,
} from "@/styles/CommonStyles";
import { Article, ArticleSortOption } from "@/types/articleTypes";
import {
ArticleInfo,
ArticleInfoWrapper,
ArticleThumbnail,
ArticleTitle,
ImageWrapper,
MainContent,
Timestamp,
} from "@/styles/BoardsStyles";
} from "@/styles/BoardStyles";
import Image from "next/image";
import { format } from "date-fns";
import Link from "next/link";
import SearchBar from "@/components/ui/SearchBar";
import DropdownMenu from "@/components/ui/DropdownMenu";
import { useEffect, useState } from "react";
import LikeCountDisplay from "@/components/ui/LikeCountDisplay";
import EmptyState from "@/components/ui/EmptyState";
import { useRouter } from "next/router";
import ArticleInfo from "@/components/board/ArticleInfo";

import { ItemContainer, ArticleInfoDiv, AddArticleLink } from "./AllArticlesSection.styles";
const ItemContainer = styled(Link)``;

type ArticleItemProps = {
interface ArticleItemProps {
article: Article;
}

const ArticleItem = ({ article }: ArticleItemProps) => {
const dateString = format(article.createdAt, "yyyy. MM. dd");

const ArticleItem: React.FC<ArticleItemProps> = ({ article }) => {
return (
<>
<ItemContainer href={`/boards/${article.id}`}>
<ItemContainer href={`/board/${article.id}`}>
<MainContent>
<ArticleTitle>{article.title}</ArticleTitle>
{article.image && (
Expand All @@ -51,25 +49,27 @@ const ArticleItem = ({ article }: ArticleItemProps) => {
)}
</MainContent>

<ArticleInfo>
<ArticleInfoDiv>
{article.writer.nickname} <Timestamp>{dateString}</Timestamp>
</ArticleInfoDiv>
<ArticleInfoWrapper>
<ArticleInfo article={article} />

<LikeCountDisplay count={article.likeCount} iconWidth={24} gap={8} />
</ArticleInfo>
</ArticleInfoWrapper>
</ItemContainer>

<LineDivider $margin="24px 0" />
</>
);
};

type AllArticlesSectionProps = {
const AddArticleLink = styled(StyledLink)``;

interface AllArticlesSectionProps {
initialArticles: Article[];
}

const AllArticlesSection = ({initialArticles}: AllArticlesSectionProps) => {
const AllArticlesSection: React.FC<AllArticlesSectionProps> = ({
initialArticles,
}) => {
const [orderBy, setOrderBy] = useState<ArticleSortOption>("recent");
const [articles, setArticles] = useState(initialArticles);

Expand Down
49 changes: 49 additions & 0 deletions components/board/ArticleCommentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ChangeEvent, useState } from "react";
import { TextArea } from "@/styles/CommonStyles";
import ArticlePageCommentThread from "@/components/board/ArticlePageCommentThread";
import {
CommentInputSection,
CommentSectionTitle,
PostCommentButton,
} from "@/styles/CommentStyles";

interface ArticleCommentSectionProps {
articleId: number;
}

const ArticleCommentSection: React.FC<ArticleCommentSectionProps> = ({
articleId,
}) => {
const [comment, setComment] = useState("");

const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setComment(e.target.value);
};

const handlePostComment = () => {};

return (
<>
<CommentInputSection>
<CommentSectionTitle>댓글 달기</CommentSectionTitle>

<TextArea
placeholder={"댓글을 입력해 주세요."}
value={comment}
onChange={handleInputChange}
/>

<PostCommentButton
onClick={handlePostComment}
disabled={!comment.trim()}
>
등록
</PostCommentButton>
</CommentInputSection>

<ArticlePageCommentThread articleId={articleId} />
</>
);
};

export default ArticleCommentSection;
75 changes: 75 additions & 0 deletions components/board/ArticleContentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import styled from "styled-components";
import { FlexRowCentered, LineDivider } from "@/styles/CommonStyles";
import { Article } from "@/types/articleTypes";
import SeeMoreIcon from "@/public/images/icons/ic_kebab.svg";
import ArticleInfo from "@/components/board/ArticleInfo";
import LikeCountDisplay from "@/components/ui/LikeCountDisplay";

const SectionContainer = styled.div`
margin-bottom: 40px;
@media ${({ theme }) => theme.mediaQuery.tablet} {
margin-bottom: 64px;
}
`;

const ArticleHeaderContainer = styled.div`
position: relative;
`;

const SeeMoreButton = styled.button`
position: absolute;
top: 0;
right: 0;
`;

const Title = styled.h1`
font-size: 20px;
font-weight: 700;
margin-bottom: 16px;
`;

const ArticleInfoWrapper = styled(FlexRowCentered)`
gap: 16px;
`;

const VerticalDivider = styled.div`
border-left: 1px solid var(--gray-200);
height: 24px;
`;

const Content = styled.p`
font-size: 16px;
`;

interface ArticleContentSectionProps {
article: Article;
}

const ArticleContentSection: React.FC<ArticleContentSectionProps> = ({
article,
}) => {
return (
<SectionContainer>
<ArticleHeaderContainer>
<Title>{article.title}</Title>

<SeeMoreButton>
<SeeMoreIcon />
</SeeMoreButton>

<ArticleInfoWrapper>
<ArticleInfo article={article} />
<VerticalDivider />
<LikeCountDisplay count={article.likeCount} iconWidth={24} gap={8} />
</ArticleInfoWrapper>
</ArticleHeaderContainer>

<LineDivider />

<Content>{article.content}</Content>
</SectionContainer>
);
};

export default ArticleContentSection;
29 changes: 29 additions & 0 deletions components/board/ArticleInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import styled from "styled-components";
import { FlexRowCentered } from "@/styles/CommonStyles";
import ProfilePlaceholder from "@/public/images/ui/ic_profile.svg";
import { Article } from "@/types/articleTypes";
import { formatDate } from "date-fns";
import { Timestamp } from "@/styles/BoardStyles";

const Container = styled(FlexRowCentered)`
gap: 8px;
color: var(--gray-600);
font-size: 14px;
`;

interface ArticleInfoProps {
article: Article;
}

const ArticleInfo: React.FC<ArticleInfoProps> = ({ article }) => {
const dateString = formatDate(article.createdAt, "yyyy. MM. dd");

return (
<Container>
<ProfilePlaceholder width={24} height={24} />
{article.writer.nickname} <Timestamp>{dateString}</Timestamp>
</Container>
);
};

export default ArticleInfo;
69 changes: 69 additions & 0 deletions components/board/ArticlePageCommentThread.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useEffect, useState } from "react";
import styled from "styled-components";
import { Comment, CommentListResponse } from "@/types/commentTypes";
import EmptyState from "@/components/ui/EmptyState";
import CommentItem from "@/components/thread/CommentItem";
import { getArticleComments } from "@/api/articleApi";

const ThreadContainer = styled.div`
margin-bottom: 40px;
`;

interface ArticlePageCommentThreadProps {
articleId: number;
}

const ArticlePageCommentThread: React.FC<ArticlePageCommentThreadProps> = ({
articleId,
}) => {
const [comments, setComments] = useState<Comment[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (!articleId) return;

const fetchComments = async () => {
setIsLoading(true);

try {
const response: CommentListResponse = await getArticleComments({
articleId,
});
setComments(response.list);
setError(null);
} catch (error) {
console.error("Error fetching comments:", error);
setError("게시글의 댓글을 불러오지 못했어요.");
} finally {
setIsLoading(false);
}
};

fetchComments();
}, [articleId]);

if (isLoading) {
return <div>게시글 댓글 로딩중...</div>;
}

if (error) {
return <div>오류: {error}</div>;
}

if (comments && !comments.length) {
return (
<EmptyState text={`아직 댓글이 없어요,\n지금 댓글을 달아 보세요!`} />
);
} else {
return (
<ThreadContainer>
{comments.map((item) => (
<CommentItem item={item} key={`comment-${item.id}`} />
))}
</ThreadContainer>
);
}
};

export default ArticlePageCommentThread;
Loading

0 comments on commit d0b2b5b

Please sign in to comment.