mirror of
				https://github.com/r-freeman/portfolio.git
				synced 2025-11-04 15:51:11 +00:00 
			
		
		
		
	Added comment reply functionality
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build And Publish / BuildAndPublish (push) Successful in 3m13s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build And Publish / BuildAndPublish (push) Successful in 3m13s
				
			This commit is contained in:
		
							parent
							
								
									f4e920bff4
								
							
						
					
					
						commit
						e0a116c4f2
					
				@ -41,21 +41,27 @@ export async function addComment(prevState: { message: string }, formData: FormD
 | 
			
		||||
    const authorisation_error = 'Error, you must be logged in to post a comment.'
 | 
			
		||||
    const success_message = 'Thanks, your comment was submitted and is awaiting approval.'
 | 
			
		||||
 | 
			
		||||
    console.log(formData)
 | 
			
		||||
 | 
			
		||||
    const schema = z.object({
 | 
			
		||||
        comment: z.string().min(3).max(255),
 | 
			
		||||
        slug: z.string()
 | 
			
		||||
        comment: z.string().min(3).max(255).trim(),
 | 
			
		||||
        slug: z.string(),
 | 
			
		||||
        parent_id: z.string().optional()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const {comment, slug} = {
 | 
			
		||||
    let {comment, slug, parent_id} = {
 | 
			
		||||
        comment: formData.get('comment'),
 | 
			
		||||
        slug: formData.get('slug')
 | 
			
		||||
        slug: formData.get('slug'),
 | 
			
		||||
        parent_id: formData.get('parent_id')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parse = schema.safeParse({comment, slug})
 | 
			
		||||
    const parse = schema.safeParse({comment, slug, parent_id});
 | 
			
		||||
    if (!parse.success) {
 | 
			
		||||
        return {message: general_error}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (parent_id === '') parent_id = null
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const supabase = await createClient()
 | 
			
		||||
        const session = await auth()
 | 
			
		||||
@ -73,7 +79,7 @@ export async function addComment(prevState: { message: string }, formData: FormD
 | 
			
		||||
 | 
			
		||||
        const {data: newComment, error} = await supabase
 | 
			
		||||
            .from('comments')
 | 
			
		||||
            .insert({content: comment, article_id: article?.id, user_id: user?.id})
 | 
			
		||||
            .insert({content: comment, article_id: article?.id, user_id: user?.id, parent_id: parent_id})
 | 
			
		||||
            .select('*')
 | 
			
		||||
            .single()
 | 
			
		||||
 | 
			
		||||
@ -81,7 +87,9 @@ export async function addComment(prevState: { message: string }, formData: FormD
 | 
			
		||||
            return {message: general_error}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await sendNotification(notificationBody(newComment, user, article))
 | 
			
		||||
        if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
            await sendNotification(notificationBody(newComment, user, article))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {message: success_message}
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								components/icons/ArrowLeftIcon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								components/icons/ArrowLeftIcon.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
import {Props} from '@/types'
 | 
			
		||||
 | 
			
		||||
export function ArrowLeftIcon(props: Props) {
 | 
			
		||||
    return (
 | 
			
		||||
        <svg fill="none" strokeWidth={1.5} stroke="currentColor" viewBox="0 0 24 24"
 | 
			
		||||
             xmlns="http://www.w3.org/2000/svg" aria-hidden="true" {...props}>
 | 
			
		||||
            <path strokeLinecap="round" strokeLinejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
'use client'
 | 
			
		||||
 | 
			
		||||
import React, {ReactNode, useActionState} from 'react'
 | 
			
		||||
import React, {createContext, ReactNode, RefObject, useActionState, useContext, useEffect, useRef, useState} from 'react'
 | 
			
		||||
import {useSession} from 'next-auth/react'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import clsx from 'clsx'
 | 
			
		||||
@ -8,35 +8,76 @@ import {addComment, loginWithGitHub} from '@/app/actions/comments'
 | 
			
		||||
import {Button} from '@/components/ui/Button'
 | 
			
		||||
import {GitHubIcon} from '@/components/icons/SocialIcons'
 | 
			
		||||
import {formatDistanceToNow} from 'date-fns'
 | 
			
		||||
import {ArrowLeftIcon} from '@/components/icons/ArrowLeftIcon'
 | 
			
		||||
 | 
			
		||||
type Comment = {
 | 
			
		||||
    id: number
 | 
			
		||||
    content: string
 | 
			
		||||
    created_at: string
 | 
			
		||||
    parent_id: number | null
 | 
			
		||||
    user: {
 | 
			
		||||
        id: number
 | 
			
		||||
        name: string
 | 
			
		||||
        image: string
 | 
			
		||||
    }
 | 
			
		||||
    replies?: Comment[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Comments.Comment = function Comment({comment, isReply = false}: { comment: Comment, isReply?: boolean }) {
 | 
			
		||||
type ReplyButton = {
 | 
			
		||||
    comment: Comment
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Comments.ReplyButton = function ReplyButton({comment}: ReplyButton) {
 | 
			
		||||
    const replyContext = useContext(ReplyContext)
 | 
			
		||||
    const commentFormContext = useContext(CommentFormContext)
 | 
			
		||||
 | 
			
		||||
    const handleReplyButton = (e: React.MouseEvent<HTMLButtonElement>) => {
 | 
			
		||||
        e.preventDefault()
 | 
			
		||||
        replyContext?.setReplyTo(comment)
 | 
			
		||||
        commentFormContext?.focusCommentForm()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <article className="flex gap-x-4 py-5">
 | 
			
		||||
            <Image src={comment.user.image} alt={comment.user.name} width={64} height={64}
 | 
			
		||||
                   className="size-12 rounded-full"/>
 | 
			
		||||
            <div className="flex-auto">
 | 
			
		||||
                <div className="flex items-baseline gap-x-1">
 | 
			
		||||
                    <p className="font-semibold text-sm text-zinc-800 dark:text-zinc-100">{comment.user.name}</p>
 | 
			
		||||
                    <p className="text-sm text-zinc-500 dark:text-zinc-400">
 | 
			
		||||
                        <time dateTime={comment.created_at}>
 | 
			
		||||
                            <span>· {`${formatDistanceToNow(comment.created_at)} ago`}</span>
 | 
			
		||||
                        </time>
 | 
			
		||||
                    </p>
 | 
			
		||||
        <button
 | 
			
		||||
            className="flex mt-4 text-sm gap-x-2 items-center group hover:dark:text-indigo-500 text-zinc-800 dark:text-zinc-100"
 | 
			
		||||
            onClick={handleReplyButton}
 | 
			
		||||
        >
 | 
			
		||||
            <ArrowLeftIcon
 | 
			
		||||
                className="w-4 h-4 stroke-zinc-500 dark:stroke-zinc-400 group-hover:dark:stroke-indigo-500 group-hover:stroke-indigo-500"/>Reply
 | 
			
		||||
        </button>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Comments.Comment = function Comment({comment, children, isReply = false}: {
 | 
			
		||||
    comment: Comment,
 | 
			
		||||
    children?: ReactNode,
 | 
			
		||||
    isReply?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
    const {data: session} = useSession()
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <article
 | 
			
		||||
                className={clsx('flex gap-x-4 py-5', isReply && 'ml-[62px]')}>
 | 
			
		||||
                <Image src={comment.user.image} alt={comment.user.name} width={64} height={64}
 | 
			
		||||
                       className="size-12 rounded-full"/>
 | 
			
		||||
                <div className="flex-auto">
 | 
			
		||||
                    <div className="flex items-baseline gap-x-1">
 | 
			
		||||
                        <p className="font-semibold text-sm text-zinc-800 dark:text-zinc-100">{comment.user.name}</p>
 | 
			
		||||
                        <p className="text-sm text-zinc-500 dark:text-zinc-400">
 | 
			
		||||
                            <time dateTime={comment.created_at}>
 | 
			
		||||
                                <span>· {`${formatDistanceToNow(comment.created_at)} ago`}</span>
 | 
			
		||||
                            </time>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400 max-w-xl">{comment.content}</p>
 | 
			
		||||
                    {session &&
 | 
			
		||||
                        <Comments.ReplyButton comment={comment}/>
 | 
			
		||||
                    }
 | 
			
		||||
                </div>
 | 
			
		||||
                <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400 max-w-xl">{comment.content}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </article>
 | 
			
		||||
            </article>
 | 
			
		||||
            {children}
 | 
			
		||||
        </>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -53,7 +94,11 @@ Comments.List = function List({comments}: CommentsListProps) {
 | 
			
		||||
                        Comments
 | 
			
		||||
                    </h3>
 | 
			
		||||
                    {comments.map((comment) => (
 | 
			
		||||
                        <Comments.Comment key={comment.id} comment={comment}/>
 | 
			
		||||
                        <Comments.Comment key={comment.id} comment={comment}>
 | 
			
		||||
                            {comment.replies && comment.replies.map(reply => (
 | 
			
		||||
                                <Comments.Comment key={reply.id} comment={reply} isReply={true}/>
 | 
			
		||||
                            ))}
 | 
			
		||||
                        </Comments.Comment>
 | 
			
		||||
                    ))}
 | 
			
		||||
                </section>
 | 
			
		||||
            }
 | 
			
		||||
@ -66,8 +111,8 @@ type CommentsStatusProps = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Comments.Status = function Status({children}: CommentsStatusProps) {
 | 
			
		||||
    const conditions = ['error', 'problem']
 | 
			
		||||
    const isError = conditions.some(condition => children?.toString().toLowerCase().includes(condition))
 | 
			
		||||
    const errorConditions = ['error', 'problem']
 | 
			
		||||
    const isError = errorConditions.some(condition => children?.toString().toLowerCase().includes(condition))
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <p aria-live="polite" role="status"
 | 
			
		||||
@ -86,15 +131,32 @@ const initialState: InitialState = {
 | 
			
		||||
    message: ''
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Comments.Form = function Form({slug}: CommentsProps) {
 | 
			
		||||
const CommentFormContext = createContext<{ focusCommentForm: () => void } | null>(null)
 | 
			
		||||
 | 
			
		||||
Comments.Form = function Form({slug, commentFormRef}: CommentsProps) {
 | 
			
		||||
    const [parentId, setParentId] = useState<string | number | null>('')
 | 
			
		||||
    const [state, formAction, pending] = useActionState(addComment, initialState)
 | 
			
		||||
    const {data: session} = useSession()
 | 
			
		||||
    const replyContext = useContext(ReplyContext)
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (replyContext?.replyTo?.parent_id !== null) {
 | 
			
		||||
            setParentId(replyContext?.replyTo?.parent_id ?? '')
 | 
			
		||||
        } else {
 | 
			
		||||
            setParentId(replyContext?.replyTo?.id)
 | 
			
		||||
        }
 | 
			
		||||
    }, [replyContext?.replyTo])
 | 
			
		||||
 | 
			
		||||
    const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
 | 
			
		||||
        if ((e.ctrlKey || e.metaKey) && (e.key === 'Enter' || e.key === 'NumpadEnter')) {
 | 
			
		||||
            e.preventDefault()
 | 
			
		||||
            e.currentTarget.form?.requestSubmit()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (e.key === 'Escape' && replyContext?.replyTo !== null) {
 | 
			
		||||
            replyContext?.setReplyTo(null)
 | 
			
		||||
            commentFormRef?.current?.blur()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@ -108,7 +170,7 @@ Comments.Form = function Form({slug}: CommentsProps) {
 | 
			
		||||
                <form action={formAction}>
 | 
			
		||||
                    <label htmlFor="comment"
 | 
			
		||||
                           className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
 | 
			
		||||
                        Add a comment
 | 
			
		||||
                        {replyContext?.replyTo ? `Reply to ${replyContext?.replyTo.user.name}` : 'Add a comment'}
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <div className="mt-2 flex">
 | 
			
		||||
                            <textarea
 | 
			
		||||
@ -120,17 +182,25 @@ Comments.Form = function Form({slug}: CommentsProps) {
 | 
			
		||||
                                disabled={pending}
 | 
			
		||||
                                defaultValue={''}
 | 
			
		||||
                                maxLength={255}
 | 
			
		||||
                                ref={commentFormRef}
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <input type="hidden" name="parent_id" value={parentId ?? ''}/>
 | 
			
		||||
                    <input type="hidden" name="slug" value={slug}/>
 | 
			
		||||
                    <div className="mt-2 flex justify-between items-start gap-x-4">
 | 
			
		||||
                        <Comments.Status>
 | 
			
		||||
                            {state?.message}
 | 
			
		||||
                        </Comments.Status>
 | 
			
		||||
                        <Button variant="secondary" disabled={pending}>
 | 
			
		||||
                            Comment
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        <div className="flex gap-x-4">
 | 
			
		||||
                            {replyContext?.replyTo &&
 | 
			
		||||
                                <button className="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 hover:dark:text-zinc-50"
 | 
			
		||||
                                        onClick={() => replyContext?.setReplyTo(null)}>Cancel</button>
 | 
			
		||||
                            }
 | 
			
		||||
                            <Button variant="secondary" disabled={pending}>
 | 
			
		||||
                                Comment
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
            }
 | 
			
		||||
@ -138,18 +208,38 @@ Comments.Form = function Form({slug}: CommentsProps) {
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type ReplyContextType = {
 | 
			
		||||
    replyTo: Comment | null
 | 
			
		||||
    setReplyTo: (replyTo: Comment | null) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ReplyContext = createContext<ReplyContextType | null>(null)
 | 
			
		||||
 | 
			
		||||
type CommentsProps = {
 | 
			
		||||
    slug: string
 | 
			
		||||
    comments?: any
 | 
			
		||||
    commentFormRef?: RefObject<HTMLTextAreaElement | null>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Comments({slug, comments}: CommentsProps) {
 | 
			
		||||
    const [replyTo, setReplyTo] = useState<Comment | null>(null)
 | 
			
		||||
    const commentFormRef = useRef<HTMLTextAreaElement>(null)
 | 
			
		||||
 | 
			
		||||
    const focusCommentForm = () => {
 | 
			
		||||
        commentFormRef.current?.focus()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="mt-24">
 | 
			
		||||
            {comments &&
 | 
			
		||||
                <Comments.List comments={comments}/>
 | 
			
		||||
            }
 | 
			
		||||
            <Comments.Form slug={slug}/>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ReplyContext.Provider value={{replyTo, setReplyTo}}>
 | 
			
		||||
            <CommentFormContext.Provider value={{focusCommentForm}}>
 | 
			
		||||
                <div className="mt-24">
 | 
			
		||||
                    {comments &&
 | 
			
		||||
                        <Comments.List comments={comments}/>
 | 
			
		||||
                    }
 | 
			
		||||
                    <Comments.Form slug={slug} commentFormRef={commentFormRef}/>
 | 
			
		||||
                </div>
 | 
			
		||||
            </CommentFormContext.Provider>
 | 
			
		||||
        </ReplyContext.Provider>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +1,17 @@
 | 
			
		||||
import {createClient} from '@/lib/supabase/client'
 | 
			
		||||
import {QueryData} from '@supabase/supabase-js'
 | 
			
		||||
 | 
			
		||||
export async function getComments(slug: string) {
 | 
			
		||||
    try {
 | 
			
		||||
        const supabase = await createClient()
 | 
			
		||||
        const {data: comments, error} = await supabase
 | 
			
		||||
        const commentsQuery = supabase
 | 
			
		||||
            .from('comments')
 | 
			
		||||
            .select(`
 | 
			
		||||
                    id,
 | 
			
		||||
                    content,
 | 
			
		||||
                    published,
 | 
			
		||||
                    created_at,
 | 
			
		||||
                    parent_id,
 | 
			
		||||
                    user:users!inner(id, name, image),
 | 
			
		||||
                    article:articles!inner(id, title, slug)
 | 
			
		||||
                `)
 | 
			
		||||
@ -17,7 +19,32 @@ export async function getComments(slug: string) {
 | 
			
		||||
            .eq('published', true)
 | 
			
		||||
            .order('created_at', {ascending: false})
 | 
			
		||||
 | 
			
		||||
        return comments
 | 
			
		||||
        type Comments = QueryData<typeof commentsQuery>
 | 
			
		||||
 | 
			
		||||
        const {data: comments, error} = await commentsQuery
 | 
			
		||||
 | 
			
		||||
        const commentMap = comments?.reduce<{ [key: number]: Comment }>((acc, comment) => {
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            acc[comment.id] = {...comment, replies: []}
 | 
			
		||||
            return acc
 | 
			
		||||
        }, {});
 | 
			
		||||
 | 
			
		||||
        return comments?.reduce<Comment[]>((nested, comment) => {
 | 
			
		||||
            if (typeof commentMap !== 'undefined') {
 | 
			
		||||
                if (comment.parent_id !== null) {
 | 
			
		||||
                    const parent = commentMap[comment.parent_id];
 | 
			
		||||
                    if (parent) {
 | 
			
		||||
                        // @ts-ignore
 | 
			
		||||
                        parent.replies?.push(commentMap[comment.id])
 | 
			
		||||
                        // @ts-ignore
 | 
			
		||||
                        parent.replies?.sort((a, b) => a.id - b.id)
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    nested.push(commentMap[comment.id]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return nested;
 | 
			
		||||
        }, [])
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error(error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "next dev",
 | 
			
		||||
    "dev": "next dev --turbopack",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint"
 | 
			
		||||
 | 
			
		||||
@ -1,215 +1,249 @@
 | 
			
		||||
export type Json =
 | 
			
		||||
    | string
 | 
			
		||||
    | number
 | 
			
		||||
    | boolean
 | 
			
		||||
    | null
 | 
			
		||||
    | { [key: string]: Json }
 | 
			
		||||
    | Json[]
 | 
			
		||||
  | string
 | 
			
		||||
  | number
 | 
			
		||||
  | boolean
 | 
			
		||||
  | null
 | 
			
		||||
  | { [key: string]: Json | undefined }
 | 
			
		||||
  | Json[]
 | 
			
		||||
 | 
			
		||||
export interface Database {
 | 
			
		||||
    graphql_public: {
 | 
			
		||||
        Tables: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
export type Database = {
 | 
			
		||||
  public: {
 | 
			
		||||
    Tables: {
 | 
			
		||||
      analytics: {
 | 
			
		||||
        Row: {
 | 
			
		||||
          article_id: number
 | 
			
		||||
          created_at: string | null
 | 
			
		||||
          id: number
 | 
			
		||||
          views: number
 | 
			
		||||
        }
 | 
			
		||||
        Views: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        Insert: {
 | 
			
		||||
          article_id: number
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          id?: number
 | 
			
		||||
          views?: number
 | 
			
		||||
        }
 | 
			
		||||
        Functions: {
 | 
			
		||||
            graphql: {
 | 
			
		||||
                Args: {
 | 
			
		||||
                    operationName?: string
 | 
			
		||||
                    query?: string
 | 
			
		||||
                    variables?: Json
 | 
			
		||||
                    extensions?: Json
 | 
			
		||||
                }
 | 
			
		||||
                Returns: Json
 | 
			
		||||
            }
 | 
			
		||||
        Update: {
 | 
			
		||||
          article_id?: number
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          id?: number
 | 
			
		||||
          views?: number
 | 
			
		||||
        }
 | 
			
		||||
        Enums: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        Relationships: [
 | 
			
		||||
          {
 | 
			
		||||
            foreignKeyName: "analytics_article_id_fkey"
 | 
			
		||||
            columns: ["article_id"]
 | 
			
		||||
            isOneToOne: false
 | 
			
		||||
            referencedRelation: "articles"
 | 
			
		||||
            referencedColumns: ["id"]
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
      articles: {
 | 
			
		||||
        Row: {
 | 
			
		||||
          created_at: string | null
 | 
			
		||||
          id: number
 | 
			
		||||
          slug: string
 | 
			
		||||
          title: string | null
 | 
			
		||||
        }
 | 
			
		||||
        CompositeTypes: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        Insert: {
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          id?: number
 | 
			
		||||
          slug: string
 | 
			
		||||
          title?: string | null
 | 
			
		||||
        }
 | 
			
		||||
        Update: {
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          id?: number
 | 
			
		||||
          slug?: string
 | 
			
		||||
          title?: string | null
 | 
			
		||||
        }
 | 
			
		||||
        Relationships: []
 | 
			
		||||
      }
 | 
			
		||||
      comments: {
 | 
			
		||||
        Row: {
 | 
			
		||||
          article_id: number
 | 
			
		||||
          content: string
 | 
			
		||||
          created_at: string | null
 | 
			
		||||
          id: number
 | 
			
		||||
          published: boolean | null
 | 
			
		||||
          user_id: number
 | 
			
		||||
        }
 | 
			
		||||
        Insert: {
 | 
			
		||||
          article_id: number
 | 
			
		||||
          content: string
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          id?: number
 | 
			
		||||
          published?: boolean | null
 | 
			
		||||
          user_id: number
 | 
			
		||||
        }
 | 
			
		||||
        Update: {
 | 
			
		||||
          article_id?: number
 | 
			
		||||
          content?: string
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          id?: number
 | 
			
		||||
          published?: boolean | null
 | 
			
		||||
          user_id?: number
 | 
			
		||||
        }
 | 
			
		||||
        Relationships: [
 | 
			
		||||
          {
 | 
			
		||||
            foreignKeyName: "comments_article_id_fkey"
 | 
			
		||||
            columns: ["article_id"]
 | 
			
		||||
            isOneToOne: false
 | 
			
		||||
            referencedRelation: "articles"
 | 
			
		||||
            referencedColumns: ["id"]
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            foreignKeyName: "comments_user_id_fkey"
 | 
			
		||||
            columns: ["user_id"]
 | 
			
		||||
            isOneToOne: false
 | 
			
		||||
            referencedRelation: "users"
 | 
			
		||||
            referencedColumns: ["id"]
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
      users: {
 | 
			
		||||
        Row: {
 | 
			
		||||
          created_at: string | null
 | 
			
		||||
          email: string
 | 
			
		||||
          id: number
 | 
			
		||||
          image: string | null
 | 
			
		||||
          name: string
 | 
			
		||||
        }
 | 
			
		||||
        Insert: {
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          email: string
 | 
			
		||||
          id?: number
 | 
			
		||||
          image?: string | null
 | 
			
		||||
          name: string
 | 
			
		||||
        }
 | 
			
		||||
        Update: {
 | 
			
		||||
          created_at?: string | null
 | 
			
		||||
          email?: string
 | 
			
		||||
          id?: number
 | 
			
		||||
          image?: string | null
 | 
			
		||||
          name?: string
 | 
			
		||||
        }
 | 
			
		||||
        Relationships: []
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    public: {
 | 
			
		||||
        Tables: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        }
 | 
			
		||||
        Views: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        }
 | 
			
		||||
        Functions: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        }
 | 
			
		||||
        Enums: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        }
 | 
			
		||||
        CompositeTypes: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        }
 | 
			
		||||
    Views: {
 | 
			
		||||
      [_ in never]: never
 | 
			
		||||
    }
 | 
			
		||||
    storage: {
 | 
			
		||||
        Tables: {
 | 
			
		||||
            buckets: {
 | 
			
		||||
                Row: {
 | 
			
		||||
                    allowed_mime_types: string[] | null
 | 
			
		||||
                    avif_autodetection: boolean | null
 | 
			
		||||
                    created_at: string | null
 | 
			
		||||
                    file_size_limit: number | null
 | 
			
		||||
                    id: string
 | 
			
		||||
                    name: string
 | 
			
		||||
                    owner: string | null
 | 
			
		||||
                    public: boolean | null
 | 
			
		||||
                    updated_at: string | null
 | 
			
		||||
                }
 | 
			
		||||
                Insert: {
 | 
			
		||||
                    allowed_mime_types?: string[] | null
 | 
			
		||||
                    avif_autodetection?: boolean | null
 | 
			
		||||
                    created_at?: string | null
 | 
			
		||||
                    file_size_limit?: number | null
 | 
			
		||||
                    id: string
 | 
			
		||||
                    name: string
 | 
			
		||||
                    owner?: string | null
 | 
			
		||||
                    public?: boolean | null
 | 
			
		||||
                    updated_at?: string | null
 | 
			
		||||
                }
 | 
			
		||||
                Update: {
 | 
			
		||||
                    allowed_mime_types?: string[] | null
 | 
			
		||||
                    avif_autodetection?: boolean | null
 | 
			
		||||
                    created_at?: string | null
 | 
			
		||||
                    file_size_limit?: number | null
 | 
			
		||||
                    id?: string
 | 
			
		||||
                    name?: string
 | 
			
		||||
                    owner?: string | null
 | 
			
		||||
                    public?: boolean | null
 | 
			
		||||
                    updated_at?: string | null
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            migrations: {
 | 
			
		||||
                Row: {
 | 
			
		||||
                    executed_at: string | null
 | 
			
		||||
                    hash: string
 | 
			
		||||
                    id: number
 | 
			
		||||
                    name: string
 | 
			
		||||
                }
 | 
			
		||||
                Insert: {
 | 
			
		||||
                    executed_at?: string | null
 | 
			
		||||
                    hash: string
 | 
			
		||||
                    id: number
 | 
			
		||||
                    name: string
 | 
			
		||||
                }
 | 
			
		||||
                Update: {
 | 
			
		||||
                    executed_at?: string | null
 | 
			
		||||
                    hash?: string
 | 
			
		||||
                    id?: number
 | 
			
		||||
                    name?: string
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            objects: {
 | 
			
		||||
                Row: {
 | 
			
		||||
                    bucket_id: string | null
 | 
			
		||||
                    created_at: string | null
 | 
			
		||||
                    id: string
 | 
			
		||||
                    last_accessed_at: string | null
 | 
			
		||||
                    metadata: Json | null
 | 
			
		||||
                    name: string | null
 | 
			
		||||
                    owner: string | null
 | 
			
		||||
                    path_tokens: string[] | null
 | 
			
		||||
                    updated_at: string | null
 | 
			
		||||
                    version: string | null
 | 
			
		||||
                }
 | 
			
		||||
                Insert: {
 | 
			
		||||
                    bucket_id?: string | null
 | 
			
		||||
                    created_at?: string | null
 | 
			
		||||
                    id?: string
 | 
			
		||||
                    last_accessed_at?: string | null
 | 
			
		||||
                    metadata?: Json | null
 | 
			
		||||
                    name?: string | null
 | 
			
		||||
                    owner?: string | null
 | 
			
		||||
                    path_tokens?: string[] | null
 | 
			
		||||
                    updated_at?: string | null
 | 
			
		||||
                    version?: string | null
 | 
			
		||||
                }
 | 
			
		||||
                Update: {
 | 
			
		||||
                    bucket_id?: string | null
 | 
			
		||||
                    created_at?: string | null
 | 
			
		||||
                    id?: string
 | 
			
		||||
                    last_accessed_at?: string | null
 | 
			
		||||
                    metadata?: Json | null
 | 
			
		||||
                    name?: string | null
 | 
			
		||||
                    owner?: string | null
 | 
			
		||||
                    path_tokens?: string[] | null
 | 
			
		||||
                    updated_at?: string | null
 | 
			
		||||
                    version?: string | null
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Views: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        }
 | 
			
		||||
        Functions: {
 | 
			
		||||
            can_insert_object: {
 | 
			
		||||
                Args: {
 | 
			
		||||
                    bucketid: string
 | 
			
		||||
                    name: string
 | 
			
		||||
                    owner: string
 | 
			
		||||
                    metadata: Json
 | 
			
		||||
                }
 | 
			
		||||
                Returns: undefined
 | 
			
		||||
            }
 | 
			
		||||
            extension: {
 | 
			
		||||
                Args: {
 | 
			
		||||
                    name: string
 | 
			
		||||
                }
 | 
			
		||||
                Returns: string
 | 
			
		||||
            }
 | 
			
		||||
            filename: {
 | 
			
		||||
                Args: {
 | 
			
		||||
                    name: string
 | 
			
		||||
                }
 | 
			
		||||
                Returns: string
 | 
			
		||||
            }
 | 
			
		||||
            foldername: {
 | 
			
		||||
                Args: {
 | 
			
		||||
                    name: string
 | 
			
		||||
                }
 | 
			
		||||
                Returns: string[]
 | 
			
		||||
            }
 | 
			
		||||
            get_size_by_bucket: {
 | 
			
		||||
                Args: Record<PropertyKey, never>
 | 
			
		||||
                Returns: {
 | 
			
		||||
                    size: number
 | 
			
		||||
                    bucket_id: string
 | 
			
		||||
                }[]
 | 
			
		||||
            }
 | 
			
		||||
            search: {
 | 
			
		||||
                Args: {
 | 
			
		||||
                    prefix: string
 | 
			
		||||
                    bucketname: string
 | 
			
		||||
                    limits?: number
 | 
			
		||||
                    levels?: number
 | 
			
		||||
                    offsets?: number
 | 
			
		||||
                    search?: string
 | 
			
		||||
                    sortcolumn?: string
 | 
			
		||||
                    sortorder?: string
 | 
			
		||||
                }
 | 
			
		||||
                Returns: {
 | 
			
		||||
                    name: string
 | 
			
		||||
                    id: string
 | 
			
		||||
                    updated_at: string
 | 
			
		||||
                    created_at: string
 | 
			
		||||
                    last_accessed_at: string
 | 
			
		||||
                    metadata: Json
 | 
			
		||||
                }[]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Enums: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
        }
 | 
			
		||||
        CompositeTypes: {
 | 
			
		||||
            [_ in never]: never
 | 
			
		||||
    Functions: {
 | 
			
		||||
      increment_views: {
 | 
			
		||||
        Args: {
 | 
			
		||||
          param_slug: string
 | 
			
		||||
          param_title: string
 | 
			
		||||
        }
 | 
			
		||||
        Returns: undefined
 | 
			
		||||
      }
 | 
			
		||||
      total_views: {
 | 
			
		||||
        Args: Record<PropertyKey, never>
 | 
			
		||||
        Returns: number
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    Enums: {
 | 
			
		||||
      [_ in never]: never
 | 
			
		||||
    }
 | 
			
		||||
    CompositeTypes: {
 | 
			
		||||
      [_ in never]: never
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PublicSchema = Database[Extract<keyof Database, "public">]
 | 
			
		||||
 | 
			
		||||
export type Tables<
 | 
			
		||||
  PublicTableNameOrOptions extends
 | 
			
		||||
    | keyof (PublicSchema["Tables"] & PublicSchema["Views"])
 | 
			
		||||
    | { schema: keyof Database },
 | 
			
		||||
  TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
    ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
 | 
			
		||||
        Database[PublicTableNameOrOptions["schema"]]["Views"])
 | 
			
		||||
    : never = never,
 | 
			
		||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
  ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
 | 
			
		||||
      Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
 | 
			
		||||
      Row: infer R
 | 
			
		||||
    }
 | 
			
		||||
    ? R
 | 
			
		||||
    : never
 | 
			
		||||
  : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
 | 
			
		||||
        PublicSchema["Views"])
 | 
			
		||||
    ? (PublicSchema["Tables"] &
 | 
			
		||||
        PublicSchema["Views"])[PublicTableNameOrOptions] extends {
 | 
			
		||||
        Row: infer R
 | 
			
		||||
      }
 | 
			
		||||
      ? R
 | 
			
		||||
      : never
 | 
			
		||||
    : never
 | 
			
		||||
 | 
			
		||||
export type TablesInsert<
 | 
			
		||||
  PublicTableNameOrOptions extends
 | 
			
		||||
    | keyof PublicSchema["Tables"]
 | 
			
		||||
    | { schema: keyof Database },
 | 
			
		||||
  TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
    ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
 | 
			
		||||
    : never = never,
 | 
			
		||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
  ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
 | 
			
		||||
      Insert: infer I
 | 
			
		||||
    }
 | 
			
		||||
    ? I
 | 
			
		||||
    : never
 | 
			
		||||
  : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
 | 
			
		||||
    ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
 | 
			
		||||
        Insert: infer I
 | 
			
		||||
      }
 | 
			
		||||
      ? I
 | 
			
		||||
      : never
 | 
			
		||||
    : never
 | 
			
		||||
 | 
			
		||||
export type TablesUpdate<
 | 
			
		||||
  PublicTableNameOrOptions extends
 | 
			
		||||
    | keyof PublicSchema["Tables"]
 | 
			
		||||
    | { schema: keyof Database },
 | 
			
		||||
  TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
    ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
 | 
			
		||||
    : never = never,
 | 
			
		||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
  ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
 | 
			
		||||
      Update: infer U
 | 
			
		||||
    }
 | 
			
		||||
    ? U
 | 
			
		||||
    : never
 | 
			
		||||
  : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
 | 
			
		||||
    ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
 | 
			
		||||
        Update: infer U
 | 
			
		||||
      }
 | 
			
		||||
      ? U
 | 
			
		||||
      : never
 | 
			
		||||
    : never
 | 
			
		||||
 | 
			
		||||
export type Enums<
 | 
			
		||||
  PublicEnumNameOrOptions extends
 | 
			
		||||
    | keyof PublicSchema["Enums"]
 | 
			
		||||
    | { schema: keyof Database },
 | 
			
		||||
  EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
    ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
 | 
			
		||||
    : never = never,
 | 
			
		||||
> = PublicEnumNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
  ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
 | 
			
		||||
  : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
 | 
			
		||||
    ? PublicSchema["Enums"][PublicEnumNameOrOptions]
 | 
			
		||||
    : never
 | 
			
		||||
 | 
			
		||||
export type CompositeTypes<
 | 
			
		||||
  PublicCompositeTypeNameOrOptions extends
 | 
			
		||||
    | keyof PublicSchema["CompositeTypes"]
 | 
			
		||||
    | { schema: keyof Database },
 | 
			
		||||
  CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
 | 
			
		||||
    schema: keyof Database
 | 
			
		||||
  }
 | 
			
		||||
    ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
 | 
			
		||||
    : never = never,
 | 
			
		||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
 | 
			
		||||
  ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
 | 
			
		||||
  : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"]
 | 
			
		||||
    ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
 | 
			
		||||
    : never
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user