Update comment component
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 3m18s

This commit is contained in:
Ryan Freeman 2025-05-14 21:20:14 +01:00
parent 90a4bce926
commit 4abb9cb689
5 changed files with 79 additions and 68 deletions

View File

@ -8,22 +8,19 @@ import clsx from 'clsx'
export function Code({children}: { children: ReactNode }) { export function Code({children}: { children: ReactNode }) {
const [copied, setCopied] = useState<boolean>(false) const [copied, setCopied] = useState<boolean>(false)
const preRef = useRef<HTMLPreElement>(null) const preRef = useRef<HTMLPreElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>) => { const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault() e.preventDefault()
await navigator.clipboard.writeText(preRef.current?.innerText ?? '') await navigator.clipboard.writeText(preRef.current?.innerText ?? '')
setCopied(true) setCopied(true)
setTimeout(() => setCopied(false), 1000) setTimeout(() => setCopied(false), 1000)
buttonRef.current?.blur()
} }
return ( return (
<pre className="relative group" ref={preRef}> <pre className="relative group" ref={preRef}>
<button <button
className="absolute top-0 right-0 m-5 text-zinc-400 hover:text-zinc-50" className="absolute top-0 right-0 m-5 text-zinc-400"
onClick={handleCopy} aria-label={`${!copied ? 'Copy this code' : 'Copied!'}`} onClick={handleCopy} aria-label={`${!copied ? 'Copy this code' : 'Copied!'}`}>
ref={buttonRef}>
<div className="relative size-6"> <div className="relative size-6">
<CheckIcon <CheckIcon
className={clsx('absolute text-green-500 ease-in transform transition', !copied ? 'scale-0' : 'scale-100')}/> className={clsx('absolute text-green-500 ease-in transform transition', !copied ? 'scale-0' : 'scale-100')}/>

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import React, {useActionState, useEffect, useState} from 'react' import React, {useActionState, useEffect, useMemo, useState} from 'react'
import {useSession} from 'next-auth/react' import {useSession} from 'next-auth/react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
@ -11,6 +11,7 @@ import {GitHubIcon} from '@/components/icons/SocialIcons'
import {ArrowLeftIcon} from '@/components/icons/ArrowLeftIcon' import {ArrowLeftIcon} from '@/components/icons/ArrowLeftIcon'
import {StatusMessage} from '@/components/ui/StatusMessage' import {StatusMessage} from '@/components/ui/StatusMessage'
import {getShortDurationFromNow} from '@/lib/dateFns' import {getShortDurationFromNow} from '@/lib/dateFns'
import {getCommentCount} from '@/lib/comments'
import fetcher from '@/lib/fetcher' import fetcher from '@/lib/fetcher'
import CommentFormProvider, {useCommentFormContext} from '@/app/context/CommentFormProvider' import CommentFormProvider, {useCommentFormContext} from '@/app/context/CommentFormProvider'
import useSWR from 'swr' import useSWR from 'swr'
@ -27,9 +28,6 @@ Comments.ReplyButton = function ReplyButton({comment}: ReplyButton) {
const handleReplyButton = async (e: React.MouseEvent<HTMLButtonElement>) => { const handleReplyButton = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault() e.preventDefault()
if (!session) {
await loginWithGitHub()
}
commentFormContext?.setCommentLength(0) commentFormContext?.setCommentLength(0)
commentFormContext?.commentFormRef?.current?.form?.reset() commentFormContext?.commentFormRef?.current?.form?.reset()
commentFormContext?.setReplyTo(comment); commentFormContext?.setReplyTo(comment);
@ -52,6 +50,8 @@ Comments.Comment = function Comment({comment, isReply = false, className}: {
isReply?: boolean isReply?: boolean
className?: string className?: string
}) { }) {
const {data: session} = useSession()
return ( return (
<> <>
<article <article
@ -70,7 +70,9 @@ Comments.Comment = function Comment({comment, isReply = false, className}: {
</p> </p>
</div> </div>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400 max-w-xl">{comment.content}</p> <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400 max-w-xl">{comment.content}</p>
{session &&
<Comments.ReplyButton comment={comment}/> <Comments.ReplyButton comment={comment}/>
}
</div> </div>
</article> </article>
</> </>
@ -82,12 +84,14 @@ type CommentsListProps = {
} }
Comments.List = function List({comments}: CommentsListProps) { Comments.List = function List({comments}: CommentsListProps) {
const commentCount = useMemo(() => getCommentCount(comments),
[comments]
)
return ( return (
<>
{comments.length > 0 &&
<section> <section>
<h3 className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100 mb-4"> <h3 className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100 mb-4">
Comments {commentCount > 0 ? `${commentCount} comment${commentCount > 1 ? 's' : ''}` : 'No comments'}
</h3> </h3>
{comments.map((comment) => ( {comments.map((comment) => (
<React.Fragment key={comment.id}> <React.Fragment key={comment.id}>
@ -101,8 +105,6 @@ Comments.List = function List({comments}: CommentsListProps) {
</React.Fragment> </React.Fragment>
))} ))}
</section> </section>
}
</>
) )
} }
@ -141,7 +143,11 @@ Comments.Form = function Form({slug}: { slug: string }) {
} }
} }
const handleSubmit = () => { const handleSubmit = async (e: React.FormEvent) => {
if (!session) {
e.preventDefault()
await loginWithGitHub()
}
commentFormContext?.setReplyTo(null) commentFormContext?.setReplyTo(null)
commentFormContext?.setCommentLength(0) commentFormContext?.setCommentLength(0)
} }
@ -154,16 +160,11 @@ Comments.Form = function Form({slug}: { slug: string }) {
return ( return (
<div className="mt-16"> <div className="mt-16">
{!session ?
<form action={async () => await loginWithGitHub()}>
<Button variant="secondary">
<GitHubIcon className="w-6 h-6 dark:fill-white"/>Sign in to comment
</Button>
</form> :
<form action={formAction} onSubmit={handleSubmit}> <form action={formAction} onSubmit={handleSubmit}>
<div className="flex gap-x-4"> <div className="flex gap-x-4">
{session?.user?.image !== null && {session &&
<Image className="size-12 rounded-full" src={session?.user?.image ?? ''} <Image className="size-12 rounded-full"
src={session?.user?.image ?? ''}
alt={session?.user?.name ?? ''} alt={session?.user?.name ?? ''}
width={64} height={64}/> width={64} height={64}/>
} }
@ -175,12 +176,12 @@ Comments.Form = function Form({slug}: { slug: string }) {
className="resize-none block w-full rounded-md px-3 py-1.5 text-base text-zinc-600 dark:text-zinc-400 bg-[#fafafa] dark:bg-[#121212] border-[1px] dark:border-zinc-700/40 -outline-offset-1 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 focus:dark:outline-indigo-600" className="resize-none block w-full rounded-md px-3 py-1.5 text-base text-zinc-600 dark:text-zinc-400 bg-[#fafafa] dark:bg-[#121212] border-[1px] dark:border-zinc-700/40 -outline-offset-1 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 focus:dark:outline-indigo-600"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onChange={(e) => commentFormContext?.setCommentLength(e.target.value.length ?? 0)} onChange={(e) => commentFormContext?.setCommentLength(e.target.value.length ?? 0)}
disabled={pending} disabled={pending || !session}
defaultValue={''} defaultValue={''}
maxLength={commentFormContext?.commentMaxLength} maxLength={commentFormContext?.commentMaxLength}
ref={commentFormRef} ref={commentFormRef}
placeholder={`${commentFormContext?.replyTo ? `Reply to ${commentFormContext.replyTo.user.name}` : 'Add a comment'}`} placeholder={`${!session ? 'Sign in to comment' : commentFormContext?.replyTo ? `Reply to ${commentFormContext.replyTo.user.name}` : 'Add a comment'}`}
required required={!!session}
/> />
<input type="hidden" name="parent_id" value={parentId ?? ''}/> <input type="hidden" name="parent_id" value={parentId ?? ''}/>
<input type="hidden" name="slug" value={slug}/> <input type="hidden" name="slug" value={slug}/>
@ -192,9 +193,14 @@ Comments.Form = function Form({slug}: { slug: string }) {
className="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 hover:dark:text-zinc-50" className="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 hover:dark:text-zinc-50"
onClick={handleCancel}>Cancel</button> onClick={handleCancel}>Cancel</button>
} }
{session ?
<Button variant="secondary" disabled={pending}> <Button variant="secondary" disabled={pending}>
Comment Comment
</Button> :
<Button variant="secondary">
<GitHubIcon className="w-6 h-6 dark:fill-white"/>Sign in with GitHub
</Button> </Button>
}
</div> </div>
</div> </div>
<StatusMessage className="mt-2"> <StatusMessage className="mt-2">
@ -203,7 +209,6 @@ Comments.Form = function Form({slug}: { slug: string }) {
</div> </div>
</div> </div>
</form> </form>
}
</div> </div>
) )
} }

View File

@ -17,7 +17,7 @@ export function StatusMessage({children, className, errorConditions}: StatusMess
<> <>
{children && {children &&
<div <div
className={clsx(`flex items-start sm:items-center ${className ?? ''}, ${isError ? 'text-red-800 dark:text-red-600' : 'text-green-800 dark:text-green-600'}`)}> className={clsx(`flex items-start sm:items-center ${className ?? ''}, ${isError ? 'text-red-800 dark:text-red-500' : 'text-green-800 dark:text-green-500'}`)}>
{!isError ? <CheckIcon className="size-5 mt-0.5 sm:mt-0 mr-1"/> {!isError ? <CheckIcon className="size-5 mt-0.5 sm:mt-0 mr-1"/>
: <CrossIcon className="size-5 mt-0.5 sm:mt-0 mr-1 "/>} : <CrossIcon className="size-5 mt-0.5 sm:mt-0 mr-1 "/>}
<p aria-live="polite" role="status" <p aria-live="polite" role="status"

9
lib/comments.ts Normal file
View File

@ -0,0 +1,9 @@
import type {Comment} from '@/types'
export function getCommentCount(comments: Comment[]) {
return comments.reduce((acc, comment) => {
if (!comment) return acc
const replyCount = comment.replies?.length || 0
return (acc + 1) + replyCount
}, 0)
}

View File

@ -23,15 +23,15 @@ const nextConfig = {
eslint: { eslint: {
// Warning: This allows production builds to successfully complete even if // Warning: This allows production builds to successfully complete even if
// your project has ESLint errors. // your project has ESLint errors.
ignoreDuringBuilds: true, ignoreDuringBuilds: true
}, }
} }
const withMDX = nextMDX({ const withMDX = nextMDX({
extension: /\.mdx?$/, extension: /\.mdx?$/,
options: { options: {
remarkPlugins: [remarkGfm, remarkMermaid], remarkPlugins: [remarkGfm, remarkMermaid],
rehypePlugins: [rehypePrism], rehypePlugins: [rehypePrism]
} }
}) })