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

Enhance the comment section #836

Merged
merged 33 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a3df2a2
refactor(web): pluralize some other parts
tszhong0411 Aug 12, 2024
c0fa599
feat(web): add comments count, replies count and filter
tszhong0411 Aug 12, 2024
f5ad006
fix(web): fix count not show in comment section
tszhong0411 Aug 13, 2024
61905a0
feat(web): add toast message when a message deleted in guestbook
tszhong0411 Aug 13, 2024
9611835
fix(web): no need to pluralize the like button
tszhong0411 Aug 14, 2024
546eb39
feat(root): add infinite scrolling for comment section
tszhong0411 Aug 14, 2024
fb6d141
refactor(root): remove unused code
tszhong0411 Aug 15, 2024
facaa37
feat(web): markdown support for comment section
tszhong0411 Aug 15, 2024
32cf8ad
feat(tailwind-config): update the style of ul, blockquote
tszhong0411 Aug 15, 2024
3dbde55
refactor(root): improve the performance of tiptap editor
tszhong0411 Aug 15, 2024
7863674
feat(web): ⌘ + Enter to post comments/replies
tszhong0411 Aug 15, 2024
256a885
feat(web): support quote reply
tszhong0411 Aug 16, 2024
0cf4e0c
feat(web): close reply editor on escape
tszhong0411 Aug 16, 2024
80e221c
feat(tailwind-config): set blockquote to normal font style
tszhong0411 Aug 17, 2024
6852fa1
feat(web): use pure textarea instead of tiptap
tszhong0411 Aug 17, 2024
ec38dd9
Merge branch 'main' into app-18-enhance-the-comment-section
tszhong0411 Aug 18, 2024
998bf9f
feat(web): support code block in comment
tszhong0411 Aug 18, 2024
34bd9f7
fix(web): quote reply content should not be trimmed
tszhong0411 Aug 18, 2024
8124c0b
refactor(mdx): only compile cli
tszhong0411 Aug 18, 2024
a01425c
refactor(web): remove console.log
tszhong0411 Aug 18, 2024
f0ce4bb
Merge branch 'main' into app-18-enhance-the-comment-section
tszhong0411 Aug 18, 2024
8270bbc
fix(web): remove default ring effect on textarea in comment section
tszhong0411 Aug 19, 2024
c47424b
feat(web): remove quote reply function
tszhong0411 Aug 19, 2024
c38c3ed
feat(web): support table in comment
tszhong0411 Aug 19, 2024
40bff64
fix(web): code block overflows in comment
tszhong0411 Aug 19, 2024
799fee4
fix(mdx): type error
tszhong0411 Aug 19, 2024
c3d6fc3
refactor(web): remove unused ref in comment editor
tszhong0411 Aug 19, 2024
3f39e86
fix(web): build error
tszhong0411 Aug 19, 2024
0d664aa
feat(web): add margin to comment
tszhong0411 Aug 19, 2024
b58d852
feat(ui): add figure className in code block
tszhong0411 Aug 19, 2024
44daae4
feat(web): update styles of comment section
tszhong0411 Aug 19, 2024
b8347b7
feat(web): update comment identifier in email
tszhong0411 Aug 19, 2024
eb8765b
feat(emails): update comment notification email
tszhong0411 Aug 19, 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
5 changes: 5 additions & 0 deletions .changeset/empty-waves-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tszhong0411/tailwind-config': patch
---

update the style of ul, blockquote
5 changes: 5 additions & 0 deletions .changeset/forty-schools-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tszhong0411/ui': patch
---

figure className is now available in code block
5 changes: 5 additions & 0 deletions .changeset/pink-windows-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tszhong0411/tailwind-config': patch
---

set blockquote to normal font style
3 changes: 2 additions & 1 deletion .cspell/libraries.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ nanostores
nextauth
nextra
normy
nuqs
nuxt
paralleldrive
rehype
Expand All @@ -33,10 +34,10 @@ sonarjs
sonner
sqld
tinycolor2
tiptap
tsup
turso
tursodatabase
unifiedjs
usehooks
vfile
zustand
17 changes: 6 additions & 11 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,6 @@
"@tanstack/react-query-next-experimental": "^5.44.0",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@tiptap/core": "^2.4.0",
"@tiptap/extension-bold": "^2.4.0",
"@tiptap/extension-document": "^2.4.0",
"@tiptap/extension-italic": "^2.4.0",
"@tiptap/extension-paragraph": "^2.4.0",
"@tiptap/extension-placeholder": "^2.4.0",
"@tiptap/extension-strike": "^2.4.0",
"@tiptap/extension-text": "^2.4.0",
"@tiptap/html": "^2.4.0",
"@tiptap/pm": "^2.4.0",
"@tiptap/react": "^2.4.0",
"@trpc/client": "11.0.0-rc.373",
"@trpc/react-query": "11.0.0-rc.373",
"@trpc/server": "11.0.0-rc.373",
Expand All @@ -62,12 +51,16 @@
"jiti": "^1.21.6",
"js-sha512": "^0.9.0",
"lucide-react": "^0.394.0",
"markdown-to-jsx": "^7.4.7",
"nanostores": "^0.10.3",
"next": "14.2.3",
"next-auth": "5.0.0-beta.19",
"next-themes": "^0.3.0",
"nuqs": "^1.17.8",
"pluralize": "^8.0.0",
"react": "18.3.1",
"react-dom": "18.2.0",
"react-intersection-observer": "^9.13.0",
"react-medium-image-zoom": "^5.2.4",
"react-spring": "^9.7.3",
"resend": "^3.2.0",
Expand All @@ -76,6 +69,7 @@
"superjson": "^2.2.1",
"tinycolor2": "^1.6.0",
"use-debounce": "^10.0.1",
"usehooks-ts": "^3.1.0",
"zod": "^3.23.8"
},
"devDependencies": {
Expand All @@ -87,6 +81,7 @@
"@tszhong0411/tsconfig": "workspace:*",
"@types/canvas-confetti": "^1.6.4",
"@types/node": "^20.14.2",
"@types/pluralize": "^0.0.33",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/rss": "^0.0.32",
Expand Down
Binary file removed apps/web/public/images/email/logo.png
Binary file not shown.
8 changes: 4 additions & 4 deletions apps/web/src/app/blog/[slug]/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ const Header = () => {
onSettled: () => utils.views.get.invalidate()
})

const viewsQuery = api.views.get.useQuery({
const viewsCountQuery = api.views.get.useQuery({
slug
})

const commentsQuery = api.comments.getCount.useQuery({
const commentsCountQuery = api.comments.getTotalCommentsCount.useQuery({
slug
})

Expand Down Expand Up @@ -63,11 +63,11 @@ const Header = () => {
</div>
<div className='space-y-1 md:mx-auto'>
<div className='text-muted-foreground'>Views</div>
{viewsQuery.isLoading ? '--' : <div>{viewsQuery.data?.views}</div>}
{viewsCountQuery.isLoading ? '--' : <div>{viewsCountQuery.data?.views}</div>}
</div>
<div className='space-y-1 md:mx-auto'>
<div className='text-muted-foreground'>Comments</div>
{commentsQuery.isLoading ? '--' : <div>{commentsQuery.data?.value}</div>}
{commentsCountQuery.isLoading ? '--' : <div>{commentsCountQuery.data?.value}</div>}
</div>
</div>
</div>
Expand Down
15 changes: 8 additions & 7 deletions apps/web/src/app/blog/[slug]/like-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ const LikeButton = (props: LikeButtonProps) => {
const buttonRef = useRef<HTMLButtonElement>(null)
const utils = api.useUtils()

const likesQuery = api.likes.get.useQuery({ slug })
const queryKey = { slug }

const likesQuery = api.likes.get.useQuery(queryKey)
const likesMutation = api.likes.patch.useMutation({
onMutate: (newData) => {
void utils.likes.get.cancel({ slug })
onMutate: async (newData) => {
await utils.likes.get.cancel(queryKey)

const previousData = utils.likes.get.getData({ slug })
const previousData = utils.likes.get.getData(queryKey)

utils.likes.get.setData({ slug }, (old) => {
utils.likes.get.setData(queryKey, (old) => {
if (!old) return old

return {
Expand All @@ -41,7 +43,7 @@ const LikeButton = (props: LikeButtonProps) => {
},
onError: (_, __, ctx) => {
if (ctx?.previousData) {
utils.likes.get.setData({ slug }, ctx.previousData)
utils.likes.get.setData(queryKey, ctx.previousData)
}
},
onSettled: () => utils.likes.get.invalidate()
Expand Down Expand Up @@ -139,7 +141,6 @@ const LikeButton = (props: LikeButtonProps) => {
</g>
</svg>
Like
{likesQuery.data && likesQuery.data.likes + cacheCount === 1 ? '' : 's'}
<Separator orientation='vertical' className='bg-zinc-700' />
{likesQuery.isLoading ? <div> -- </div> : <div>{likesQuery.data!.likes + cacheCount}</div>}
</button>
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getTOC } from '@tszhong0411/mdx'
import { allBlogPosts } from 'mdx/generated'
import type { Metadata, ResolvingMetadata } from 'next'
import { notFound } from 'next/navigation'
import { Suspense } from 'react'
import { type Article, type WithContext } from 'schema-dts'

import Comments from '@/components/comments'
Expand Down Expand Up @@ -154,7 +155,11 @@ const Page = async (props: PageProps) => {
<Footer />
</Providers>

{flags.comment ? <Comments slug={slug} /> : null}
{flags.comment ? (
<Suspense>
<Comments slug={slug} />
</Suspense>
) : null}
</>
)
}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/guestbook/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Menu = () => {
const utils = api.useUtils()

const guestbookMutation = api.guestbook.delete.useMutation({
onSuccess: () => toast.success('Delete message successfully'),
onSettled: () => utils.guestbook.get.invalidate(),
onError: (error) => toast.error(error.message)
})
Expand Down
132 changes: 70 additions & 62 deletions apps/web/src/components/comments/comment-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { UseTRPCMutationOptions } from '@trpc/react-query/shared'
import { Button, buttonVariants } from '@tszhong0411/ui'
import { Button, buttonVariants, toast } from '@tszhong0411/ui'
import { cva } from 'class-variance-authority'
import { ThumbsDownIcon, ThumbsUpIcon } from 'lucide-react'
import { useSession } from 'next-auth/react'

import { useCommentContext } from '@/contexts/comment'
import { useCommentsContext } from '@/contexts/comments'
import { useRatesContext } from '@/contexts/rates'
import { api, type RouterInputs } from '@/trpc/react'
import type { CommentsOutput } from '@/trpc/routers/comments'
import { useCommentParams } from '@/hooks/use-comment-params'
import { api } from '@/trpc/react'
import type { CommentsInput } from '@/trpc/routers/comments'

const rateVariants = cva(
buttonVariants({
Expand All @@ -28,100 +28,109 @@ const rateVariants = cva(
const CommentActions = () => {
const { comment, setIsReplying } = useCommentContext()
const { increment, decrement, getCount } = useRatesContext()
const { slug } = useCommentsContext()
const { slug, sort } = useCommentsContext()
const { status } = useSession()
const utils = api.useUtils()
const [params] = useCommentParams()

const queryKey: CommentsInput = {
slug,
...(comment.parentId
? {
parentId: comment.parentId,
sort: 'oldest',
type: 'replies',
...(params.reply ? { highlightedCommentId: params.reply } : {})
}
: { sort, ...(params.comment ? { highlightedCommentId: params.comment } : {}) })
}

const mutationOptions: UseTRPCMutationOptions<
RouterInputs['rates']['set'] | RouterInputs['rates']['delete'],
unknown,
void,
{
previousData: CommentsOutput | undefined
}
> = {
onMutate: (newData) => {
const ratesSetMutation = api.rates.set.useMutation({
onMutate: async (newData) => {
increment()
void utils.comments.get.cancel()

const target = {
slug,
sort: 'newest',
...(comment.parentId ? { parentId: comment.parentId } : {})
} as const

const previousData = utils.comments.get.getData(target)

utils.comments.get.setData(target, (oldData) => {
if (!oldData) return oldData

return oldData.map((c) => {
if (c.id === newData.id) {
const hasLike = 'like' in newData

let likes: number = c.likes
let dislikes: number = c.dislikes
await utils.comments.getInfiniteComments.cancel(queryKey)

if (c.liked === true) likes--
if (c.liked === false) dislikes--
const previousData = utils.comments.getInfiniteComments.getInfiniteData(queryKey)

if (hasLike && newData.like) likes++
if (hasLike && !newData.like) dislikes++
utils.comments.getInfiniteComments.setInfiniteData(queryKey, (oldData) => {
if (!oldData) {
return {
pages: [],
pageParams: []
}
}

return {
...oldData,
pages: oldData.pages.map((page) => {
return {
...c,
likes,
dislikes,
liked: hasLike ? newData.like : undefined
...page,
comments: page.comments.map((c) => {
if (c.id === newData.id) {
let likes: number = c.likes
let dislikes: number = c.dislikes

if (c.liked === true) likes--
if (c.liked === false) dislikes--

if (newData.like === true) likes++
if (newData.like === false) dislikes++

return {
...c,
likes,
dislikes,
liked: newData.like
}
}

return c
})
}
}

return c
})
})
}
})

return { previousData }
},
onError: (_, __, ctx) => {
onError: (error, _, ctx) => {
if (ctx?.previousData) {
utils.comments.get.setData({ slug }, ctx.previousData)
utils.comments.getInfiniteComments.setInfiniteData(queryKey, ctx.previousData)
}
toast.error(error.message)
},
onSettled: () => {
decrement()

if (getCount() === 0) {
void utils.comments.get.invalidate()
void utils.comments.getInfiniteComments.invalidate()
}
}
}

const ratesSetMutation = api.rates.set.useMutation(mutationOptions)
const ratesDeleteMutation = api.rates.delete.useMutation(mutationOptions)
})

const isAuthenticated = status === 'authenticated'

const rateHandler = (like: boolean) => {
if (like === comment.liked) {
ratesDeleteMutation.mutate({ id: comment.id })
} else {
ratesSetMutation.mutate({ id: comment.id, like })
if (!isAuthenticated) {
toast.error('You need to be logged in to rate comments')
return
}
ratesSetMutation.mutate({ id: comment.id, like: like === comment.liked ? null : like })
}

return (
<div className='flex gap-1'>
<div className='flex gap-1 pl-10'>
<Button
type='button'
variant='secondary'
onClick={() => {
rateHandler(true)
}}
className={rateVariants({
active: comment.liked === true || !isAuthenticated
active: comment.liked === true
})}
aria-label='Like'
disabled={!isAuthenticated}
>
<ThumbsUpIcon className='size-4' />
{comment.likes}
Expand All @@ -133,15 +142,14 @@ const CommentActions = () => {
rateHandler(false)
}}
className={rateVariants({
active: comment.liked === false || !isAuthenticated
active: comment.liked === false
})}
aria-label='Dislike'
disabled={!isAuthenticated}
>
<ThumbsDownIcon className='size-4' />
{comment.dislikes}
</Button>
{!comment.parentId && isAuthenticated ? (
{comment.parentId ? null : (
<Button
type='button'
variant='secondary'
Expand All @@ -152,7 +160,7 @@ const CommentActions = () => {
>
Reply
</Button>
) : null}
)}
</div>
)
}
Expand Down
Loading