More comment improvements
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 3m11s

This commit is contained in:
Ryan Freeman 2025-04-26 13:41:29 +01:00
parent ed815cf9a3
commit b6c7dbdd71
7 changed files with 23 additions and 24 deletions

View File

@ -3,6 +3,7 @@ SPOTIFY_CLIENT_SECRET=
SPOTIFY_REFRESH_TOKEN= SPOTIFY_REFRESH_TOKEN=
NEXT_PUBLIC_SITE_URL= NEXT_PUBLIC_SITE_URL=
GITHUB_ACCESS_TOKEN= GITHUB_ACCESS_TOKEN=
GITHUB_USER_ID=
GITHUB_USERNAME= GITHUB_USERNAME=
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_SECRET= GITHUB_SECRET=

BIN
.env.gpg

Binary file not shown.

View File

@ -4,6 +4,7 @@ import {auth, signIn} from '@/auth'
import {createClient} from '@/lib/supabase/server' import {createClient} from '@/lib/supabase/server'
import {z} from 'zod' import {z} from 'zod'
import {sendNotification} from '@/lib/ntfy' import {sendNotification} from '@/lib/ntfy'
import {extractUserId} from '@/lib/github'
export async function loginWithGitHub() { export async function loginWithGitHub() {
await signIn('github') await signIn('github')
@ -22,15 +23,6 @@ const notificationBody = (comment: { id: number, content: string }, user: { name
headers: { headers: {
Authorization: `Bearer ${process.env.NTFY_TOKEN}` Authorization: `Bearer ${process.env.NTFY_TOKEN}`
} }
},
{
action: 'http',
label: 'Delete comment',
url: `${process.env.NEXT_PUBLIC_SITE_URL}/api/comments/moderate/${comment.id}`,
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.NTFY_TOKEN}`
}
} }
] ]
} }
@ -43,7 +35,7 @@ export async function addComment(prevState: { message: string }, formData: FormD
const success_message = 'Your comment was submitted and is awaiting approval.' const success_message = 'Your comment was submitted and is awaiting approval.'
const schema = z.object({ const schema = z.object({
comment: z.string().min(1).max(300), comment: z.string().min(1).max(500),
slug: z.string(), slug: z.string(),
parent_id: z.string().optional() parent_id: z.string().optional()
}) })
@ -54,7 +46,7 @@ export async function addComment(prevState: { message: string }, formData: FormD
parent_id: formData.get('parent_id') parent_id: formData.get('parent_id')
} }
const parse = schema.safeParse({comment, slug, parent_id}); const parse = schema.safeParse({comment, slug, parent_id})
if (!parse.success) { if (!parse.success) {
return {message: validation_error} return {message: validation_error}
} }
@ -64,6 +56,7 @@ export async function addComment(prevState: { message: string }, formData: FormD
try { try {
const supabase = await createClient() const supabase = await createClient()
const session = await auth() const session = await auth()
const isMe = process.env.GITHUB_USER_ID === extractUserId(session?.user?.image ?? '')
if (!session?.user) { if (!session?.user) {
return {message: authorisation_error} return {message: authorisation_error}
@ -78,7 +71,7 @@ export async function addComment(prevState: { message: string }, formData: FormD
const {data: newComment, error} = await supabase const {data: newComment, error} = await supabase
.from('comments') .from('comments')
.insert({content: comment, article_id: article?.id, user_id: user?.id, parent_id: parent_id}) .insert({content: comment, article_id: article?.id, user_id: user?.id, parent_id: parent_id, published: isMe})
.select('*') .select('*')
.single() .single()
@ -86,7 +79,7 @@ export async function addComment(prevState: { message: string }, formData: FormD
return {message: server_error} return {message: server_error}
} }
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production' && !isMe) {
await sendNotification(notificationBody(newComment, user, article)) await sendNotification(notificationBody(newComment, user, article))
} }

View File

@ -19,7 +19,7 @@ export default function CommentFormProvider({children}: { children: ReactNode })
const [replyTo, setReplyTo] = useState<Comment | null>(null) const [replyTo, setReplyTo] = useState<Comment | null>(null)
const [commentLength, setCommentLength] = useState<number>(0) const [commentLength, setCommentLength] = useState<number>(0)
const commentFormRef = useRef<HTMLTextAreaElement>(null) const commentFormRef = useRef<HTMLTextAreaElement>(null)
const commentMaxLength = 300 const commentMaxLength = 500
const focusCommentForm = () => { const focusCommentForm = () => {
commentFormRef.current?.focus() commentFormRef.current?.focus()

View File

@ -44,14 +44,15 @@ Comments.ReplyButton = function ReplyButton({comment}: ReplyButton) {
) )
} }
Comments.Comment = function Comment({comment, isReply = false}: { Comments.Comment = function Comment({comment, isReply = false, className}: {
comment: Comment, comment: Comment,
isReply?: boolean isReply?: boolean
className?: string
}) { }) {
return ( return (
<> <>
<article <article
className={clsx('flex gap-x-4 py-5', isReply && 'ml-[66px] border-l border-zinc-100 pl-6 dark:border-zinc-700/40')}> className={clsx('flex gap-x-4 py-5', `${className ?? ''}`, isReply && 'ml-[66px] border-l border-zinc-100 pl-6 dark:border-zinc-700/40')}>
<Image src={comment.user.image} alt={comment.user.name} width={64} height={64} <Image src={comment.user.image} alt={comment.user.name} width={64} height={64}
className={clsx('rounded-full', isReply ? 'size-8' : 'size-12')}/> className={clsx('rounded-full', isReply ? 'size-8' : 'size-12')}/>
<div className="flex-auto"> <div className="flex-auto">
@ -87,8 +88,9 @@ Comments.List = function List({comments}: CommentsListProps) {
<React.Fragment key={comment.id}> <React.Fragment key={comment.id}>
<Comments.Comment comment={comment}/> <Comments.Comment comment={comment}/>
{(typeof comment.replies !== 'undefined' && comment.replies.length > 0) ? {(typeof comment.replies !== 'undefined' && comment.replies.length > 0) ?
comment.replies.map(reply => ( comment.replies.map((reply, i) => (
<Comments.Comment key={reply.id} comment={reply} isReply={true}/> <Comments.Comment key={reply.id} comment={reply} isReply={true}
className={`${i + 1 === comment.replies?.length ? 'mb-6' : ''}`}/>
)) : null )) : null
} }
</React.Fragment> </React.Fragment>

View File

@ -1,5 +1,6 @@
import {createClient} from '@/lib/supabase/client' import {createClient} from '@/lib/supabase/client'
import {QueryData} from '@supabase/supabase-js' import {QueryData} from '@supabase/supabase-js'
import type {Comment} from '@/types'
export async function getComments(slug: string) { export async function getComments(slug: string) {
try { try {
@ -27,23 +28,21 @@ export async function getComments(slug: string) {
// @ts-ignore // @ts-ignore
acc[comment.id] = {...comment, replies: []} acc[comment.id] = {...comment, replies: []}
return acc return acc
}, {}); }, {})
return comments?.reduce<Comment[]>((nested, comment) => { return comments?.reduce<Comment[]>((nested, comment) => {
if (typeof commentMap !== 'undefined') { if (typeof commentMap !== 'undefined') {
if (comment.parent_id !== null) { if (comment.parent_id !== null) {
const parent = commentMap[comment.parent_id]; const parent: Comment = commentMap[comment.parent_id]
if (parent) { if (parent) {
// @ts-ignore
parent.replies?.push(commentMap[comment.id]) parent.replies?.push(commentMap[comment.id])
// @ts-ignore
parent.replies?.sort((a, b) => a.id - b.id) parent.replies?.sort((a, b) => a.id - b.id)
} }
} else { } else {
nested.push(commentMap[comment.id]); nested.push(commentMap[comment.id])
} }
} }
return nested; return nested
}, []) }, [])
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -46,4 +46,8 @@ export async function getPinnedRepos() {
}) as PinnedReposResponse }) as PinnedReposResponse
return response.data.user.pinnedItems.nodes return response.data.user.pinnedItems.nodes
}
export function extractUserId(avatarUrl: string) {
return new URL(avatarUrl).pathname.split('/')[2]
} }