├── .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 |
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 | |
29 |
30 | |
31 |
32 | {person.name}
33 | |
34 | {person.dob} |
35 | {person.address} |
36 | {person.email} |
37 | {person.phone} |
38 | {person.countries?.name} |
39 |
40 | ))}
41 |
42 |
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 | | Picture |
25 |
26 | {personQuery.data?.picture && (
27 |
31 | )}
32 | |
33 |
34 |
35 | | Email |
36 | {personQuery.data?.email} |
37 |
38 |
39 |
40 | | Address |
41 | {personQuery.data?.address} |
42 |
43 |
44 |
45 | | Phone |
46 | {personQuery.data?.phone} |
47 |
48 |
49 |
50 | | DOB |
51 | {personQuery.data?.dob} |
52 |
53 |
54 |
55 | | Country |
56 | {personQuery.data?.countries?.name} |
57 |
58 |
59 |
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 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------