Skip to content

Commit

Permalink
아티클 상세 페이지에서 본문에 해당하는 내용을 서버로부터 가져옵니다. (#132)
Browse files Browse the repository at this point in the history
* feat : 아티클 상세페이지를 가져올 수 있는 api와 훅을 추가합니다.

* feat : header 에서 slug를 가져와 article 을 프리패치합니다.

* feat : 아티클 상세 페이지에서 관심사에 따라 컴포넌트를 분리해줍니다.

* feat : header 에서 가져오는 params 를 page props 에서 가져올 수 있도록 수정합니다.
  • Loading branch information
hhhminme authored Sep 20, 2023
1 parent f798f6e commit ddc2778
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 138 deletions.
11 changes: 11 additions & 0 deletions apps/web/app/(articles)/_api/get-article.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from "zod";
import { http } from "../../../lib/http";
import type { ArticleDTO } from "../_types/articles.types";
import { ArticleResponseSchema } from "../_types/articles.types";

export const getArticle = (slug: string): Promise<ArticleDTO | undefined> => {
return http.get({
url: `/articles/${slug}`,
schema: ArticleResponseSchema,
});
};
40 changes: 40 additions & 0 deletions apps/web/app/(articles)/_components/article-action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ArticleDTO } from "../_types/articles.types";

interface ArticleActionProps {
article: ArticleDTO;
}

export function ArticleAction({ article: { article } }: ArticleActionProps): JSX.Element {
const { author } = article;

return (
<div className="article-actions">
<div className="article-meta">
<a href="profile.html">
<img alt="" src="http://i.imgur.com/Qr71crq.jpg" />
</a>
<div className="info">
<a className="author" href="/">
{author.username}
</a>
<span className="date">January 20th</span>
</div>
<button className="btn btn-sm btn-outline-secondary" type="button">
<i className="ion-plus-round" />
&nbsp; Following {author.username}
</button>
&nbsp;
<button className={`btn btn-sm btn-outline-primary ${article.favorited ? "active" : ""}`} type="button">
<i className="ion-heart" />
&nbsp; Favorite Article <span className="counter">({article.favoritesCount})</span>
</button>
{/* <button className="btn btn-sm btn-outline-secondary" type="button">
<i className="ion-edit" /> Edit Article
</button>
<button className="btn btn-sm btn-outline-danger" type="button">
<i className="ion-trash-a" /> Delete Article
</button> */}
</div>
</div>
);
}
55 changes: 55 additions & 0 deletions apps/web/app/(articles)/_components/article-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Link from "next/link";
import { useGetArticle } from "../_hooks/use-get-article";
import type { ArticleDTO } from "../_types/articles.types";

interface ArticleBannerProps {
article: ArticleDTO;
}

export default function ArticleBanner({ article: { article } }: ArticleBannerProps): JSX.Element {
const { author } = article;

return (
<div className="banner">
<div className="container">
<h1>{article.title}</h1>

<div className="article-meta">
<Link
href={{
pathname: `/profile/${author.username}`,
}}
>
<img alt="" src={author.image} />
</Link>
<div className="info">
<Link
className="author"
href={{
pathname: `/profile/${author.username}`,
}}
>
{author.username}
</Link>
<span className="date">January 20th</span>
</div>
<button className="btn btn-sm btn-outline-secondary" type="button">
<i className="ion-plus-round" />
&nbsp; Follow {article.author.username} <span className="counter">({author.following})</span>
</button>
&nbsp;&nbsp;
<button className={`btn btn-sm btn-outline-primary ${article.favorited ? "active" : ""}`} type="button">
<i className="ion-heart" />
&nbsp; Favorite Post <span className="counter">({article.favoritesCount})</span>
</button>
{/* <button className="btn btn-sm btn-outline-secondary" type="button">
<i className="ion-edit" /> Edit Article
</button> */}
{/* <button className="btn btn-sm btn-outline-danger" type="button">
<i className="ion-trash-a" /> Delete Article
</button> */}
</div>
</div>
</div>
);
}
56 changes: 56 additions & 0 deletions apps/web/app/(articles)/_components/article-comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

export function ArticleComment(): JSX.Element {
return (
<div className="row">
<div className="col-xs-12 col-md-8 offset-md-2">
<form className="card comment-form">
<div className="card-block">
<textarea className="form-control" placeholder="Write a comment..." rows={3} />
</div>
<div className="card-footer">
<img alt="" className="comment-author-img" src="http://i.imgur.com/Qr71crq.jpg" />
<button className="btn btn-sm btn-primary" type="button">
Post Comment
</button>
</div>
</form>

<div className="card">
<div className="card-block">
<p className="card-text">With supporting text below as a natural lead-in to additional content.</p>
</div>
<div className="card-footer">
<a className="comment-author" href="/profile/author">
<img alt="" className="comment-author-img" src="http://i.imgur.com/Qr71crq.jpg" />
</a>
&nbsp;
<a className="comment-author" href="/profile/jacob-schmidt">
Jacob Schmidt
</a>
<span className="date-posted">Dec 29th</span>
</div>
</div>

<div className="card">
<div className="card-block">
<p className="card-text">With supporting text below as a natural lead-in to additional content.</p>
</div>
<div className="card-footer">
<a className="comment-author" href="/profile/author">
<img alt="" className="comment-author-img" src="http://i.imgur.com/Qr71crq.jpg" />
</a>
&nbsp;
<a className="comment-author" href="/profile/jacob-schmidt">
Jacob Schmidt
</a>
<span className="date-posted">Dec 29th</span>
<span className="mod-options">
<i className="ion-trash-a" />
</span>
</div>
</div>
</div>
</div>
);
}
28 changes: 28 additions & 0 deletions apps/web/app/(articles)/_components/article-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ArticleDTO } from "../_types/articles.types";

interface ArticleContentProps {
article: ArticleDTO;
}

export function ArticleContent({ article: { article } }: ArticleContentProps): JSX.Element {
return (
<>
<div className="row article-content">
<div className="col-md-12">
<h2 id="introducing-ionic">Description</h2>
<p>{article.description}</p>
<h2 id="introducing-ionic">Main</h2>
<p>{article.body.replace(/\\n/g, "\n")}</p>
<ul className="tag-list">
{article.tagList.map(tag => (
<li className="tag-default tag-pill tag-outline" key={tag}>
{tag}
</li>
))}
</ul>
</div>
</div>
<hr />
</>
);
}
30 changes: 30 additions & 0 deletions apps/web/app/(articles)/_components/article-detail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { useGetArticle } from "../_hooks/use-get-article";
import { ArticleComment } from "./article-comment";
import ArticleBanner from "./article-banner";
import { ArticleAction } from "./article-action";
import { ArticleContent } from "./article-content";

interface ArticleDetailProps {
slug: string;
}

export function ArticleDetail({ slug }: ArticleDetailProps): JSX.Element {
const { data } = useGetArticle(slug);

return (
<div className="article-page">
{data ? (
<>
<ArticleBanner article={data} />
<div className="container page">
<ArticleContent article={data} />
<ArticleAction article={data} />
<ArticleComment />
</div>
</>
) : null}
</div>
);
}
5 changes: 3 additions & 2 deletions apps/web/app/(articles)/_constants/querykeys.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ArticlesQueryParamsType } from "../_types/articles.types";

export const queryKeys = {
articles: (params: ArticlesQueryParamsType) => ["articles", params] as const,
};
articles: (params: ArticlesQueryParamsType) => ["articles", params],
article: (slug: string) => ["articles", slug],
} as const;
11 changes: 11 additions & 0 deletions apps/web/app/(articles)/_hooks/use-get-article.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { UseQueryResult } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import type { ArticleDTO } from "../_types/articles.types";
import { queryKeys } from "../_constants/querykeys";
import { getArticle } from "../_api/get-article";

export function useGetArticle(slug: string): UseQueryResult<ArticleDTO | undefined, Error> {
return useQuery(queryKeys.article(slug), () => getArticle(slug), {
suspense: true,
});
}
5 changes: 5 additions & 0 deletions apps/web/app/(articles)/_types/articles.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ export const ArticlesResponseSchema = z.object({
articlesCount: z.number(),
});

export const ArticleResponseSchema = z.object({
article: ArticleSchema,
});

export type ArticlesDTO = z.infer<typeof ArticlesResponseSchema>;
export type ArticleDTO = z.infer<typeof ArticleResponseSchema>;
33 changes: 33 additions & 0 deletions apps/web/app/(articles)/article/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { dehydrate } from "@tanstack/query-core";
import { Suspense } from "react";
import getQueryClient from "../../../../lib/get-query-clinet";
import { queryKeys } from "../../_constants/querykeys";
import { getArticle } from "../../_api/get-article";
import Hydrate from "../../../../lib/query-hydrate";
import { ArticleDetail } from "../../_components/article-detail";

interface ArticlePageProps {
params: {
slug: string;
};
}

export default async function ArticlePage({ params }: ArticlePageProps): Promise<JSX.Element> {
const { slug } = params;

if (!slug) {
throw new Error("ERROR : article path is not found");
}

const queryClient = getQueryClient();
await queryClient.prefetchQuery(queryKeys.article(slug), () => getArticle(slug));
const dehydrateState = dehydrate(queryClient);

return (
<Hydrate state={dehydrateState}>
<Suspense fallback={<div>loading...</div>}>
<ArticleDetail slug={slug} />
</Suspense>
</Hydrate>
);
}
Loading

0 comments on commit ddc2778

Please sign in to comment.