├── .eslintrc.json ├── .env.example ├── public ├── favicon.ico ├── vercel.svg └── next.svg ├── jsconfig.json ├── src ├── pages │ ├── signup.jsx │ ├── index.js │ ├── api │ │ └── hello.js │ ├── _document.js │ ├── _app.js │ ├── people │ │ ├── index.jsx │ │ ├── create.jsx │ │ └── [id] │ │ │ ├── index.jsx │ │ │ └── edit.jsx │ └── dashboard.jsx ├── services │ ├── deleteCompany.js │ ├── getCompanies.js │ ├── schema.js │ ├── createPerson.js │ ├── getPeople.js │ ├── securePage.js │ └── images.js ├── styles │ └── globals.css └── components │ ├── SupaImage.jsx │ ├── ImageInput.jsx │ ├── Login.jsx │ └── PersonForm.jsx ├── next.config.mjs └── .gitignore /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL= 2 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 3 | 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gangludev/supabase-nextjs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/signup.jsx: -------------------------------------------------------------------------------- 1 | import { Login } from "@/components/Login"; 2 | 3 | export default function SignUp() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/services/deleteCompany.js: -------------------------------------------------------------------------------- 1 | export async function deleteCompany(supabase, id) { 2 | await supabase.from("companies").delete().eq("id", id); 3 | } 4 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | export default nextConfig; 7 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "bootstrap/dist/css/bootstrap.min.css"; 2 | 3 | table, 4 | th, 5 | td { 6 | border: 1px solid black; 7 | border-collapse: collapse; 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import { Login } from "@/components/Login"; 2 | 3 | export default function Home() { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/services/getCompanies.js: -------------------------------------------------------------------------------- 1 | export async function getCompanies(supabase) { 2 | const { data: companies } = await supabase.from("companies").select("*"); 3 | return companies; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: "John Doe" }); 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/schema.js: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const PersonSchema = z.object({ 4 | name: z.string().min(1, "Name is required"), 5 | dob: z.date(), 6 | email: z.string().email("Email not correct formatted"), 7 | address: z.string().min(1, "Address is required"), 8 | phone: z.string().min(1, "Phone is required"), 9 | country_id: z.string().min(1, "Country is required"), 10 | picture: z.string().min(1, "Picture is required"), 11 | }); 12 | -------------------------------------------------------------------------------- /src/services/createPerson.js: -------------------------------------------------------------------------------- 1 | export async function createPerson(supabase, personData) { 2 | const { data, error } = await supabase 3 | .from("people") 4 | .insert([personData]) 5 | .select(); 6 | return data; 7 | } 8 | 9 | export async function updatePerson(supabase, id, personData) { 10 | const { data, error } = await supabase 11 | .from("people") 12 | .update(personData) 13 | .eq("id", id) 14 | .select(); 15 | return data; 16 | } 17 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/components/SupaImage.jsx: -------------------------------------------------------------------------------- 1 | import { getImageUrl } from "@/services/images"; 2 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export function SupaImage({ bucket, fileName }) { 6 | const supabase = useSupabaseClient(); 7 | const [imageUrl, setImageUrl] = useState(null); 8 | 9 | useEffect(() => { 10 | getImageUrl(supabase, bucket, fileName).then(setImageUrl); 11 | }, [supabase, bucket, fileName]); 12 | 13 | if (!imageUrl) { 14 | return null; 15 | } 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /src/services/getPeople.js: -------------------------------------------------------------------------------- 1 | export async function getPeople(supabase) { 2 | let { data: people, error } = await supabase 3 | .from("people") 4 | .select("*, countries(*)"); 5 | return people; 6 | } 7 | 8 | export async function getCountries(supabase) { 9 | let { data: countries, error } = await supabase.from("countries").select("*"); 10 | return countries; 11 | } 12 | 13 | export async function getPerson(supabase, id) { 14 | let { data: person, error } = await supabase 15 | .from("people") 16 | .select("*, countries(*)") 17 | .eq("id", id) 18 | .single(); 19 | return person; 20 | } 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/services/securePage.js: -------------------------------------------------------------------------------- 1 | // src/services/securePage.js 2 | import { useRouter } from "next/router"; 3 | import { useEffect } from "react"; 4 | import { useSessionContext } from "@supabase/auth-helpers-react"; 5 | 6 | export function securePage(Page) { 7 | return () => { 8 | const { isLoading, session } = useSessionContext(); 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | if (!isLoading && !session) { 13 | router.replace("/"); 14 | } 15 | }, [isLoading, session, router]); 16 | 17 | if (isLoading) { 18 | return null; 19 | } 20 | 21 | if (!session) { 22 | return null; 23 | } 24 | 25 | return ; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/services/images.js: -------------------------------------------------------------------------------- 1 | export async function getImageUrl(supabase, bucketName, filePath) { 2 | const { data, urlError } = await supabase.storage 3 | .from(bucketName) 4 | .getPublicUrl(filePath); 5 | 6 | if (urlError) { 7 | throw urlError; 8 | } 9 | 10 | return data.publicUrl; 11 | } 12 | 13 | export async function uploadImage(supabase, bucketName, file) { 14 | if (!file) { 15 | return; 16 | } 17 | const fileExt = file.name.split(".").pop(); 18 | const fileName = `${Math.random()}.${fileExt}`; 19 | const filePath = fileName; 20 | 21 | const { error } = await supabase.storage 22 | .from(bucketName) 23 | .upload(filePath, file); 24 | 25 | if (error) { 26 | throw error; 27 | } 28 | return filePath; 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs"; 3 | import { SessionContextProvider } from "@supabase/auth-helpers-react"; 4 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 5 | import { useEffect, useState } from "react"; 6 | 7 | const client = new QueryClient(); 8 | 9 | export default function MyApp({ Component, pageProps }) { 10 | const [supabaseClient] = useState(() => createPagesBrowserClient()); 11 | 12 | useEffect(() => { 13 | if (supabaseClient) { 14 | globalThis.supabase = supabaseClient; 15 | } 16 | }, [supabaseClient]); 17 | 18 | return ( 19 | 20 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ImageInput.jsx: -------------------------------------------------------------------------------- 1 | import { getImageUrl, uploadImage } from "@/services/images"; 2 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export function ImageInput({ bucket, value, onChange }) { 6 | const supabase = useSupabaseClient(); 7 | const [imageUrl, setImageUrl] = useState(null); 8 | 9 | useEffect(() => { 10 | if (value) { 11 | getImageUrl(supabase, bucket, value).then(setImageUrl); 12 | } 13 | }, [value]); 14 | 15 | const handleClear = () => { 16 | onChange(null); 17 | }; 18 | 19 | const handleSelectedImage = (event) => { 20 | uploadImage(supabase, bucket, event.target.files[0]).then(onChange); 21 | }; 22 | 23 | if (value) { 24 | return ( 25 |
26 | {imageUrl && } 27 | 28 |
29 | ); 30 | } 31 | 32 | return ; 33 | } 34 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/people/index.jsx: -------------------------------------------------------------------------------- 1 | import { SupaImage } from "@/components/SupaImage"; 2 | import { getPeople } from "@/services/getPeople"; 3 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 4 | import { useQuery } from "@tanstack/react-query"; 5 | import Link from "next/link"; 6 | 7 | export default function PeoplePage() { 8 | const supabase = useSupabaseClient(); 9 | 10 | const peopleQuery = useQuery({ 11 | queryKey: ["people"], 12 | queryFn: () => getPeople(supabase), 13 | refetchInterval: 1000, 14 | }); 15 | 16 | return ( 17 |
18 |

People

19 | 20 | Add Person 21 | 22 | 23 | {peopleQuery.isLoading &&
Loading...
} 24 | 25 | 26 | {peopleQuery.data?.map((person) => ( 27 | 28 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ))} 41 | 42 |
29 | 30 | 32 | {person.name} 33 | {person.dob}{person.address}{person.email}{person.phone}{person.countries?.name}
43 | {peopleQuery.isError &&
Error fetching data
} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/pages/dashboard.jsx: -------------------------------------------------------------------------------- 1 | // src/pages/dashboard.jsx 2 | import { useSupabaseClient, useUser } from "@supabase/auth-helpers-react"; 3 | import { useRouter } from "next/router"; 4 | import { securePage } from "@/services/securePage"; 5 | import { useEffect, useState } from "react"; 6 | import { getCompanies } from "@/services/getCompanies"; 7 | import { deleteCompany } from "@/services/deleteCompany"; 8 | import { ImageInput } from "@/components/ImageInput"; 9 | 10 | export default securePage(function Dashboard() { 11 | const supabase = useSupabaseClient(); 12 | const router = useRouter(); 13 | const user = useUser(); 14 | const [imageUrl, setImageUrl] = useState("0.1466368422093962.jpg"); 15 | 16 | const [companies, setCompanies] = useState([]); 17 | useEffect(() => { 18 | getCompanies(supabase).then(setCompanies); 19 | }, []); 20 | 21 | const handleSignOut = async () => { 22 | await supabase.auth.signOut(); 23 | router.push("/"); 24 | }; 25 | 26 | const handlerDelete = (id) => async () => { 27 | await deleteCompany(supabase, id); 28 | }; 29 | 30 | return ( 31 |
32 | Dashboard for {user?.email || "Not authenticated"} 33 | 36 |
    37 | {companies.map((company, index) => ( 38 |
  • 39 | {company.name}{" "} 40 | 43 |
  • 44 | ))} 45 |
46 |

47 | {imageUrl} 48 |

49 | 50 |
51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /src/pages/people/create.jsx: -------------------------------------------------------------------------------- 1 | import { PersonSchema } from "@/services/schema"; 2 | import { Controller, useForm } from "react-hook-form"; 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { createPerson } from "@/services/createPerson"; 5 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 6 | import { useRouter } from "next/router"; 7 | import { useMutation, useQuery } from "@tanstack/react-query"; 8 | import { getCountries } from "@/services/getPeople"; 9 | import { ImageInput } from "@/components/ImageInput"; 10 | import { PersonForm } from "@/components/PersonForm"; 11 | 12 | export default function CreatePersonPage() { 13 | const supabase = useSupabaseClient(); 14 | const router = useRouter(); 15 | 16 | const countriesQuery = useQuery({ 17 | queryKey: ["countries"], 18 | queryFn: () => getCountries(supabase), 19 | }); 20 | 21 | const form = useForm({ 22 | defaultValues: { 23 | name: "", 24 | dob: new Date(), 25 | email: "", 26 | address: "", 27 | phone: "", 28 | }, 29 | resolver: zodResolver(PersonSchema), 30 | }); 31 | 32 | const peopleQuery = useMutation({ 33 | mutationFn: (data) => createPerson(supabase, data), 34 | }); 35 | 36 | const handleSaveData = async (data) => { 37 | peopleQuery.mutate(data, { 38 | onSuccess: (record) => { 39 | console.log(record); 40 | router.push("/people"); 41 | }, 42 | onError: (e) => { 43 | alert(e.message); 44 | }, 45 | }); 46 | }; 47 | 48 | return ( 49 |
50 |

Create person

51 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/pages/people/[id]/index.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useQuery } from "@tanstack/react-query"; 3 | import { getPerson } from "@/services/getPeople"; 4 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 5 | import { SupaImage } from "@/components/SupaImage"; 6 | import Link from "next/link"; 7 | 8 | export default function PersonPage() { 9 | const router = useRouter(); 10 | const id = router.query.id; 11 | const supabase = useSupabaseClient(); 12 | 13 | const personQuery = useQuery({ 14 | queryKey: ["person", id], 15 | queryFn: () => getPerson(supabase, id), 16 | }); 17 | 18 | return ( 19 |
20 |

{personQuery.data?.name}

21 | 22 | 23 | 24 | 25 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
Picture 26 | {personQuery.data?.picture && ( 27 | 31 | )} 32 |
Email{personQuery.data?.email}
Address{personQuery.data?.address}
Phone{personQuery.data?.phone}
DOB{personQuery.data?.dob}
Country{personQuery.data?.countries?.name}
60 | 61 | Edit 62 | 63 | 64 | All People 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 2 | import { useRouter } from "next/router"; 3 | import { useForm } from "react-hook-form"; 4 | 5 | export function Login() { 6 | const form = useForm({ 7 | defaultValues: { 8 | email: "", 9 | password: "", 10 | }, 11 | }); 12 | const supabase = useSupabaseClient(); 13 | const router = useRouter(); 14 | 15 | const handleLogin = async () => { 16 | const values = form.getValues(); 17 | const { data, error: signInError } = await supabase.auth.signInWithPassword( 18 | { 19 | email: values.email, 20 | password: values.password, 21 | } 22 | ); 23 | 24 | if (signInError) { 25 | return; 26 | } 27 | 28 | if (!signInError && data && data.session) { 29 | await supabase.auth.setSession({ 30 | access_token: data.session.access_token, 31 | refresh_token: data.session.refresh_token, 32 | }); 33 | router.replace("/dashboard"); 34 | } 35 | }; 36 | 37 | const handleSignup = async () => { 38 | const values = form.getValues(); 39 | const { data, error: signInError } = await supabase.auth.signUp({ 40 | email: values.email, 41 | password: values.password, 42 | }); 43 | 44 | if (signInError) { 45 | return; 46 | } 47 | 48 | if (!signInError && data && data.session) { 49 | await supabase.auth.setSession({ 50 | access_token: data.session.access_token, 51 | refresh_token: data.session.refresh_token, 52 | }); 53 | router.replace("/dashboard"); 54 | } 55 | }; 56 | 57 | return ( 58 |
59 |

Login/Register

60 | 61 | 62 | 63 | 64 | 67 | 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/pages/people/[id]/edit.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useForm } from "react-hook-form"; 3 | import { useQuery, useMutation } from "@tanstack/react-query"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 6 | import { PersonSchema } from "@/services/schema"; 7 | import { updatePerson } from "@/services/createPerson"; 8 | import { PersonForm } from "@/components/PersonForm"; 9 | import { getCountries, getPerson } from "@/services/getPeople"; 10 | import { useEffect } from "react"; 11 | 12 | export default function EditPerson() { 13 | const router = useRouter(); 14 | const id = router.query.id; 15 | const supabase = useSupabaseClient(); 16 | 17 | const countriesQuery = useQuery({ 18 | queryKey: ["countries"], 19 | queryFn: () => getCountries(supabase), 20 | }); 21 | 22 | const personQuery = useQuery({ 23 | queryKey: ["person", id], 24 | queryFn: () => getPerson(supabase, id), 25 | }); 26 | 27 | const form = useForm({ 28 | defaultValues: { 29 | name: "", 30 | dob: new Date(), 31 | email: "", 32 | address: "", 33 | phone: "", 34 | }, 35 | resolver: zodResolver(PersonSchema), 36 | }); 37 | 38 | useEffect(() => { 39 | if (personQuery.data) { 40 | form.reset(personQuery.data); 41 | } 42 | }, [personQuery.data, form]); 43 | 44 | const peopleQuery = useMutation({ 45 | mutationFn: (data) => updatePerson(supabase, id, data), 46 | }); 47 | 48 | const handleSaveData = async (data) => { 49 | peopleQuery.mutate(data, { 50 | onSuccess: (record) => { 51 | console.log(record); 52 | router.push(`/people/${id}`); 53 | }, 54 | onError: (e) => { 55 | alert(e.message); 56 | }, 57 | }); 58 | }; 59 | 60 | return ( 61 |
62 |

Edit person

63 | {personQuery.data && ( 64 | 71 | )} 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/PersonForm.jsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ImageInput } from "./ImageInput"; 3 | 4 | export function PersonForm({ 5 | form, 6 | onSaveData, 7 | countries, 8 | isPending, 9 | submitLabel, 10 | }) { 11 | return ( 12 |
13 | 14 | 15 | {form.formState.errors.name && ( 16 |
{form.formState.errors.name?.message}
17 | )} 18 | 19 | 20 | 25 | {form.formState.errors.dob && ( 26 |
{form.formState.errors.dob?.message}
27 | )} 28 | 29 | 30 | 31 | {form.formState.errors.email && ( 32 |
33 | {form.formState.errors.email?.message} 34 |
35 | )} 36 | 37 | 38 | 39 | {form.formState.errors.phone && ( 40 |
41 | {form.formState.errors.phone?.message} 42 |
43 | )} 44 | 45 | 46 | 51 | {form.formState.errors.address && ( 52 |
53 | {form.formState.errors.address?.message} 54 |
55 | )} 56 | 57 | 58 | 66 | {form.formState.errors.country_id && ( 67 |
68 | {form.formState.errors.country_id?.message} 69 |
70 | )} 71 | 72 | 73 |
74 | ( 78 | 83 | )} 84 | /> 85 | {form.formState.errors.picture && ( 86 |
87 | {form.formState.errors.picture?.message} 88 |
89 | )} 90 |
91 | 92 | 95 |
96 | ); 97 | } 98 | --------------------------------------------------------------------------------