├── .eslintrc.json ├── .prettierrc ├── styles └── globals.css ├── public ├── favicon.ico └── vercel.svg ├── postcss.config.js ├── next.config.js ├── utils ├── revalidation.ts └── supabase.ts ├── tailwind.config.js ├── next-env.d.ts ├── components ├── Spinner.tsx ├── Layout.tsx ├── CommentItem.tsx ├── NoteItem.tsx ├── CommentForm.tsx └── NoteForm.tsx ├── types └── types.ts ├── .gitignore ├── tsconfig.json ├── pages ├── api │ └── revalidate │ │ ├── [id].tsx │ │ └── index.ts ├── _app.tsx ├── notes.tsx ├── note │ └── [id].tsx └── index.tsx ├── README.md ├── package.json ├── store.ts ├── hooks ├── useMutateAuth.ts ├── useMutateNote.ts └── useMutateComment.ts └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GomaGoma676/note-app-supabase-nextjs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /utils/revalidation.ts: -------------------------------------------------------------------------------- 1 | export const revalidateList = () => { 2 | fetch('/api/revalidate') 3 | } 4 | export const revalidateSingle = (id: string) => { 5 | fetch(`/api/revalidate/${id}`) 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /utils/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | export const supabase = createClient( 4 | process.env.NEXT_PUBLIC_SUPABASE_URL as string, 5 | process.env.NEXT_PUBLIC_SUPABASE_API_KEY as string 6 | ) 7 | -------------------------------------------------------------------------------- /components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { VFC } from 'react' 2 | 3 | export const Spinner: VFC = () => { 4 | return ( 5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /types/types.ts: -------------------------------------------------------------------------------- 1 | export type Comment = { 2 | id: string 3 | content: string 4 | created_at: string 5 | note_id: string 6 | user_id: string | undefined 7 | } 8 | export type Note = { 9 | id: string 10 | title: string 11 | content: string 12 | created_at: string 13 | user_id: string | undefined 14 | comments: Comment[] 15 | } 16 | export type EditedComment = Omit 17 | export type EditedNote = Omit 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /pages/api/revalidate/[id].tsx: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | type Data = { 4 | revalidated: boolean 5 | } 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | console.log('Revalidating detail page...') 12 | const { 13 | query: { id }, 14 | } = req 15 | let revalidated = false 16 | try { 17 | await res.unstable_revalidate(`/note/${id}`) 18 | //Nextjs12.2-> 19 | //await res.revalidate(`/note/${id}`) 20 | revalidated = true 21 | } catch (err) { 22 | console.log(err) 23 | } 24 | res.json({ 25 | revalidated, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /pages/api/revalidate/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | type Data = { 4 | revalidated: boolean 5 | } 6 | type Msg = { 7 | message: string 8 | } 9 | export default async function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | console.log('Revalidating notes page...') 14 | if (req.query.secret !== process.env.REVALIDATE_SECRET) { 15 | return res.status(401).json({ message: 'Your secret is invalid !' }) 16 | } 17 | let revalidated = false 18 | try { 19 | await res.unstable_revalidate('/notes') 20 | //Nextjs12.2-> 21 | //await res.revalidate('/notes') 22 | revalidated = true 23 | } catch (err) { 24 | console.log(err) 25 | } 26 | res.json({ 27 | revalidated, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Project setup 4 | ~~~ 5 | npx create-next-app@12.3.3 note-app --typescript 6 | ~~~ 7 | ~~~ 8 | yarn add next@12.3.3 9 | ~~~ 10 | ~~~ 11 | yarn add @heroicons/react@1.0.6 @supabase/supabase-js@1.33.3 react-query@3.34.19 zustand@3.7.1 12 | ~~~ 13 | ~~~ 14 | yarn add -D tailwindcss postcss autoprefixer 15 | ~~~ 16 | ~~~ 17 | yarn add -D prettier prettier-plugin-tailwindcss 18 | ~~~ 19 | https://tailwindcss.com/docs/guides/nextjs 20 | 21 | ### CASCADE Delete SQL query 22 | ~~~ 23 | ALTER TABLE public.comments 24 | ADD CONSTRAINT comments_note_id_fkey 25 | FOREIGN KEY (note_id) 26 | REFERENCES notes(id) 27 | ON DELETE CASCADE; 28 | ~~~ 29 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { VFC, ReactNode } from 'react' 2 | import Head from 'next/head' 3 | import { BadgeCheckIcon } from '@heroicons/react/solid' 4 | 5 | type Title = { 6 | title: string 7 | children: ReactNode 8 | } 9 | 10 | export const Layout: VFC = ({ children, title = 'Note app' }) => { 11 | return ( 12 | <div className="flex min-h-screen flex-col items-center justify-center font-mono text-gray-800"> 13 | <Head> 14 | <title>{title} 15 | 16 |
17 |
18 | {children} 19 |
20 |
21 | 22 |
23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "note-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^1.0.6", 13 | "@supabase/supabase-js": "^1.33.3", 14 | "next": "12.1.4", 15 | "react": "18.0.0", 16 | "react-dom": "18.0.0", 17 | "react-query": "^3.34.19", 18 | "zustand": "^3.7.1" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "17.0.23", 22 | "@types/react": "17.0.43", 23 | "@types/react-dom": "17.0.14", 24 | "autoprefixer": "^10.4.4", 25 | "eslint": "8.12.0", 26 | "eslint-config-next": "12.1.4", 27 | "postcss": "^8.4.12", 28 | "prettier": "^2.6.2", 29 | "prettier-plugin-tailwindcss": "^0.1.8", 30 | "tailwindcss": "^3.0.23", 31 | "typescript": "4.6.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import { EditedNote, EditedComment } from './types/types' 3 | 4 | type State = { 5 | editedNote: EditedNote 6 | editedComment: EditedComment 7 | updateEditedNote: (payload: EditedNote) => void 8 | updateEditedComment: (payload: EditedComment) => void 9 | resetEditedNote: () => void 10 | resetEditedComment: () => void 11 | } 12 | const useStore = create((set, _) => ({ 13 | editedNote: { id: '', title: '', content: '' }, 14 | editedComment: { id: '', content: '' }, 15 | updateEditedNote: (payload) => 16 | set({ 17 | editedNote: { 18 | id: payload.id, 19 | title: payload.title, 20 | content: payload.content, 21 | }, 22 | }), 23 | resetEditedNote: () => 24 | set({ editedNote: { id: '', title: '', content: '' } }), 25 | updateEditedComment: (payload) => 26 | set({ 27 | editedComment: { 28 | id: payload.id, 29 | content: payload.content, 30 | }, 31 | }), 32 | resetEditedComment: () => set({ editedComment: { id: '', content: '' } }), 33 | })) 34 | export default useStore 35 | -------------------------------------------------------------------------------- /hooks/useMutateAuth.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../utils/supabase' 2 | import { useState } from 'react' 3 | import { useMutation } from 'react-query' 4 | 5 | export const useMutateAuth = () => { 6 | const [email, setEmail] = useState('') 7 | const [password, setPassword] = useState('') 8 | 9 | const reset = () => { 10 | setEmail('') 11 | setPassword('') 12 | } 13 | const loginMutation = useMutation( 14 | async () => { 15 | const { error } = await supabase.auth.signIn({ email, password }) 16 | if (error) throw new Error(error.message) 17 | }, 18 | { 19 | onError: (err: any) => { 20 | alert(err.message) 21 | reset() 22 | }, 23 | } 24 | ) 25 | const registerMutation = useMutation( 26 | async () => { 27 | const { error } = await supabase.auth.signUp({ email, password }) 28 | if (error) throw new Error(error.message) 29 | }, 30 | { 31 | onError: (err: any) => { 32 | alert(err.message) 33 | reset() 34 | }, 35 | } 36 | ) 37 | 38 | return { 39 | email, 40 | setEmail, 41 | password, 42 | setPassword, 43 | loginMutation, 44 | registerMutation, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { useEffect } from 'react' 4 | import { QueryClient, QueryClientProvider } from 'react-query' 5 | import { useRouter } from 'next/router' 6 | import { supabase } from '../utils/supabase' 7 | 8 | const queryClient = new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | retry: false, 12 | refetchOnWindowFocus: false, 13 | }, 14 | }, 15 | }) 16 | 17 | function MyApp({ Component, pageProps }: AppProps) { 18 | const { push, pathname } = useRouter() 19 | const validateSession = async () => { 20 | const user = supabase.auth.user() 21 | if (user && pathname === '/') { 22 | push('/notes') 23 | } else if (!user && pathname !== '/') { 24 | await push('/') 25 | } 26 | } 27 | supabase.auth.onAuthStateChange((event, _) => { 28 | if (event === 'SIGNED_IN' && pathname === '/') { 29 | push('/notes') 30 | } 31 | if (event === 'SIGNED_OUT') { 32 | push('/') 33 | } 34 | }) 35 | useEffect(() => { 36 | validateSession() 37 | }, []) 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | export default MyApp 46 | -------------------------------------------------------------------------------- /components/CommentItem.tsx: -------------------------------------------------------------------------------- 1 | import { VFC, useEffect, useState } from 'react' 2 | import { PencilAltIcon, TrashIcon } from '@heroicons/react/solid' 3 | import { supabase } from '../utils/supabase' 4 | import useStore from '../store' 5 | import { useMutateComment } from '../hooks/useMutateComment' 6 | import { Spinner } from './Spinner' 7 | import { Comment } from '../types/types' 8 | 9 | export const CommentItem: VFC> = ({ 10 | id, 11 | content, 12 | user_id, 13 | }) => { 14 | const [userId, setUserId] = useState('') 15 | const update = useStore((state) => state.updateEditedComment) 16 | const { deleteCommentMutation } = useMutateComment() 17 | useEffect(() => { 18 | setUserId(supabase.auth.user()?.id) 19 | }, []) 20 | if (deleteCommentMutation.isLoading) { 21 | return 22 | } 23 | return ( 24 |
  • 25 | {content} 26 | {userId === user_id && ( 27 |
    28 | { 31 | update({ 32 | id: id, 33 | content: content, 34 | }) 35 | }} 36 | /> 37 | { 40 | deleteCommentMutation.mutate(id) 41 | }} 42 | /> 43 |
    44 | )} 45 |
  • 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /pages/notes.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import { LogoutIcon, DocumentTextIcon } from '@heroicons/react/solid' 3 | import { GetStaticProps } from 'next' 4 | import { supabase } from '../utils/supabase' 5 | import { Layout } from '../components/Layout' 6 | import { NoteForm } from '../components/NoteForm' 7 | import { NoteItem } from '../components/NoteItem' 8 | import { Note } from '../types/types' 9 | 10 | export const getStaticProps: GetStaticProps = async () => { 11 | console.log('ISR invoked - notes page') 12 | const { data: notes, error } = await supabase 13 | .from('notes') 14 | .select('*') 15 | .order('created_at', { ascending: true }) 16 | if (error) { 17 | throw new Error(`${error.message}: ${error.details}`) 18 | } 19 | return { 20 | props: { notes }, 21 | revalidate: false, 22 | } 23 | } 24 | type StaticProps = { 25 | notes: Note[] 26 | } 27 | const Notes: NextPage = ({ notes }) => { 28 | const signOut = () => { 29 | supabase.auth.signOut() 30 | } 31 | return ( 32 | 33 | 37 | 38 |
      39 | {notes.map((note) => ( 40 | 47 | ))} 48 |
    49 | 50 |
    51 | ) 52 | } 53 | 54 | export default Notes 55 | -------------------------------------------------------------------------------- /components/NoteItem.tsx: -------------------------------------------------------------------------------- 1 | import { VFC, useEffect, useState } from 'react' 2 | import Link from 'next/link' 3 | import { PencilAltIcon, TrashIcon } from '@heroicons/react/solid' 4 | import { supabase } from '../utils/supabase' 5 | import useStore from '../store' 6 | import { useMutateNote } from '../hooks/useMutateNote' 7 | import { Spinner } from './Spinner' 8 | import { Note } from '../types/types' 9 | 10 | export const NoteItem: VFC< 11 | Omit 12 | > = ({ id, title, content, user_id }) => { 13 | const [userId, setUserId] = useState('') 14 | const update = useStore((state) => state.updateEditedNote) 15 | const { deleteNoteMutation } = useMutateNote() 16 | useEffect(() => { 17 | setUserId(supabase.auth.user()?.id) 18 | }, []) 19 | if (deleteNoteMutation.isLoading) { 20 | return 21 | } 22 | return ( 23 |
  • 24 | 25 | {title} 26 | 27 | {userId === user_id && ( 28 |
    29 | { 32 | update({ 33 | id: id, 34 | title: title, 35 | content: content, 36 | }) 37 | }} 38 | /> 39 | { 42 | deleteNoteMutation.mutate(id) 43 | }} 44 | /> 45 |
    46 | )} 47 |
  • 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /components/CommentForm.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, VFC } from 'react' 2 | import { supabase } from '../utils/supabase' 3 | import useStore from '../store' 4 | import { useMutateComment } from '../hooks/useMutateComment' 5 | import { Spinner } from './Spinner' 6 | 7 | export const CommentForm: VFC<{ noteId: string }> = ({ noteId }) => { 8 | const { editedComment } = useStore() 9 | const update = useStore((state) => state.updateEditedComment) 10 | const { createCommentMutation, updateCommentMutation } = useMutateComment() 11 | const submitHandler = (e: FormEvent) => { 12 | e.preventDefault() 13 | if (editedComment.id === '') { 14 | createCommentMutation.mutate({ 15 | content: editedComment.content, 16 | user_id: supabase.auth.user()?.id, 17 | note_id: noteId, 18 | }) 19 | } else { 20 | updateCommentMutation.mutate({ 21 | id: editedComment.id, 22 | content: editedComment.content, 23 | }) 24 | } 25 | } 26 | if (updateCommentMutation.isLoading || createCommentMutation.isLoading) { 27 | return 28 | } 29 | return ( 30 |
    31 | update({ ...editedComment, content: e.target.value })} 37 | /> 38 | 44 |
    45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /hooks/useMutateNote.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from 'react-query' 2 | import { supabase } from '../utils/supabase' 3 | import useStore from '../store' 4 | import { revalidateList, revalidateSingle } from '../utils/revalidation' 5 | import { Note, EditedNote } from '../types/types' 6 | 7 | export const useMutateNote = () => { 8 | const reset = useStore((state) => state.resetEditedNote) 9 | const createNoteMutation = useMutation( 10 | async (note: Omit) => { 11 | const { data, error } = await supabase.from('notes').insert(note) 12 | if (error) throw new Error(error.message) 13 | return data 14 | }, 15 | { 16 | onSuccess: () => { 17 | //revalidateList() 18 | reset() 19 | alert('Successfully completed !!') 20 | }, 21 | onError: (err: any) => { 22 | alert(err.message) 23 | reset() 24 | }, 25 | } 26 | ) 27 | const updateNoteMutation = useMutation( 28 | async (note: EditedNote) => { 29 | const { data, error } = await supabase 30 | .from('notes') 31 | .update({ title: note.title, content: note.content }) 32 | .eq('id', note.id) 33 | if (error) throw new Error(error.message) 34 | return data 35 | }, 36 | { 37 | onSuccess: (res) => { 38 | //revalidateList() 39 | revalidateSingle(res[0].id) 40 | reset() 41 | alert('Successfully completed !!') 42 | }, 43 | onError: (err: any) => { 44 | alert(err.message) 45 | reset() 46 | }, 47 | } 48 | ) 49 | const deleteNoteMutation = useMutation( 50 | async (id: string) => { 51 | const { data, error } = await supabase.from('notes').delete().eq('id', id) 52 | if (error) throw new Error(error.message) 53 | return data 54 | }, 55 | { 56 | onSuccess: () => { 57 | //revalidateList() 58 | reset() 59 | alert('Successfully completed !!') 60 | }, 61 | onError: (err: any) => { 62 | alert(err.message) 63 | reset() 64 | }, 65 | } 66 | ) 67 | return { deleteNoteMutation, createNoteMutation, updateNoteMutation } 68 | } 69 | -------------------------------------------------------------------------------- /pages/note/[id].tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import { GetStaticProps, GetStaticPaths } from 'next' 3 | import Link from 'next/link' 4 | import { ChevronDoubleLeftIcon } from '@heroicons/react/solid' 5 | import { supabase } from '../../utils/supabase' 6 | import { Layout } from '../../components/Layout' 7 | import { CommentForm } from '../../components/CommentForm' 8 | import { CommentItem } from '../../components/CommentItem' 9 | import { Note } from '../../types/types' 10 | 11 | const getAllNoteIds = async () => { 12 | const { data: ids } = await supabase.from('notes').select('id') 13 | return ids!.map((id) => { 14 | return { 15 | params: { 16 | id: String(id.id), 17 | }, 18 | } 19 | }) 20 | } 21 | export const getStaticPaths: GetStaticPaths = async () => { 22 | const paths = await getAllNoteIds() 23 | return { 24 | paths, 25 | fallback: 'blocking', 26 | } 27 | } 28 | export const getStaticProps: GetStaticProps = async (ctx) => { 29 | console.log('ISR invoked - detail page') 30 | const { data: note } = await supabase 31 | .from('notes') 32 | .select('*, comments(*)') 33 | .eq('id', ctx.params?.id) 34 | .single() 35 | return { 36 | props: { 37 | note, 38 | }, 39 | revalidate: false, 40 | } 41 | } 42 | type StaticProps = { 43 | note: Note 44 | } 45 | const NotePage: NextPage = ({ note }) => { 46 | return ( 47 | 48 |

    {note.title}

    49 |
    50 |

    {note.content}

    51 |
    52 |
      53 | {note.comments?.map((comment) => ( 54 | 60 | ))} 61 |
    62 | 63 | 64 | 65 | 66 |
    67 | ) 68 | } 69 | 70 | export default NotePage 71 | -------------------------------------------------------------------------------- /hooks/useMutateComment.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from 'react-query' 2 | import { supabase } from '../utils/supabase' 3 | import useStore from '../store' 4 | import { revalidateSingle } from '../utils/revalidation' 5 | import { Comment, EditedComment } from '../types/types' 6 | 7 | export const useMutateComment = () => { 8 | const reset = useStore((state) => state.resetEditedComment) 9 | const createCommentMutation = useMutation( 10 | async (comment: Omit) => { 11 | const { data, error } = await supabase.from('comments').insert(comment) 12 | if (error) throw new Error(error.message) 13 | return data 14 | }, 15 | { 16 | onSuccess: (res) => { 17 | revalidateSingle(res[0].note_id) 18 | reset() 19 | alert('Successfully completed !!') 20 | }, 21 | onError: (err: any) => { 22 | alert(err.message) 23 | reset() 24 | }, 25 | } 26 | ) 27 | const updateCommentMutation = useMutation( 28 | async (comment: EditedComment) => { 29 | const { data, error } = await supabase 30 | .from('comments') 31 | .update({ content: comment.content }) 32 | .eq('id', comment.id) 33 | if (error) throw new Error(error.message) 34 | return data 35 | }, 36 | { 37 | onSuccess: (res) => { 38 | revalidateSingle(res[0].note_id) 39 | reset() 40 | alert('Successfully completed !!') 41 | }, 42 | onError: (err: any) => { 43 | alert(err.message) 44 | reset() 45 | }, 46 | } 47 | ) 48 | const deleteCommentMutation = useMutation( 49 | async (id: string) => { 50 | const { data, error } = await supabase 51 | .from('comments') 52 | .delete() 53 | .eq('id', id) 54 | if (error) throw new Error(error.message) 55 | return data 56 | }, 57 | { 58 | onSuccess: (res) => { 59 | revalidateSingle(res[0].note_id) 60 | reset() 61 | alert('Successfully completed !!') 62 | }, 63 | onError: (err: any) => { 64 | alert(err.message) 65 | reset() 66 | }, 67 | } 68 | ) 69 | return { 70 | deleteCommentMutation, 71 | createCommentMutation, 72 | updateCommentMutation, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /components/NoteForm.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, VFC } from 'react' 2 | import { supabase } from '../utils/supabase' 3 | import useStore from '../store' 4 | import { useMutateNote } from '../hooks/useMutateNote' 5 | import { Spinner } from './Spinner' 6 | 7 | export const NoteForm: VFC = () => { 8 | const { editedNote } = useStore() 9 | const update = useStore((state) => state.updateEditedNote) 10 | const { createNoteMutation, updateNoteMutation } = useMutateNote() 11 | 12 | const submitHandler = (e: FormEvent) => { 13 | e.preventDefault() 14 | if (editedNote.id === '') 15 | createNoteMutation.mutate({ 16 | title: editedNote.title, 17 | content: editedNote.content, 18 | user_id: supabase.auth.user()?.id, 19 | }) 20 | else { 21 | updateNoteMutation.mutate({ 22 | id: editedNote.id, 23 | title: editedNote.title, 24 | content: editedNote.content, 25 | }) 26 | } 27 | } 28 | if (updateNoteMutation.isLoading || createNoteMutation.isLoading) { 29 | return 30 | } 31 | return ( 32 |
    33 |
    34 | update({ ...editedNote, title: e.target.value })} 40 | /> 41 |
    42 |
    43 |