Skip to content

Commit

Permalink
Add list of comments in post
Browse files Browse the repository at this point in the history
  • Loading branch information
marshallku committed Feb 12, 2024
1 parent bf819fc commit f5d7ae6
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 2 deletions.
13 changes: 11 additions & 2 deletions apps/blog/app/[category]/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import Link from "next/link";
import { Suspense } from "react";
import { MDXRemote } from "next-mdx-remote/rsc";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypePrettyCode from "rehype-pretty-code";
Expand All @@ -11,18 +12,18 @@ import remarkUnwrapImages from "remark-unwrap-images";
import imageSize from "image-size";
import { Icon } from "@marshallku/icon";
import { classNames, formatDate } from "@marshallku/utils";
import { getComments } from "#api";
import MDXComponents from "#components/MDXComponents";
import InteractPost from "#components/InteractPost";
import Image from "#components/Image";
import PostComment from "#components/PostComment";
import PostList from "#components/PostList";
import PrevNextPost from "#components/PrevNextPost";
import Typography from "#components/Typography";
import { setImageMetaData, makeIframeResponsive } from "#utils/rehype";
import { getPostBySlug, getPostSlugs, getCategoryBySlug, getPosts } from "#utils/post";
import styles from "./page.module.scss";

export const dynamic = "error";

interface PostProps {
params: {
category: string;
Expand Down Expand Up @@ -68,6 +69,11 @@ export async function generateStaticParams() {
}));
}

async function Comments({ slug }: { slug: string }) {
const comments = await getComments(slug);
return <PostComment data={comments} />;
}

const cx = classNames(styles, "page");

export default async function Post({ params: { category, slug } }: PostProps) {
Expand Down Expand Up @@ -169,6 +175,9 @@ export default async function Post({ params: { category, slug } }: PostProps) {
</Typography>
</main>
<InteractPost className={cx("__interact")} title={post.data.title} slug={post.slug} />
<Suspense fallback={<p>Loading...</p>}>
<Comments slug={`/${postSlug}`} />
</Suspense>
<PrevNextPost previousPost={posts[postIndex + 1]} nextPost={posts[postIndex - 1]} />
<aside className={cx("-related-articles")}>
<Typography
Expand Down
65 changes: 65 additions & 0 deletions apps/blog/src/components/CommentAvatar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { useState } from "react";
import { toURL } from "#utils/url";

const AVATAR_SIZE = 40;

export interface CommentAvatarProps {
name: string;
url?: string;
postAuthor?: boolean;
}

const getAvatar = (name: string, link?: string) => {
if (link?.startsWith("https:")) {
const url = toURL(link);

if (url.hostname.endsWith("tistory.com")) {
return `${url.origin}/index.gif`;
}

if (url.hostname === "github.com") {
return `https://github.com/${url.pathname.split("/").pop()}.png`;
}

return `${url.origin}/favicon.ico`;
}

return `https://api.dicebear.com/7.x/bottts/svg?seed=${name}`;
};

function CommentAvatar({ name, url, postAuthor }: CommentAvatarProps) {
const [src, setSrc] = useState(getAvatar(name, url));

if (postAuthor) {
return (
<img
width={AVATAR_SIZE}
height={AVATAR_SIZE}
src="https://cdn.jsdelivr.net/gh/marshall-ku/[email protected]/logo/logo.svg"
alt="블로그 로고"
/>
);
}

return (
<img
width={AVATAR_SIZE}
height={AVATAR_SIZE}
src={src}
alt={`${name} 님의 아바타`}
onError={({ currentTarget }) => {
const fallback = getAvatar(name);

if (currentTarget.src === fallback) {
return;
}

setSrc(fallback);
}}
/>
);
}

export default CommentAvatar;
99 changes: 99 additions & 0 deletions apps/blog/src/components/CommentBubble/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
.comment-bubble {
$self: &;
$avatar-width: 40px;

display: flex;
padding: 12px 0;

&--author {
flex-direction: row-reverse;
}

&--border {
border-top: 1px solid color(main);
}

&__avatar {
width: $avatar-width;
height: $avatar-width;
margin-top: 12px;
margin-right: 12px;
flex-shrink: 0;
border-radius: 50%;
overflow: hidden;

@at-root #{$self}--author & {
margin-left: 12px;
margin-right: 0;
}

img {
width: 100%;
height: 100%;
object-fit: cover;
}
}

&__content {
display: flex;
max-width: 100%;
min-width: 0;
flex-wrap: wrap;
padding: 12px 0;

@at-root #{$self}--author & {
flex-direction: row-reverse;
}
}

&__name,
&__date {
width: 100%;

@at-root #{$self}--author & {
text-align: right;
}
}

&__text {
position: relative;
max-width: 100%;
margin-top: 4px;
padding: 8px 12px;
border-radius: 8px;
transform-origin: top left;
background-color: color(comment-2);
white-space: pre-wrap;
word-break: break-all;

&::after {
content: "";
box-sizing: content-box;
position: absolute;
width: 17.5px;
height: 25px;
border: 0 solid color(comment-2);
border-width: 0 20px;
border-radius: 50%;
clip: rect(0, 41px, 15px, 28px);
display: block;
z-index: 1;
left: -37.4px;
top: 5px;
}

@at-root #{$self}--author & {
background-color: color(comment);

&::after {
left: auto;
right: -37.3px;
clip: rect(0, 28px, 10px, 19px);
}
}
}

&__date {
margin-top: 4px;
}
}
41 changes: 41 additions & 0 deletions apps/blog/src/components/CommentBubble/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { classNames, formatDate } from "@marshallku/utils";
import { type Comment } from "#api";
import styles from "./index.module.scss";
import CommentAvatar from "#components/CommentAvatar";
import Typography from "#components/Typography";

export interface CommentBubbleProps {
data: Comment;
border?: boolean;
}

const cx = classNames(styles, "comment-bubble");

function CommentBubble({ border, data: { name, url, body, createdAt, byPostAuthor } }: CommentBubbleProps) {
return (
<li className={cx("", byPostAuthor && "--author", border && "--border")}>
<figure className={cx("__avatar")}>
<CommentAvatar name={name} url={url} postAuthor={byPostAuthor} />
</figure>
<div className={cx("__content")}>
<div className={cx("__name")}>
<Typography
variant="c1"
component={url ? "a" : "span"}
{...(url && { href: url, target: "_blank", rel: "noopener noreferrer nofollow" })}
>
{name}
</Typography>
</div>
<Typography variant="b1" className={cx("__text")}>
{body}
</Typography>
<Typography variant="c2" className={cx("__date")}>
{formatDate(new Date(createdAt), "yyyy. MM. dd")}
</Typography>
</div>
</li>
);
}

export default CommentBubble;
5 changes: 5 additions & 0 deletions apps/blog/src/components/CommentList/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.comment-list {
width: clamp(0px, map-get($container-width, small), 100%);
margin: 24px auto 36px;
padding: 0 12px;
}
32 changes: 32 additions & 0 deletions apps/blog/src/components/CommentList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Fragment } from "react";
import { classNames } from "@marshallku/utils";
import { type CommentListResponse } from "#api";
import CommentBubble from "#components/CommentBubble";
import styles from "./index.module.scss";

export interface CommentListProps {
data: CommentListResponse;
}

const cx = classNames(styles, "comment-list");

function CommentList({ data }: CommentListProps) {
return (
<ul className={cx()}>
{data.map(({ replies, ...comment }) => (
<Fragment key={comment._id}>
<CommentBubble data={comment} border />
{!!replies?.length && (
<ul>
{replies.map((child) => (
<CommentBubble key={child._id} data={child} />
))}
</ul>
)}
</Fragment>
))}
</ul>
);
}

export default CommentList;
2 changes: 2 additions & 0 deletions apps/blog/src/components/PostComment/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.post-comment {
}
20 changes: 20 additions & 0 deletions apps/blog/src/components/PostComment/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { classNames } from "@marshallku/utils";
import { type CommentListResponse } from "#api";
import CommentList from "#components/CommentList";
import styles from "./index.module.scss";

export interface PostCommentProps {
data: CommentListResponse;
}

const cx = classNames(styles, "post-comment");

function PostComment({ data }: PostCommentProps) {
return (
<div className={cx()}>
<CommentList data={data} />
</div>
);
}

export default PostComment;
7 changes: 7 additions & 0 deletions apps/blog/src/utils/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const toURL = (value: string) => {
try {
return new URL(value);
} catch {
return new URL(`${window.location.origin}${value[0] === "/" ? value : `/${value}`}`);
}
};
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"sonarsource",
"Svgwr",
"tailwindcss",
"tistory",
"travisci",
"treeshake",
"tsup",
Expand Down

0 comments on commit f5d7ae6

Please sign in to comment.