diff --git a/app-directory/supabase-nextjs/.eslintrc.json b/app-directory/supabase-nextjs/.eslintrc.json new file mode 100644 index 0000000000..a2569c2c7c --- /dev/null +++ b/app-directory/supabase-nextjs/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": "next/core-web-vitals" +} diff --git a/app-directory/supabase-nextjs/.gitignore b/app-directory/supabase-nextjs/.gitignore new file mode 100644 index 0000000000..3f61c7a99f --- /dev/null +++ b/app-directory/supabase-nextjs/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +/node_modules +/.pnp +.pnp.js + +# Testing +/coverage + +# Next.js +/.next/ +/out/ +next-env.d.ts + +# Production +build +dist + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local ENV files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# Turborepo +.turbo + +# typescript +*.tsbuildinfo diff --git a/app-directory/supabase-nextjs/README.md b/app-directory/supabase-nextjs/README.md new file mode 100644 index 0000000000..f0869d7f06 --- /dev/null +++ b/app-directory/supabase-nextjs/README.md @@ -0,0 +1,88 @@ +# supabase-nextjs example + +This example shows how to insert and retrieve data from a Supabase (Postgres) database using Next.js. It uses the App Router and SSR patterns: + +- Mutation logic lives in `app/action.ts` using Next.js Server Actions. +- Query logic lives in `app/queries.ts` and is called from server components. +- Supabase client configuration is under `lib/supabase/`. + +To run this example locally you need a `.env.local` file with your Supabase project keys: + +```env +NEXT_PUBLIC_SUPABASE_URL=your-supabase-url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key +``` + +Add your Supabase API keys there and then start the dev server. + +## Running this project locally + +1. Install dependencies: + +```bash +pnpm install +``` + +2. Create a `.env.local` file in the project root and add your Supabase keys (see above). + +3. Start the development server: + +```bash +pnpm dev +``` + +4. Open the app in your browser: + +```text +http://localhost:3000 +``` + +## Database schema + +This example expects a `notes` table in your Supabase Postgres database. The table stores each note created from the UI: + +| Column | Type | Description | +| ------------- | ------------- | ----------------------------------- | +| `id` | `uuid` | Primary key for the note | +| `username` | `text` | Name/handle of the note author | +| `title` | `text` | Short title for the note | +| `description` | `text` | Main content/body of the note | +| `created_at` | `timestamptz` | Timestamp when the note was created | + +A possible SQL definition for this table is: + +```sql +create table if not exists notes ( + id uuid primary key default gen_random_uuid(), + username text not null, + title text not null, + description text, + created_at timestamptz not null default now() +); +``` + +## How to Use + +You can choose from one of the following two methods to use this repository: + +### One-Click Deploy + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/app-directory/supabase-nextjs&project-name=supabase-nextjs&repository-name=supabase-nextjs) + +### Clone and Deploy + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +pnpm create next-app --example https://github.com/vercel/examples/tree/main/app-directory/supabase-nextjs +``` + +Next, run Next.js in development mode: + +```bash +pnpm dev +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=edge-middleware-eap) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/app-directory/supabase-nextjs/app/action.ts b/app-directory/supabase-nextjs/app/action.ts new file mode 100644 index 0000000000..b9c4242866 --- /dev/null +++ b/app-directory/supabase-nextjs/app/action.ts @@ -0,0 +1,34 @@ +'use server' + +import { createSupabaseServer } from '../lib/supabase/server' + +export async function createNote(formData: FormData) { + try { + const username = formData.get('username').trim() + const title = formData.get('title').trim() + const description = formData.get('description').trim() + + if ( + typeof username !== 'string' || + typeof title !== 'string' || + typeof description !== 'string' + ) { + throw new Error('Missing required fields') + } + + const supabase = await createSupabaseServer() + + const { error } = await supabase.from('notes').insert({ + username, + title, + description, + }) + + if (error) { + throw new Error(error.message) + } + } catch (err: any) { + console.error('Error creating note:', err?.message ?? err) + throw err + } +} diff --git a/app-directory/supabase-nextjs/app/layout.tsx b/app-directory/supabase-nextjs/app/layout.tsx new file mode 100644 index 0000000000..9a01790703 --- /dev/null +++ b/app-directory/supabase-nextjs/app/layout.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from 'react' +import { Layout, getMetadata } from '@vercel/examples-ui' +import '@vercel/examples-ui/globals.css' + +export const metadata = getMetadata({ + title: 'supabase-nextjs', + description: 'supabase-nextjs', +}) + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/app-directory/supabase-nextjs/app/page.tsx b/app-directory/supabase-nextjs/app/page.tsx new file mode 100644 index 0000000000..058b0cfb21 --- /dev/null +++ b/app-directory/supabase-nextjs/app/page.tsx @@ -0,0 +1,32 @@ +import { Page, Text } from '@vercel/examples-ui' +import { fetchNotes } from './queries' +import NotesCard from '../components/NotesCard' +import CreateNotes from '../components/CreateNotes' +import { createNote } from './action' + +export default async function Home() { + const notes = await fetchNotes() + + return ( + +
+ supabase-nextjs usage example + + This project demonstrates how to use Supabase with Next.js App Router + to build a simple notes web app. It uses server-side rendering for + reading data and Next.js Server Actions for inserting new notes into a + Supabase (Postgres) database. + +
+ +
+
+ Notes + +
+ {Array.isArray(notes) && + notes.map((note) => )} +
+
+ ) +} diff --git a/app-directory/supabase-nextjs/app/queries.ts b/app-directory/supabase-nextjs/app/queries.ts new file mode 100644 index 0000000000..bcc1f03b4d --- /dev/null +++ b/app-directory/supabase-nextjs/app/queries.ts @@ -0,0 +1,19 @@ +import { createSupabaseServer } from '../lib/supabase/server' + +export async function fetchNotes() { + try { + const supabase = await createSupabaseServer() + + const { data, error } = await supabase + .from('notes') + .select('*') + .order('created_at', { ascending: false }) // ascending: false, to show latest notes on top + + if (error) throw new Error(error.message) + + return data + } catch (err: any) { + console.error('Error fetching notes:', err?.message ?? err) + throw err + } +} diff --git a/app-directory/supabase-nextjs/components/CreateNotes.tsx b/app-directory/supabase-nextjs/components/CreateNotes.tsx new file mode 100644 index 0000000000..eacab98395 --- /dev/null +++ b/app-directory/supabase-nextjs/components/CreateNotes.tsx @@ -0,0 +1,101 @@ +'use client' + +import { useState, useTransition, type FormEvent } from 'react' + +type CreateNotesProps = { + createNote: (formData: FormData) => Promise +} + +export default function CreateNotes({ createNote }: CreateNotesProps) { + const [open, setOpen] = useState(false) + const [error, setError] = useState(null) + const [isPending, startTransition] = useTransition() + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + const form = event.currentTarget + const formData = new FormData(form) + + setError(null) + + startTransition(() => { + createNote(formData) + .then(() => { + form.reset() + setOpen(false) + }) + .catch((err: any) => { + setError(err?.message ?? 'Something went wrong') + }) + }) + } + + return ( +
+ + + {open && ( +
+
+

New Note

+ +
+
+ + +