├── .env.local.template ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── app ├── auth │ ├── layout.tsx │ ├── page.tsx │ └── todo-crud │ │ ├── [todoId] │ │ ├── loading.tsx │ │ ├── not-found.tsx │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx ├── blogs │ ├── [blogId] │ │ ├── not-found.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── components │ ├── auth.tsx │ ├── blog-list-static.tsx │ ├── blog-list.tsx │ ├── counter.tsx │ ├── nav-bar.tsx │ ├── news-list.tsx │ ├── notes-list.tsx │ ├── refresh-btn.tsx │ ├── router-btn.tsx │ ├── spinner.tsx │ ├── supabase-listener.tsx │ ├── timer-counter.tsx │ ├── todo-edit.tsx │ ├── todo-item.tsx │ └── todo-list.tsx ├── error.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── loading.tsx ├── nested-layout │ ├── layout.tsx │ ├── page.tsx │ └── second │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── third │ │ ├── fourth │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx ├── page.tsx └── streaming-sr │ └── page.tsx ├── database.types.ts ├── demo.code-workspace ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── store └── index.ts ├── supabase ├── .gitignore ├── config.toml ├── functions │ └── .vscode │ │ ├── extensions.json │ │ └── settings.json └── seed.sql ├── tailwind.config.js ├── tsconfig.json └── utils └── supabase.ts /.env.local.template: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_ANON_KEY=yours 2 | NEXT_PUBLIC_SUPABASE_URL=yours 3 | apikey=yours 4 | url=yours -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Create new Next.js project 2 | ```bash 3 | # npx create-next-app --example with-tailwindcss rsc-supabase --use-npm 4 | npx create-next-app@13.4.1 --tailwind rsc-supabase --use-npm 5 | npm i @heroicons/react@2.0.17 @supabase/auth-helpers-nextjs@0.6.1 @supabase/supabase-js@2.21.0 zustand@4.3.8 supabase@1.55.1 date-fns@2.30.0 6 | npm i next@13.4.1 7 | ``` 8 | ### Generate supabase types 9 | ```bash 10 | npx supabase login 11 | npx supabase init 12 | npx supabase link --project-ref your_project_id 13 | npx supabase gen types typescript --linked > database.types.ts 14 | ``` -------------------------------------------------------------------------------- /app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import { headers, cookies } from 'next/headers' 2 | import SupabaseListener from '../components/supabase-listener' 3 | import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs' 4 | import type { Database } from '../../database.types' 5 | 6 | export default async function AuthLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | const supabase = createServerComponentSupabaseClient({ 12 | headers, 13 | cookies, 14 | }) 15 | const { 16 | data: { session }, 17 | } = await supabase.auth.getSession() 18 | return ( 19 | <> 20 | 21 | {children} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/auth/page.tsx: -------------------------------------------------------------------------------- 1 | import Auth from '../components/auth' 2 | 3 | export default async function AuthPage() { 4 | return ( 5 |
8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/auth/todo-crud/[todoId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from '../../../components/spinner' 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /app/auth/todo-crud/[todoId]/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return
Task Detail Not Found
3 | } 4 | -------------------------------------------------------------------------------- /app/auth/todo-crud/[todoId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | import { headers, cookies } from 'next/headers' 3 | import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs' 4 | import { format } from 'date-fns' 5 | import type { Database } from '../../../../database.types' 6 | 7 | type PageProps = { 8 | params: { 9 | todoId: string 10 | } 11 | } 12 | 13 | export default async function TodoDetailPage({ params }: PageProps) { 14 | const supabase = createServerComponentSupabaseClient({ 15 | headers, 16 | cookies, 17 | }) 18 | const { data: todo, error } = await supabase 19 | .from('todos') 20 | .select('*') 21 | .eq('id', params.todoId) 22 | .single() 23 | if (!todo) return notFound() 24 | return ( 25 |
26 |

Task ID: {todo.id}

27 |

Title: {todo.title}

28 |

Status: {todo.completed ? 'done' : 'not yet'}

29 |

30 | Created at:{' '} 31 | {todo && format(new Date(todo.created_at), 'yyyy-MM-dd HH:mm:ss')} 32 |

33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/auth/todo-crud/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import Spinner from '../../components/spinner' 3 | import EditTask from '../../components/todo-edit' 4 | import TodoList from '../../components/todo-list' 5 | 6 | export default async function TodoLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | return ( 12 |
13 | 20 |
{children}
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/auth/todo-crud/page.tsx: -------------------------------------------------------------------------------- 1 | export default function TodoPage() { 2 | return ( 3 |
4 | 5 | Click a title on the left to view detail 🚀 6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /app/blogs/[blogId]/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return
Blog Detail Not Found
3 | } 4 | -------------------------------------------------------------------------------- /app/blogs/[blogId]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { notFound } from 'next/navigation' 3 | import { format } from 'date-fns' 4 | import { ArrowUturnLeftIcon } from '@heroicons/react/24/solid' 5 | import type { Database } from '../../../database.types' 6 | 7 | type Blog = Database['public']['Tables']['blogs']['Row'] 8 | 9 | type PageProps = { 10 | params: { 11 | blogId: string 12 | } 13 | } 14 | 15 | async function fetchBlog(blogId: string) { 16 | const res = await fetch( 17 | `${process.env.url}/rest/v1/blogs?id=eq.${blogId}&select=*`, 18 | { 19 | headers: new Headers({ 20 | apikey: process.env.apikey as string, 21 | }), 22 | //cache: 'no-store', 23 | cache: 'force-cache', 24 | } 25 | ) 26 | // if (!res.ok) { 27 | // throw new Error('Failed to fetch data in server') 28 | // } 29 | const blogs: Blog[] = await res.json() 30 | return blogs[0] 31 | } 32 | 33 | export default async function BlogDetailPage({ params }: PageProps) { 34 | const blog = await fetchBlog(params.blogId) 35 | if (!blog) return notFound() 36 | return ( 37 |
38 |

39 | Task ID: {blog.id} 40 |

41 |

42 | Title: {blog.title} 43 |

44 |

45 | Content: {blog.content} 46 |

47 |

48 | Created at: 49 | {blog && format(new Date(blog.created_at), 'yyyy-MM-dd HH:mm:ss')} 50 |

51 | 52 | 53 | 54 |
55 | ) 56 | } 57 | export async function generateStaticParams() { 58 | const res = await fetch(`${process.env.url}/rest/v1/blogs?select=*`, { 59 | headers: new Headers({ 60 | apikey: process.env.apikey as string, 61 | }), 62 | }) 63 | const blogs: Blog[] = await res.json() 64 | 65 | return blogs.map((blog) => ({ 66 | blogId: blog.id.toString(), 67 | })) 68 | } 69 | -------------------------------------------------------------------------------- /app/blogs/layout.tsx: -------------------------------------------------------------------------------- 1 | import BlogListStatic from '../components/blog-list-static' 2 | import RefreshBtn from '../components/refresh-btn' 3 | 4 | export default function BlogLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return ( 10 |
11 | 18 |
{children}
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/blogs/page.tsx: -------------------------------------------------------------------------------- 1 | import RouterBtn from '../components/router-btn' 2 | 3 | export default function BlogPage() { 4 | return ( 5 |
6 | 7 | Click a title on the left to view detail 🚀 8 | 9 |
10 | 11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/components/auth.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState, FormEvent } from 'react' 3 | import { useRouter } from 'next/navigation' 4 | import { ArrowRightOnRectangleIcon } from '@heroicons/react/24/solid' 5 | import supabase from '../../utils/supabase' 6 | import useStore from '../../store' 7 | 8 | export default function Auth() { 9 | const { loginUser } = useStore() 10 | const [isLogin, setIsLogin] = useState(true) 11 | const [email, setEmail] = useState('') 12 | const [password, setPassword] = useState('') 13 | const router = useRouter() 14 | async function handleSubmit(e: FormEvent) { 15 | e.preventDefault() 16 | if (isLogin) { 17 | const { error } = await supabase.auth.signInWithPassword({ 18 | email, 19 | password, 20 | }) 21 | setEmail('') 22 | setPassword('') 23 | if (error) { 24 | alert(error.message) 25 | } else { 26 | router.push('/auth/todo-crud') 27 | } 28 | } else { 29 | const { error } = await supabase.auth.signUp({ 30 | email, 31 | password, 32 | }) 33 | setEmail('') 34 | setPassword('') 35 | if (error) { 36 | alert(error.message) 37 | } 38 | } 39 | } 40 | function signOut() { 41 | supabase.auth.signOut() 42 | } 43 | return ( 44 |
45 |

{loginUser.email}

46 | 50 |
51 |
52 | { 59 | setEmail(e.target.value) 60 | }} 61 | /> 62 |
63 |
64 | { 71 | setPassword(e.target.value) 72 | }} 73 | /> 74 |
75 |
76 | 82 |
83 |
84 |

setIsLogin(!isLogin)} 86 | className="cursor-pointer font-medium hover:text-indigo-500" 87 | > 88 | change mode ? 89 |

90 |
91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /app/components/blog-list-static.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import type { Database } from '../../database.types' 3 | 4 | type Blog = Database['public']['Tables']['blogs']['Row'] 5 | 6 | async function fetchBlogs() { 7 | const res = await fetch(`${process.env.url}/rest/v1/blogs?select=*`, { 8 | headers: new Headers({ 9 | apikey: process.env.apikey as string, 10 | }), 11 | //cache: 'no-store', 12 | cache: 'force-cache', 13 | }) 14 | if (!res.ok) { 15 | throw new Error('Failed to fetch data in server') 16 | } 17 | const blogs: Blog[] = await res.json() 18 | return blogs 19 | } 20 | 21 | export default async function BlogListStatic() { 22 | const blogs = await fetchBlogs() 23 | return ( 24 |
25 |

26 | Blogs 27 |

28 |
    29 | {blogs?.map((blog) => ( 30 |
  • 31 | 32 | {blog.title} 33 | 34 |
  • 35 | ))} 36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/components/blog-list.tsx: -------------------------------------------------------------------------------- 1 | import type { Database } from '../../database.types' 2 | type Blog = Database['public']['Tables']['blogs']['Row'] 3 | 4 | const fetchBlogs = async () => { 5 | await new Promise((resolve) => setTimeout(resolve, 6000)) 6 | const res = await fetch(`${process.env.url}/rest/v1/blogs?select=*`, { 7 | headers: new Headers({ 8 | apikey: process.env.apikey as string, 9 | }), 10 | }) 11 | if (!res.ok) { 12 | throw new Error('Failed to fetch data') 13 | } 14 | const blogs: Blog[] = await res.json() 15 | return blogs 16 | } 17 | export default async function BlogList() { 18 | const blogs = await fetchBlogs() 19 | return ( 20 |
21 |

22 | Blogs 23 |

24 |
    25 | {blogs?.map((blog) => ( 26 |
  • 27 | {blog.title} 28 |
  • 29 | ))} 30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/components/counter.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState } from 'react' 3 | 4 | export default function Counter() { 5 | const [count, setCount] = useState(0) 6 | return ( 7 |
8 |

{count}

9 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /app/components/nav-bar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function NavBar() { 4 | return ( 5 |
6 | 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /app/components/news-list.tsx: -------------------------------------------------------------------------------- 1 | import type { Database } from '../../database.types' 2 | import Counter from './counter' 3 | 4 | type News = Database['public']['Tables']['news']['Row'] 5 | 6 | async function fetchNews() { 7 | await new Promise((resolve) => setTimeout(resolve, 2000)) 8 | const res = await fetch(`${process.env.url}/rest/v1/news?select=*`, { 9 | headers: new Headers({ 10 | apikey: process.env.apikey as string, 11 | }), 12 | }) 13 | if (!res.ok) { 14 | throw new Error('Failed to fetch data in server') 15 | } 16 | const news: News[] = await res.json() 17 | return news 18 | } 19 | 20 | export default async function NewsList() { 21 | const news = await fetchNews() 22 | return ( 23 |
24 | 25 |

26 | News 27 |

28 |
    29 | {news.map((news) => ( 30 |
  • 31 |

    {news.title}

    32 |
  • 33 | ))} 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /app/components/notes-list.tsx: -------------------------------------------------------------------------------- 1 | import type { Database } from '../../database.types' 2 | import { format } from 'date-fns' 3 | 4 | type Note = Database['public']['Tables']['notes']['Row'] 5 | 6 | async function fetchNotes() { 7 | await new Promise((resolve) => setTimeout(resolve, 2000)) 8 | const res = await fetch(`${process.env.url}/rest/v1/notes?select=*`, { 9 | headers: new Headers({ 10 | apikey: process.env.apikey as string, 11 | }), 12 | cache: 'no-store', 13 | //next: { revalidate: 10 }, 14 | }) 15 | if (!res.ok) { 16 | throw new Error('Failed to fetch data in server') 17 | } 18 | const notes: Note[] = await res.json() 19 | return notes 20 | } 21 | 22 | export default async function NotesList() { 23 | const notes = await fetchNotes() 24 | return ( 25 |
26 |

27 | Notes 28 |

29 |
    30 | {notes.map((note) => ( 31 |
  • 32 |

    {note.title}

    33 |

    34 | Created at: 35 | {note && format(new Date(note.created_at), 'yyyy-MM-dd HH:mm:ss')} 36 |

    37 |
  • 38 | ))} 39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/components/refresh-btn.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useRouter } from 'next/navigation' 3 | 4 | export default function RefreshBtn() { 5 | const router = useRouter() 6 | return ( 7 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/components/router-btn.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useRouter } from 'next/navigation' 3 | 4 | export default function RouterBtn({ 5 | destination = '', 6 | }: { 7 | destination?: string 8 | }) { 9 | const router = useRouter() 10 | return ( 11 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/components/spinner.tsx: -------------------------------------------------------------------------------- 1 | export default function Spinner({ 2 | color = 'border-blue-500', 3 | }: { 4 | color?: string 5 | }) { 6 | return ( 7 |
8 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/components/supabase-listener.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useEffect } from 'react' 3 | import { useRouter } from 'next/navigation' 4 | import supabase from '../../utils/supabase' 5 | import useStore from '../../store' 6 | 7 | export default function SupabaseListener({ 8 | accessToken, 9 | }: { 10 | accessToken?: string 11 | }) { 12 | const router = useRouter() 13 | const { updateLoginUser } = useStore() 14 | useEffect(() => { 15 | const getUserInfo = async () => { 16 | const { data } = await supabase.auth.getSession() 17 | if (data.session) { 18 | updateLoginUser({ 19 | id: data.session?.user.id, 20 | email: data.session?.user.email!, 21 | }) 22 | } 23 | } 24 | getUserInfo() 25 | supabase.auth.onAuthStateChange((_, session) => { 26 | updateLoginUser({ id: session?.user.id, email: session?.user.email! }) 27 | if (session?.access_token !== accessToken) { 28 | router.refresh() 29 | } 30 | }) 31 | }, [accessToken]) 32 | return null 33 | } 34 | -------------------------------------------------------------------------------- /app/components/timer-counter.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState, useEffect } from 'react' 3 | 4 | export default function TimerCounter() { 5 | const [count, setCount] = useState(0) 6 | useEffect(() => { 7 | const timer = setInterval(() => setCount((prevCount) => prevCount + 1), 500) 8 | return () => clearInterval(timer) 9 | }, []) 10 | return ( 11 |
12 |

{count}

13 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/components/todo-edit.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { FormEvent } from 'react' 3 | import { useRouter } from 'next/navigation' 4 | import { ArrowRightOnRectangleIcon } from '@heroicons/react/24/solid' 5 | import useStore from '../../store' 6 | import supabase from '../../utils/supabase' 7 | 8 | export default function EditTask() { 9 | const router = useRouter() 10 | const { editedTask } = useStore() 11 | const { loginUser } = useStore() 12 | const updateTask = useStore((state) => state.updateEditedTask) 13 | const reset = useStore((state) => state.resetEditedTask) 14 | 15 | function signOut() { 16 | supabase.auth.signOut() 17 | router.push('/auth') 18 | } 19 | async function submitHandler(e: FormEvent) { 20 | e.preventDefault() 21 | if (editedTask.id === '') { 22 | const { error } = await supabase 23 | .from('todos') 24 | .insert({ title: editedTask.title, user_id: loginUser.id }) 25 | router.refresh() 26 | reset() 27 | } else { 28 | const { error } = await supabase 29 | .from('todos') 30 | .update({ title: editedTask.title }) 31 | .eq('id', editedTask.id) 32 | router.refresh() 33 | reset() 34 | } 35 | } 36 | 37 | return ( 38 |
39 |

{loginUser.email}

40 |
41 | 45 |
46 |
47 | updateTask({ ...editedTask, title: e.target.value })} 53 | /> 54 | 60 |
61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/components/todo-item.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from 'next/link' 3 | import { useRouter } from 'next/navigation' 4 | import { TrashIcon, PencilIcon } from '@heroicons/react/24/solid' 5 | import supabase from '../../utils/supabase' 6 | import useStore from '../../store' 7 | import type { Database } from '../../database.types' 8 | 9 | type Todo = Database['public']['Tables']['todos']['Row'] 10 | 11 | export default function TodoItem(todo: Todo) { 12 | const router = useRouter() 13 | const updateTask = useStore((state) => state.updateEditedTask) 14 | const resetTask = useStore((state) => state.resetEditedTask) 15 | async function updateMutate(id: string, completed: boolean) { 16 | await supabase.from('todos').update({ completed: completed }).eq('id', id) 17 | resetTask() 18 | router.refresh() 19 | } 20 | async function deleteMutate(id: string) { 21 | await supabase.from('todos').delete().eq('id', id) 22 | router.refresh() 23 | } 24 | return ( 25 |
  • 26 | updateMutate(todo.id, !todo.completed)} 31 | /> 32 | {todo.title} 33 |
    34 | { 37 | updateTask({ 38 | id: todo.id, 39 | title: todo.title, 40 | }) 41 | }} 42 | /> 43 | { 46 | deleteMutate(todo.id) 47 | }} 48 | /> 49 |
    50 |
  • 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /app/components/todo-list.tsx: -------------------------------------------------------------------------------- 1 | import { headers, cookies } from 'next/headers' 2 | import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs' 3 | import type { Database } from '../../database.types' 4 | import TodoItem from './todo-item' 5 | 6 | export default async function TodoList() { 7 | const supabase = createServerComponentSupabaseClient({ 8 | headers, 9 | cookies, 10 | }) 11 | const { data: todos } = await supabase 12 | .from('todos') 13 | .select() 14 | .order('created_at', { ascending: true }) 15 | return ( 16 |
      17 | {todos?.map((todo) => ( 18 | 19 | ))} 20 |
    21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export default function Error({ error }: { error: Error }) { 4 | return ( 5 |
    6 |

    7 | Data fetching in server failed 8 |

    9 |
    10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GomaGoma676/nextjs-app-router-supabase/9e55104503a936dbf380c406bdb099a6bfa85fe2/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import NavBar from './components/nav-bar' 2 | import './globals.css' 3 | 4 | export const metadata = { 5 | title: 'Nextjs App', 6 | description: 'Generated by create next app', 7 | } 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode 13 | }) { 14 | return ( 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from './components/spinner' 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /app/nested-layout/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function FirstLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 |
    8 |

    Layout 1

    9 | {children} 10 |
    11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/nested-layout/page.tsx: -------------------------------------------------------------------------------- 1 | export default function FisrtPage() { 2 | return ( 3 |
    4 |

    Page 1

    5 |
    6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /app/nested-layout/second/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function SecondLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 | <> 8 |

    Layout 2

    9 | {children} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/nested-layout/second/page.tsx: -------------------------------------------------------------------------------- 1 | export default function SecondPage() { 2 | return ( 3 |
    4 |

    Page 2

    5 |
    6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /app/nested-layout/second/third/fourth/page.tsx: -------------------------------------------------------------------------------- 1 | export default function FourthPage() { 2 | return ( 3 |
    4 |

    Page 4

    5 |
    6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /app/nested-layout/second/third/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function FirstLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 |
    8 |

    Layout 1

    9 | {children} 10 |
    11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/nested-layout/second/third/page.tsx: -------------------------------------------------------------------------------- 1 | export default function FisrtPage() { 2 | return ( 3 |
    4 |

    Page 1

    5 |
    6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import NotesList from './components/notes-list' 2 | import TimerCounter from './components/timer-counter' 3 | import { Suspense } from 'react' 4 | import Spinner from './components/spinner' 5 | import RefreshBtn from './components/refresh-btn' 6 | 7 | export default function Page() { 8 | return ( 9 |
    10 |
    11 |

    Hello World🚀

    12 | }> 13 | {/* @ts-expect-error Async Server Component */} 14 | 15 | 16 | 17 | 18 |
    19 |
    20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/streaming-sr/page.tsx: -------------------------------------------------------------------------------- 1 | export const revalidate = 0 2 | 3 | import { Suspense } from 'react' 4 | import BlogList from '../components/blog-list' 5 | import NewsList from '../components/news-list' 6 | import Spinner from '../components/spinner' 7 | 8 | export default function StreamingServerRenderingPage() { 9 | return ( 10 |
    11 | 19 |
    20 |
    21 | }> 22 | {/* @ts-expect-error Async Server Component */} 23 | 24 | 25 |
    26 |
    27 |
    28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /database.types.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json } 7 | | Json[] 8 | 9 | export interface Database { 10 | graphql_public: { 11 | Tables: { 12 | [_ in never]: never 13 | } 14 | Views: { 15 | [_ in never]: never 16 | } 17 | Functions: { 18 | graphql: { 19 | Args: { 20 | operationName?: string 21 | query?: string 22 | variables?: Json 23 | extensions?: Json 24 | } 25 | Returns: Json 26 | } 27 | } 28 | Enums: { 29 | [_ in never]: never 30 | } 31 | CompositeTypes: { 32 | [_ in never]: never 33 | } 34 | } 35 | public: { 36 | Tables: { 37 | blogs: { 38 | Row: { 39 | content: string | null 40 | created_at: string 41 | id: string 42 | title: string | null 43 | } 44 | Insert: { 45 | content?: string | null 46 | created_at?: string 47 | id?: string 48 | title?: string | null 49 | } 50 | Update: { 51 | content?: string | null 52 | created_at?: string 53 | id?: string 54 | title?: string | null 55 | } 56 | } 57 | news: { 58 | Row: { 59 | content: string | null 60 | created_at: string 61 | id: string 62 | title: string | null 63 | } 64 | Insert: { 65 | content?: string | null 66 | created_at?: string 67 | id?: string 68 | title?: string | null 69 | } 70 | Update: { 71 | content?: string | null 72 | created_at?: string 73 | id?: string 74 | title?: string | null 75 | } 76 | } 77 | notes: { 78 | Row: { 79 | created_at: string 80 | id: string 81 | title: string | null 82 | } 83 | Insert: { 84 | created_at?: string 85 | id?: string 86 | title?: string | null 87 | } 88 | Update: { 89 | created_at?: string 90 | id?: string 91 | title?: string | null 92 | } 93 | } 94 | todos: { 95 | Row: { 96 | completed: boolean 97 | created_at: string 98 | id: string 99 | title: string | null 100 | user_id: string | null 101 | } 102 | Insert: { 103 | completed?: boolean 104 | created_at?: string 105 | id?: string 106 | title?: string | null 107 | user_id?: string | null 108 | } 109 | Update: { 110 | completed?: boolean 111 | created_at?: string 112 | id?: string 113 | title?: string | null 114 | user_id?: string | null 115 | } 116 | } 117 | } 118 | Views: { 119 | [_ in never]: never 120 | } 121 | Functions: { 122 | [_ in never]: never 123 | } 124 | Enums: { 125 | [_ in never]: never 126 | } 127 | CompositeTypes: { 128 | [_ in never]: never 129 | } 130 | } 131 | storage: { 132 | Tables: { 133 | buckets: { 134 | Row: { 135 | allowed_mime_types: string[] | null 136 | avif_autodetection: boolean | null 137 | created_at: string | null 138 | file_size_limit: number | null 139 | id: string 140 | name: string 141 | owner: string | null 142 | public: boolean | null 143 | updated_at: string | null 144 | } 145 | Insert: { 146 | allowed_mime_types?: string[] | null 147 | avif_autodetection?: boolean | null 148 | created_at?: string | null 149 | file_size_limit?: number | null 150 | id: string 151 | name: string 152 | owner?: string | null 153 | public?: boolean | null 154 | updated_at?: string | null 155 | } 156 | Update: { 157 | allowed_mime_types?: string[] | null 158 | avif_autodetection?: boolean | null 159 | created_at?: string | null 160 | file_size_limit?: number | null 161 | id?: string 162 | name?: string 163 | owner?: string | null 164 | public?: boolean | null 165 | updated_at?: string | null 166 | } 167 | } 168 | migrations: { 169 | Row: { 170 | executed_at: string | null 171 | hash: string 172 | id: number 173 | name: string 174 | } 175 | Insert: { 176 | executed_at?: string | null 177 | hash: string 178 | id: number 179 | name: string 180 | } 181 | Update: { 182 | executed_at?: string | null 183 | hash?: string 184 | id?: number 185 | name?: string 186 | } 187 | } 188 | objects: { 189 | Row: { 190 | bucket_id: string | null 191 | created_at: string | null 192 | id: string 193 | last_accessed_at: string | null 194 | metadata: Json | null 195 | name: string | null 196 | owner: string | null 197 | path_tokens: string[] | null 198 | updated_at: string | null 199 | version: string | null 200 | } 201 | Insert: { 202 | bucket_id?: string | null 203 | created_at?: string | null 204 | id?: string 205 | last_accessed_at?: string | null 206 | metadata?: Json | null 207 | name?: string | null 208 | owner?: string | null 209 | path_tokens?: string[] | null 210 | updated_at?: string | null 211 | version?: string | null 212 | } 213 | Update: { 214 | bucket_id?: string | null 215 | created_at?: string | null 216 | id?: string 217 | last_accessed_at?: string | null 218 | metadata?: Json | null 219 | name?: string | null 220 | owner?: string | null 221 | path_tokens?: string[] | null 222 | updated_at?: string | null 223 | version?: string | null 224 | } 225 | } 226 | } 227 | Views: { 228 | [_ in never]: never 229 | } 230 | Functions: { 231 | can_insert_object: { 232 | Args: { 233 | bucketid: string 234 | name: string 235 | owner: string 236 | metadata: Json 237 | } 238 | Returns: undefined 239 | } 240 | extension: { 241 | Args: { 242 | name: string 243 | } 244 | Returns: string 245 | } 246 | filename: { 247 | Args: { 248 | name: string 249 | } 250 | Returns: string 251 | } 252 | foldername: { 253 | Args: { 254 | name: string 255 | } 256 | Returns: unknown 257 | } 258 | get_size_by_bucket: { 259 | Args: Record 260 | Returns: { 261 | size: number 262 | bucket_id: string 263 | }[] 264 | } 265 | search: { 266 | Args: { 267 | prefix: string 268 | bucketname: string 269 | limits?: number 270 | levels?: number 271 | offsets?: number 272 | search?: string 273 | sortcolumn?: string 274 | sortorder?: string 275 | } 276 | Returns: { 277 | name: string 278 | id: string 279 | updated_at: string 280 | created_at: string 281 | last_accessed_at: string 282 | metadata: Json 283 | }[] 284 | } 285 | } 286 | Enums: { 287 | [_ in never]: never 288 | } 289 | CompositeTypes: { 290 | [_ in never]: never 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /demo.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "project-root", 5 | "path": "./" 6 | }, 7 | { 8 | "name": "supabase-functions", 9 | "path": "supabase/functions" 10 | } 11 | ], 12 | "settings": { 13 | "files.exclude": { 14 | "supabase/functions/": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import type { NextRequest } from 'next/server' 3 | import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs' 4 | 5 | export async function middleware(req: NextRequest) { 6 | const res = NextResponse.next() 7 | const supabase = createMiddlewareSupabaseClient({ req, res }) 8 | const { 9 | data: { session }, 10 | } = await supabase.auth.getSession() 11 | if (!session && req.nextUrl.pathname.startsWith('/auth/todo-crud')) { 12 | const redirectUrl = req.nextUrl.clone() 13 | redirectUrl.pathname = '/auth' 14 | return NextResponse.redirect(redirectUrl) 15 | } 16 | return res 17 | } 18 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverActions: true, 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 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": "^2.0.17", 13 | "@supabase/auth-helpers-nextjs": "^0.6.1", 14 | "@supabase/supabase-js": "^2.21.0", 15 | "@types/node": "20.1.0", 16 | "@types/react": "18.2.6", 17 | "@types/react-dom": "18.2.4", 18 | "autoprefixer": "10.4.14", 19 | "date-fns": "^2.30.0", 20 | "eslint": "8.40.0", 21 | "eslint-config-next": "13.4.1", 22 | "next": "13.4.1", 23 | "postcss": "8.4.23", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "supabase": "^1.55.1", 27 | "tailwindcss": "3.3.2", 28 | "typescript": "5.0.4", 29 | "zustand": "^4.3.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | type EditedTask = { 4 | id: string 5 | title: string | null 6 | } 7 | type LoginUser = { 8 | id: string | undefined 9 | email: string | undefined 10 | } 11 | type State = { 12 | editedTask: EditedTask 13 | updateEditedTask: (payload: EditedTask) => void 14 | resetEditedTask: () => void 15 | loginUser: LoginUser 16 | updateLoginUser: (payload: LoginUser) => void 17 | resetLoginUser: () => void 18 | } 19 | const useStore = create((set) => ({ 20 | editedTask: { id: '', title: '' }, 21 | updateEditedTask: (payload) => 22 | set({ 23 | editedTask: payload, 24 | }), 25 | resetEditedTask: () => set({ editedTask: { id: '', title: '' } }), 26 | loginUser: { id: '', email: '' }, 27 | updateLoginUser: (payload) => 28 | set({ 29 | loginUser: payload, 30 | }), 31 | resetLoginUser: () => set({ loginUser: { id: '', email: '' } }), 32 | })) 33 | export default useStore 34 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "demo" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 54321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = ["public", "storage", "graphql_public"] 11 | # Extra schemas to add to the search_path of every request. public is always included. 12 | extra_search_path = ["public", "extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 54322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 15 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 54323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 54324 33 | smtp_port = 54325 34 | pop3_port = 54326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, 63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 64 | [auth.external.apple] 65 | enabled = false 66 | client_id = "" 67 | secret = "" 68 | # Overrides the default auth redirectUrl. 69 | redirect_uri = "" 70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 71 | # or any other third-party OIDC providers. 72 | url = "" 73 | 74 | [analytics] 75 | enabled = false 76 | port = 54327 77 | vector_port = 54328 78 | # Setup BigQuery project to enable log viewer on local development stack. 79 | # See: https://logflare.app/guides/bigquery-setup 80 | gcp_project_id = "" 81 | gcp_project_number = "" 82 | gcp_jwt_path = "supabase/gcloud.json" 83 | -------------------------------------------------------------------------------- /supabase/functions/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /supabase/functions/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "editor.defaultFormatter": "denoland.vscode-deno" 5 | } 6 | -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GomaGoma676/nextjs-app-router-supabase/9e55104503a936dbf380c406bdb099a6bfa85fe2/supabase/seed.sql -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /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 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /utils/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' 2 | import { Database } from '../database.types' 3 | 4 | export default createBrowserSupabaseClient() 5 | --------------------------------------------------------------------------------