commit 8a1a9546032804aa9e50d9630ffe9f18d7b3efea
Author: tockawaffle <98602240+tockawaffle@users.noreply.github.com>
Date: Tue Dec 10 16:52:53 2024 +0000
Initial commit
Created from https://vercel.com/new
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..6937031
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+# Update these with your Supabase details from your project settings > API
+# https://app.supabase.com/project/_/settings/api
+NEXT_PUBLIC_SUPABASE_URL=your-project-url
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..00bba9b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,37 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+.env
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..53c555a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,96 @@
+
+
+ Next.js and Supabase Starter Kit
+
+
+
+ The fastest way to build apps with Next.js and Supabase
+
+
+
+ Features ·
+ Demo ·
+ Deploy to Vercel ·
+ Clone and run locally ·
+ Feedback and issues
+ More Examples
+
+
+
+## Features
+
+- Works across the entire [Next.js](https://nextjs.org) stack
+ - App Router
+ - Pages Router
+ - Middleware
+ - Client
+ - Server
+ - It just works!
+- supabase-ssr. A package to configure Supabase Auth to use cookies
+- Styling with [Tailwind CSS](https://tailwindcss.com)
+- Components with [shadcn/ui](https://ui.shadcn.com/)
+- Optional deployment with [Supabase Vercel Integration and Vercel deploy](#deploy-your-own)
+ - Environment variables automatically assigned to Vercel project
+
+## Demo
+
+You can view a fully working demo at [demo-nextjs-with-supabase.vercel.app](https://demo-nextjs-with-supabase.vercel.app/).
+
+## Deploy to Vercel
+
+Vercel deployment will guide you through creating a Supabase account and project.
+
+After installation of the Supabase integration, all relevant environment variables will be assigned to the project so the deployment is fully functioning.
+
+[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&demo-title=nextjs-with-supabase&demo-description=This+starter+configures+Supabase+Auth+to+use+cookies%2C+making+the+user%27s+session+available+throughout+the+entire+Next.js+app+-+Client+Components%2C+Server+Components%2C+Route+Handlers%2C+Server+Actions+and+Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2Fopengraph-image.png)
+
+The above will also clone the Starter kit to your GitHub, you can clone that locally and develop locally.
+
+If you wish to just develop locally and not deploy to Vercel, [follow the steps below](#clone-and-run-locally).
+
+## Clone and run locally
+
+1. You'll first need a Supabase project which can be made [via the Supabase dashboard](https://database.new)
+
+2. Create a Next.js app using the Supabase Starter template npx command
+
+ ```bash
+ npx create-next-app -e with-supabase
+ ```
+
+3. Use `cd` to change into the app's directory
+
+ ```bash
+ cd name-of-new-app
+ ```
+
+4. Rename `.env.example` to `.env.local` and update the following:
+
+ ```
+ NEXT_PUBLIC_SUPABASE_URL=[INSERT SUPABASE PROJECT URL]
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=[INSERT SUPABASE PROJECT API ANON KEY]
+ ```
+
+ Both `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` can be found in [your Supabase project's API settings](https://app.supabase.com/project/_/settings/api)
+
+5. You can now run the Next.js local development server:
+
+ ```bash
+ npm run dev
+ ```
+
+ The starter kit should now be running on [localhost:3000](http://localhost:3000/).
+
+6. This template comes with the default shadcn/ui style initialized. If you instead want other ui.shadcn styles, delete `components.json` and [re-install shadcn/ui](https://ui.shadcn.com/docs/installation/next)
+
+> Check out [the docs for Local Development](https://supabase.com/docs/guides/getting-started/local-development) to also run Supabase locally.
+
+## Feedback and issues
+
+Please file feedback and issues over on the [Supabase GitHub org](https://github.com/supabase/supabase/issues/new/choose).
+
+## More Supabase examples
+
+- [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments)
+- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF)
+- [Supabase Auth and the Next.js App Router](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs)
diff --git a/app/(auth-pages)/forgot-password/page.tsx b/app/(auth-pages)/forgot-password/page.tsx
new file mode 100644
index 0000000..bcf9725
--- /dev/null
+++ b/app/(auth-pages)/forgot-password/page.tsx
@@ -0,0 +1,37 @@
+import { forgotPasswordAction } from "@/app/actions";
+import { FormMessage, Message } from "@/components/form-message";
+import { SubmitButton } from "@/components/submit-button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import Link from "next/link";
+import { SmtpMessage } from "../smtp-message";
+
+export default async function ForgotPassword(props: {
+ searchParams: Promise;
+}) {
+ const searchParams = await props.searchParams;
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/(auth-pages)/layout.tsx b/app/(auth-pages)/layout.tsx
new file mode 100644
index 0000000..e038de1
--- /dev/null
+++ b/app/(auth-pages)/layout.tsx
@@ -0,0 +1,9 @@
+export default async function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ {children}
+ );
+}
diff --git a/app/(auth-pages)/sign-in/page.tsx b/app/(auth-pages)/sign-in/page.tsx
new file mode 100644
index 0000000..7628cc7
--- /dev/null
+++ b/app/(auth-pages)/sign-in/page.tsx
@@ -0,0 +1,44 @@
+import { signInAction } from "@/app/actions";
+import { FormMessage, Message } from "@/components/form-message";
+import { SubmitButton } from "@/components/submit-button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import Link from "next/link";
+
+export default async function Login(props: { searchParams: Promise }) {
+ const searchParams = await props.searchParams;
+ return (
+
+ );
+}
diff --git a/app/(auth-pages)/sign-up/page.tsx b/app/(auth-pages)/sign-up/page.tsx
new file mode 100644
index 0000000..31b5a6d
--- /dev/null
+++ b/app/(auth-pages)/sign-up/page.tsx
@@ -0,0 +1,51 @@
+import { signUpAction } from "@/app/actions";
+import { FormMessage, Message } from "@/components/form-message";
+import { SubmitButton } from "@/components/submit-button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import Link from "next/link";
+import { SmtpMessage } from "../smtp-message";
+
+export default async function Signup(props: {
+ searchParams: Promise;
+}) {
+ const searchParams = await props.searchParams;
+ if ("message" in searchParams) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/(auth-pages)/smtp-message.tsx b/app/(auth-pages)/smtp-message.tsx
new file mode 100644
index 0000000..84c21fc
--- /dev/null
+++ b/app/(auth-pages)/smtp-message.tsx
@@ -0,0 +1,25 @@
+import { ArrowUpRight, InfoIcon } from "lucide-react";
+import Link from "next/link";
+
+export function SmtpMessage() {
+ return (
+
+
+
+
+ Note: Emails are rate limited. Enable Custom SMTP to
+ increase the rate limit.
+
+
+
+
+ );
+}
diff --git a/app/actions.ts b/app/actions.ts
new file mode 100644
index 0000000..dbf8a26
--- /dev/null
+++ b/app/actions.ts
@@ -0,0 +1,134 @@
+"use server";
+
+import { encodedRedirect } from "@/utils/utils";
+import { createClient } from "@/utils/supabase/server";
+import { headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+export const signUpAction = async (formData: FormData) => {
+ const email = formData.get("email")?.toString();
+ const password = formData.get("password")?.toString();
+ const supabase = await createClient();
+ const origin = (await headers()).get("origin");
+
+ if (!email || !password) {
+ return encodedRedirect(
+ "error",
+ "/sign-up",
+ "Email and password are required",
+ );
+ }
+
+ const { error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ emailRedirectTo: `${origin}/auth/callback`,
+ },
+ });
+
+ if (error) {
+ console.error(error.code + " " + error.message);
+ return encodedRedirect("error", "/sign-up", error.message);
+ } else {
+ return encodedRedirect(
+ "success",
+ "/sign-up",
+ "Thanks for signing up! Please check your email for a verification link.",
+ );
+ }
+};
+
+export const signInAction = async (formData: FormData) => {
+ const email = formData.get("email") as string;
+ const password = formData.get("password") as string;
+ const supabase = await createClient();
+
+ const { error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (error) {
+ return encodedRedirect("error", "/sign-in", error.message);
+ }
+
+ return redirect("/protected");
+};
+
+export const forgotPasswordAction = async (formData: FormData) => {
+ const email = formData.get("email")?.toString();
+ const supabase = await createClient();
+ const origin = (await headers()).get("origin");
+ const callbackUrl = formData.get("callbackUrl")?.toString();
+
+ if (!email) {
+ return encodedRedirect("error", "/forgot-password", "Email is required");
+ }
+
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
+ redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`,
+ });
+
+ if (error) {
+ console.error(error.message);
+ return encodedRedirect(
+ "error",
+ "/forgot-password",
+ "Could not reset password",
+ );
+ }
+
+ if (callbackUrl) {
+ return redirect(callbackUrl);
+ }
+
+ return encodedRedirect(
+ "success",
+ "/forgot-password",
+ "Check your email for a link to reset your password.",
+ );
+};
+
+export const resetPasswordAction = async (formData: FormData) => {
+ const supabase = await createClient();
+
+ const password = formData.get("password") as string;
+ const confirmPassword = formData.get("confirmPassword") as string;
+
+ if (!password || !confirmPassword) {
+ encodedRedirect(
+ "error",
+ "/protected/reset-password",
+ "Password and confirm password are required",
+ );
+ }
+
+ if (password !== confirmPassword) {
+ encodedRedirect(
+ "error",
+ "/protected/reset-password",
+ "Passwords do not match",
+ );
+ }
+
+ const { error } = await supabase.auth.updateUser({
+ password: password,
+ });
+
+ if (error) {
+ encodedRedirect(
+ "error",
+ "/protected/reset-password",
+ "Password update failed",
+ );
+ }
+
+ encodedRedirect("success", "/protected/reset-password", "Password updated");
+};
+
+export const signOutAction = async () => {
+ const supabase = await createClient();
+ await supabase.auth.signOut();
+ return redirect("/sign-in");
+};
diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts
new file mode 100644
index 0000000..dd415a4
--- /dev/null
+++ b/app/auth/callback/route.ts
@@ -0,0 +1,24 @@
+import { createClient } from "@/utils/supabase/server";
+import { NextResponse } from "next/server";
+
+export async function GET(request: Request) {
+ // The `/auth/callback` route is required for the server-side auth flow implemented
+ // by the SSR package. It exchanges an auth code for the user's session.
+ // https://supabase.com/docs/guides/auth/server-side/nextjs
+ const requestUrl = new URL(request.url);
+ const code = requestUrl.searchParams.get("code");
+ const origin = requestUrl.origin;
+ const redirectTo = requestUrl.searchParams.get("redirect_to")?.toString();
+
+ if (code) {
+ const supabase = await createClient();
+ await supabase.auth.exchangeCodeForSession(code);
+ }
+
+ if (redirectTo) {
+ return NextResponse.redirect(`${origin}${redirectTo}`);
+ }
+
+ // URL to redirect to after sign up process completes
+ return NextResponse.redirect(`${origin}/protected`);
+}
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/app/favicon.ico differ
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..f450d1e
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,69 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --radius: 0.5rem;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ }
+
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..8d80488
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,72 @@
+import DeployButton from "@/components/deploy-button";
+import { EnvVarWarning } from "@/components/env-var-warning";
+import HeaderAuth from "@/components/header-auth";
+import { ThemeSwitcher } from "@/components/theme-switcher";
+import { hasEnvVars } from "@/utils/supabase/check-env-vars";
+import { GeistSans } from "geist/font/sans";
+import { ThemeProvider } from "next-themes";
+import Link from "next/link";
+import "./globals.css";
+
+const defaultUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : "http://localhost:3000";
+
+export const metadata = {
+ metadataBase: new URL(defaultUrl),
+ title: "Next.js and Supabase Starter Kit",
+ description: "The fastest way to build apps with Next.js and Supabase",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+
+
+
+
Next.js Supabase Starter
+
+
+
+
+ {!hasEnvVars ?
:
}
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/opengraph-image.png b/app/opengraph-image.png
new file mode 100644
index 0000000..57595e6
Binary files /dev/null and b/app/opengraph-image.png differ
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..7e2e91a
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,16 @@
+import Hero from "@/components/hero";
+import ConnectSupabaseSteps from "@/components/tutorial/connect-supabase-steps";
+import SignUpUserSteps from "@/components/tutorial/sign-up-user-steps";
+import { hasEnvVars } from "@/utils/supabase/check-env-vars";
+
+export default async function Index() {
+ return (
+ <>
+
+
+ Next steps
+ {hasEnvVars ? : }
+
+ >
+ );
+}
diff --git a/app/protected/page.tsx b/app/protected/page.tsx
new file mode 100644
index 0000000..5508aba
--- /dev/null
+++ b/app/protected/page.tsx
@@ -0,0 +1,38 @@
+import FetchDataSteps from "@/components/tutorial/fetch-data-steps";
+import { createClient } from "@/utils/supabase/server";
+import { InfoIcon } from "lucide-react";
+import { redirect } from "next/navigation";
+
+export default async function ProtectedPage() {
+ const supabase = await createClient();
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ if (!user) {
+ return redirect("/sign-in");
+ }
+
+ return (
+
+
+
+
+ This is a protected page that you can only see as an authenticated
+ user
+
+
+
+
Your user details
+
+ {JSON.stringify(user, null, 2)}
+
+
+
+
Next steps
+
+
+
+ );
+}
diff --git a/app/protected/reset-password/page.tsx b/app/protected/reset-password/page.tsx
new file mode 100644
index 0000000..9cd7084
--- /dev/null
+++ b/app/protected/reset-password/page.tsx
@@ -0,0 +1,37 @@
+import { resetPasswordAction } from "@/app/actions";
+import { FormMessage, Message } from "@/components/form-message";
+import { SubmitButton } from "@/components/submit-button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export default async function ResetPassword(props: {
+ searchParams: Promise;
+}) {
+ const searchParams = await props.searchParams;
+ return (
+
+ );
+}
diff --git a/app/twitter-image.png b/app/twitter-image.png
new file mode 100644
index 0000000..57595e6
Binary files /dev/null and b/app/twitter-image.png differ
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..ec9676b
--- /dev/null
+++ b/components.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
diff --git a/components/deploy-button.tsx b/components/deploy-button.tsx
new file mode 100644
index 0000000..8a5a192
--- /dev/null
+++ b/components/deploy-button.tsx
@@ -0,0 +1,25 @@
+import Link from "next/link";
+import { Button } from "./ui/button";
+
+export default function DeployButton() {
+ return (
+ <>
+
+
+
+
+
+ Deploy to Vercel
+
+
+ >
+ );
+}
diff --git a/components/env-var-warning.tsx b/components/env-var-warning.tsx
new file mode 100644
index 0000000..b6a193f
--- /dev/null
+++ b/components/env-var-warning.tsx
@@ -0,0 +1,33 @@
+import Link from "next/link";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+
+export function EnvVarWarning() {
+ return (
+
+
+ Supabase environment variables required
+
+
+
+ Sign in
+
+
+ Sign up
+
+
+
+ );
+}
diff --git a/components/form-message.tsx b/components/form-message.tsx
new file mode 100644
index 0000000..547fb9f
--- /dev/null
+++ b/components/form-message.tsx
@@ -0,0 +1,24 @@
+export type Message =
+ | { success: string }
+ | { error: string }
+ | { message: string };
+
+export function FormMessage({ message }: { message: Message }) {
+ return (
+
+ {"success" in message && (
+
+ {message.success}
+
+ )}
+ {"error" in message && (
+
+ {message.error}
+
+ )}
+ {"message" in message && (
+
{message.message}
+ )}
+
+ );
+}
diff --git a/components/header-auth.tsx b/components/header-auth.tsx
new file mode 100644
index 0000000..eb9d65c
--- /dev/null
+++ b/components/header-auth.tsx
@@ -0,0 +1,70 @@
+import { signOutAction } from "@/app/actions";
+import { hasEnvVars } from "@/utils/supabase/check-env-vars";
+import Link from "next/link";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+import { createClient } from "@/utils/supabase/server";
+
+export default async function AuthButton() {
+ const supabase = await createClient();
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ if (!hasEnvVars) {
+ return (
+ <>
+
+
+
+ Please update .env.local file with anon key and url
+
+
+
+
+ Sign in
+
+
+ Sign up
+
+
+
+ >
+ );
+ }
+ return user ? (
+
+ Hey, {user.email}!
+
+
+ ) : (
+
+
+ Sign in
+
+
+ Sign up
+
+
+ );
+}
diff --git a/components/hero.tsx b/components/hero.tsx
new file mode 100644
index 0000000..6afca6b
--- /dev/null
+++ b/components/hero.tsx
@@ -0,0 +1,44 @@
+import NextLogo from "./next-logo";
+import SupabaseLogo from "./supabase-logo";
+
+export default function Header() {
+ return (
+
+
+
Supabase and Next.js Starter Template
+
+ The fastest way to build apps with{" "}
+
+ Supabase
+ {" "}
+ and{" "}
+
+ Next.js
+
+
+
+
+ );
+}
diff --git a/components/next-logo.tsx b/components/next-logo.tsx
new file mode 100644
index 0000000..1655582
--- /dev/null
+++ b/components/next-logo.tsx
@@ -0,0 +1,46 @@
+export default function NextLogo() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/submit-button.tsx b/components/submit-button.tsx
new file mode 100644
index 0000000..c1cd9f8
--- /dev/null
+++ b/components/submit-button.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { type ComponentProps } from "react";
+import { useFormStatus } from "react-dom";
+
+type Props = ComponentProps & {
+ pendingText?: string;
+};
+
+export function SubmitButton({
+ children,
+ pendingText = "Submitting...",
+ ...props
+}: Props) {
+ const { pending } = useFormStatus();
+
+ return (
+
+ {pending ? pendingText : children}
+
+ );
+}
diff --git a/components/supabase-logo.tsx b/components/supabase-logo.tsx
new file mode 100644
index 0000000..96a56a5
--- /dev/null
+++ b/components/supabase-logo.tsx
@@ -0,0 +1,102 @@
+export default function SupabaseLogo() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/theme-switcher.tsx b/components/theme-switcher.tsx
new file mode 100644
index 0000000..d838e40
--- /dev/null
+++ b/components/theme-switcher.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Laptop, Moon, Sun } from "lucide-react";
+import { useTheme } from "next-themes";
+import { useEffect, useState } from "react";
+
+const ThemeSwitcher = () => {
+ const [mounted, setMounted] = useState(false);
+ const { theme, setTheme } = useTheme();
+
+ // useEffect only runs on the client, so now we can safely show the UI
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return null;
+ }
+
+ const ICON_SIZE = 16;
+
+ return (
+
+
+
+ {theme === "light" ? (
+
+ ) : theme === "dark" ? (
+
+ ) : (
+
+ )}
+
+
+
+ setTheme(e)}
+ >
+
+ {" "}
+ Light
+
+
+ {" "}
+ Dark
+
+
+ {" "}
+ System
+
+
+
+
+ );
+};
+
+export { ThemeSwitcher };
diff --git a/components/tutorial/code-block.tsx b/components/tutorial/code-block.tsx
new file mode 100644
index 0000000..9f1b13d
--- /dev/null
+++ b/components/tutorial/code-block.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "../ui/button";
+
+const CopyIcon = () => (
+
+
+
+
+);
+
+const CheckIcon = () => (
+
+
+
+);
+
+export function CodeBlock({ code }: { code: string }) {
+ const [icon, setIcon] = useState(CopyIcon);
+
+ const copy = async () => {
+ await navigator?.clipboard?.writeText(code);
+ setIcon(CheckIcon);
+ setTimeout(() => setIcon(CopyIcon), 2000);
+ };
+
+ return (
+
+
+ {icon}
+
+ {code}
+
+ );
+}
diff --git a/components/tutorial/connect-supabase-steps.tsx b/components/tutorial/connect-supabase-steps.tsx
new file mode 100644
index 0000000..04ca37f
--- /dev/null
+++ b/components/tutorial/connect-supabase-steps.tsx
@@ -0,0 +1,62 @@
+import { TutorialStep } from "./tutorial-step";
+
+export default function ConnectSupabaseSteps() {
+ return (
+
+
+
+ Head over to{" "}
+
+ database.new
+ {" "}
+ and create a new Supabase project.
+
+
+
+
+
+ Rename the{" "}
+
+ .env.example
+ {" "}
+ file in your Next.js app to{" "}
+
+ .env.local
+ {" "}
+ and populate with values from{" "}
+
+ your Supabase project's API Settings
+
+ .
+
+
+
+
+
+ You may need to quit your Next.js development server and run{" "}
+
+ npm run dev
+ {" "}
+ again to load the new environment variables.
+
+
+
+
+
+ You may need to refresh the page for Next.js to load the new
+ environment variables.
+
+
+
+ );
+}
diff --git a/components/tutorial/fetch-data-steps.tsx b/components/tutorial/fetch-data-steps.tsx
new file mode 100644
index 0000000..f0193fe
--- /dev/null
+++ b/components/tutorial/fetch-data-steps.tsx
@@ -0,0 +1,96 @@
+import { TutorialStep } from "./tutorial-step";
+import { CodeBlock } from "./code-block";
+
+const create = `create table notes (
+ id bigserial primary key,
+ title text
+);
+
+insert into notes(title)
+values
+ ('Today I created a Supabase project.'),
+ ('I added some data and queried it from Next.js.'),
+ ('It was awesome!');
+`.trim();
+
+const server = `import { createClient } from '@/utils/supabase/server'
+
+export default async function Page() {
+ const supabase = createClient()
+ const { data: notes } = await supabase.from('notes').select()
+
+ return {JSON.stringify(notes, null, 2)}
+}
+`.trim();
+
+const client = `'use client'
+
+import { createClient } from '@/utils/supabase/client'
+import { useEffect, useState } from 'react'
+
+export default function Page() {
+ const [notes, setNotes] = useState(null)
+ const supabase = createClient()
+
+ useEffect(() => {
+ const getData = async () => {
+ const { data } = await supabase.from('notes').select()
+ setNotes(data)
+ }
+ getData()
+ }, [])
+
+ return {JSON.stringify(notes, null, 2)}
+}
+`.trim();
+
+export default function FetchDataSteps() {
+ return (
+
+
+
+ Head over to the{" "}
+
+ Table Editor
+ {" "}
+ for your Supabase project to create a table and insert some example
+ data. If you're stuck for creativity, you can copy and paste the
+ following into the{" "}
+
+ SQL Editor
+ {" "}
+ and click RUN!
+
+
+
+
+
+
+ To create a Supabase client and query data from an Async Server
+ Component, create a new page.tsx file at{" "}
+
+ /app/notes/page.tsx
+ {" "}
+ and add the following.
+
+
+ Alternatively, you can use a Client Component.
+
+
+
+
+ You're ready to launch your product to the world! 🚀
+
+
+ );
+}
diff --git a/components/tutorial/sign-up-user-steps.tsx b/components/tutorial/sign-up-user-steps.tsx
new file mode 100644
index 0000000..c00fb66
--- /dev/null
+++ b/components/tutorial/sign-up-user-steps.tsx
@@ -0,0 +1,88 @@
+import Link from "next/link";
+import { TutorialStep } from "./tutorial-step";
+import { ArrowUpRight } from "lucide-react";
+
+export default function SignUpUserSteps() {
+ return (
+
+ {process.env.VERCEL_ENV === "preview" ||
+ process.env.VERCEL_ENV === "production" ? (
+
+ It looks like this App is hosted on Vercel.
+
+ This particular deployment is
+
+ "{process.env.VERCEL_ENV}"
+ {" "}
+ on
+
+ https://{process.env.VERCEL_URL}
+
+ .
+
+
+ You will need to{" "}
+
+ update your Supabase project
+ {" "}
+ with redirect URLs based on your Vercel deployment URLs.
+
+
+
+ -{" "}
+
+ http://localhost:3000/**
+
+
+
+ -{" "}
+
+ {`https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}/**`}
+
+
+
+ -{" "}
+
+ {`https://${process.env.VERCEL_PROJECT_PRODUCTION_URL?.replace(".vercel.app", "")}-*-[vercel-team-url].vercel.app/**`}
+ {" "}
+ (Vercel Team URL can be found in{" "}
+
+ Vercel Team settings
+
+ )
+
+
+
+ Redirect URLs Docs
+
+
+ ) : null}
+
+
+ Head over to the{" "}
+
+ Sign up
+ {" "}
+ page and sign up your first user. It's okay if this is just you for
+ now. Your awesome idea will have plenty of users later!
+
+
+
+ );
+}
diff --git a/components/tutorial/tutorial-step.tsx b/components/tutorial/tutorial-step.tsx
new file mode 100644
index 0000000..0ab9cd4
--- /dev/null
+++ b/components/tutorial/tutorial-step.tsx
@@ -0,0 +1,30 @@
+import { Checkbox } from "../ui/checkbox";
+
+export function TutorialStep({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {title}
+
+ {children}
+
+
+
+ );
+}
diff --git a/components/typography/inline-code.tsx b/components/typography/inline-code.tsx
new file mode 100644
index 0000000..288f9e3
--- /dev/null
+++ b/components/typography/inline-code.tsx
@@ -0,0 +1,7 @@
+export function TypographyInlineCode() {
+ return (
+
+ @radix-ui/react-alert-dialog
+
+ );
+}
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..d3d5d60
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..57c9fe4
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx
new file mode 100644
index 0000000..5985e3c
--- /dev/null
+++ b/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import * as React from "react";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { Check } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..3a0c7fe
--- /dev/null
+++ b/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+"use client";
+
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..9d631e7
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
new file mode 100644
index 0000000..84f8b0c
--- /dev/null
+++ b/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..365058c
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 0000000..53428f8
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,20 @@
+import { type NextRequest } from "next/server";
+import { updateSession } from "@/utils/supabase/middleware";
+
+export async function middleware(request: NextRequest) {
+ return await updateSession(request);
+}
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except:
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ * - images - .svg, .png, .jpg, .jpeg, .gif, .webp
+ * Feel free to modify this pattern to include more paths.
+ */
+ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
+ ],
+};
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..658404a
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+module.exports = nextConfig;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6f02c89
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@radix-ui/react-checkbox": "^1.1.1",
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@supabase/ssr": "latest",
+ "@supabase/supabase-js": "latest",
+ "autoprefixer": "10.4.20",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "geist": "^1.2.1",
+ "lucide-react": "^0.456.0",
+ "next": "latest",
+ "next-themes": "^0.4.3",
+ "prettier": "^3.3.3",
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
+ },
+ "devDependencies": {
+ "@types/node": "22.9.0",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "18.3.1",
+ "postcss": "8.4.49",
+ "tailwind-merge": "^2.5.2",
+ "tailwindcss": "3.4.14",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "5.6.3"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..12a703d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..41668a3
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,80 @@
+import type { Config } from "tailwindcss";
+
+const config = {
+ darkMode: ["class"],
+ content: [
+ "./pages/**/*.{ts,tsx}",
+ "./components/**/*.{ts,tsx}",
+ "./app/**/*.{ts,tsx}",
+ "./src/**/*.{ts,tsx}",
+ ],
+ prefix: "",
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+} satisfies Config;
+
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e06a445
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/utils/cn.ts b/utils/cn.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/utils/cn.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/utils/supabase/check-env-vars.ts b/utils/supabase/check-env-vars.ts
new file mode 100644
index 0000000..7180f45
--- /dev/null
+++ b/utils/supabase/check-env-vars.ts
@@ -0,0 +1,6 @@
+// This check can be removed
+// it is just for tutorial purposes
+
+export const hasEnvVars =
+ process.env.NEXT_PUBLIC_SUPABASE_URL &&
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
diff --git a/utils/supabase/client.ts b/utils/supabase/client.ts
new file mode 100644
index 0000000..e2660d0
--- /dev/null
+++ b/utils/supabase/client.ts
@@ -0,0 +1,7 @@
+import { createBrowserClient } from "@supabase/ssr";
+
+export const createClient = () =>
+ createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ );
diff --git a/utils/supabase/middleware.ts b/utils/supabase/middleware.ts
new file mode 100644
index 0000000..8619ec0
--- /dev/null
+++ b/utils/supabase/middleware.ts
@@ -0,0 +1,62 @@
+import { createServerClient } from "@supabase/ssr";
+import { type NextRequest, NextResponse } from "next/server";
+
+export const updateSession = async (request: NextRequest) => {
+ // This `try/catch` block is only here for the interactive tutorial.
+ // Feel free to remove once you have Supabase connected.
+ try {
+ // Create an unmodified response
+ let response = NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll();
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value }) =>
+ request.cookies.set(name, value),
+ );
+ response = NextResponse.next({
+ request,
+ });
+ cookiesToSet.forEach(({ name, value, options }) =>
+ response.cookies.set(name, value, options),
+ );
+ },
+ },
+ },
+ );
+
+ // This will refresh session if expired - required for Server Components
+ // https://supabase.com/docs/guides/auth/server-side/nextjs
+ const user = await supabase.auth.getUser();
+
+ // protected routes
+ if (request.nextUrl.pathname.startsWith("/protected") && user.error) {
+ return NextResponse.redirect(new URL("/sign-in", request.url));
+ }
+
+ if (request.nextUrl.pathname === "/" && !user.error) {
+ return NextResponse.redirect(new URL("/protected", request.url));
+ }
+
+ return response;
+ } catch (e) {
+ // If you are here, a Supabase client could not be created!
+ // This is likely because you have not set up environment variables.
+ // Check out http://localhost:3000 for Next Steps.
+ return NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+ }
+};
diff --git a/utils/supabase/server.ts b/utils/supabase/server.ts
new file mode 100644
index 0000000..2c00bbc
--- /dev/null
+++ b/utils/supabase/server.ts
@@ -0,0 +1,29 @@
+import { createServerClient } from "@supabase/ssr";
+import { cookies } from "next/headers";
+
+export const createClient = async () => {
+ const cookieStore = await cookies();
+
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll();
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) => {
+ cookieStore.set(name, value, options);
+ });
+ } catch (error) {
+ // The `set` method was called from a Server Component.
+ // This can be ignored if you have middleware refreshing
+ // user sessions.
+ }
+ },
+ },
+ },
+ );
+};
diff --git a/utils/utils.ts b/utils/utils.ts
new file mode 100644
index 0000000..c9fbbe8
--- /dev/null
+++ b/utils/utils.ts
@@ -0,0 +1,16 @@
+import { redirect } from "next/navigation";
+
+/**
+ * Redirects to a specified path with an encoded message as a query parameter.
+ * @param {('error' | 'success')} type - The type of message, either 'error' or 'success'.
+ * @param {string} path - The path to redirect to.
+ * @param {string} message - The message to be encoded and added as a query parameter.
+ * @returns {never} This function doesn't return as it triggers a redirect.
+ */
+export function encodedRedirect(
+ type: "error" | "success",
+ path: string,
+ message: string,
+) {
+ return redirect(`${path}?${type}=${encodeURIComponent(message)}`);
+}