diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..da8128c
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "singleQuote": true,
+ "jsxSingleQuote": true,
+ "trailingComma": "all"
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..a678827
--- /dev/null
+++ b/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/styles/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index bf2b9e9..016515f 100644
--- a/package.json
+++ b/package.json
@@ -16,10 +16,15 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@radix-ui/react-slot": "^1.2.2",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.4",
"@t3-oss/env-nextjs": "^0.12.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.510.0",
"next": "^15.2.3",
+ "next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.2"
@@ -35,7 +40,10 @@
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
+ "tailwind-merge": "^3.3.0",
"tailwindcss": "^4.0.15",
+ "tailwindcss-animate": "^1.0.7",
+ "tw-animate-css": "^1.2.9",
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ac6cc8d..43669f0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@radix-ui/react-slot':
+ specifier: ^1.2.2
+ version: 1.2.2(@types/react@19.1.4)(react@19.1.0)
'@supabase/ssr':
specifier: ^0.6.1
version: 0.6.1(@supabase/supabase-js@2.49.4)
@@ -17,9 +20,21 @@ importers:
'@t3-oss/env-nextjs':
specifier: ^0.12.0
version: 0.12.0(typescript@5.8.3)(zod@3.24.4)
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lucide-react:
+ specifier: ^0.510.0
+ version: 0.510.0(react@19.1.0)
next:
specifier: ^15.2.3
version: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ next-themes:
+ specifier: ^0.4.6
+ version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react:
specifier: ^19.0.0
version: 19.1.0
@@ -60,9 +75,18 @@ importers:
prettier-plugin-tailwindcss:
specifier: ^0.6.11
version: 0.6.11(prettier@3.5.3)
+ tailwind-merge:
+ specifier: ^3.3.0
+ version: 3.3.0
tailwindcss:
specifier: ^4.0.15
version: 4.1.6
+ tailwindcss-animate:
+ specifier: ^1.0.7
+ version: 1.0.7(tailwindcss@4.1.6)
+ tw-animate-css:
+ specifier: ^1.2.9
+ version: 1.2.9
typescript:
specifier: ^5.8.2
version: 5.8.3
@@ -356,6 +380,24 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
+ '@radix-ui/react-compose-refs@1.1.2':
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.2':
+ resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@@ -803,9 +845,16 @@ packages:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1512,6 +1561,11 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ lucide-react@0.510.0:
+ resolution: {integrity: sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@@ -1586,6 +1640,12 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
+ next-themes@0.4.6:
+ resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
next@15.3.2:
resolution: {integrity: sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@@ -2000,6 +2060,14 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ tailwind-merge@3.3.0:
+ resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==}
+
+ tailwindcss-animate@1.0.7:
+ resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || insiders'
+
tailwindcss@4.1.6:
resolution: {integrity: sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==}
@@ -2038,6 +2106,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tw-animate-css@1.2.9:
+ resolution: {integrity: sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -2404,6 +2475,19 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.4)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.4
+
+ '@radix-ui/react-slot@1.2.2(@types/react@19.1.4)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.0)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.4
+
'@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.11.0': {}
@@ -2870,8 +2954,14 @@ snapshots:
chownr@3.0.0: {}
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
client-only@0.0.1: {}
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -3737,6 +3827,10 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ lucide-react@0.510.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -3788,6 +3882,11 @@ snapshots:
negotiator@1.0.0: {}
+ next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.3.2
@@ -4237,6 +4336,12 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ tailwind-merge@3.3.0: {}
+
+ tailwindcss-animate@1.0.7(tailwindcss@4.1.6):
+ dependencies:
+ tailwindcss: 4.1.6
+
tailwindcss@4.1.6: {}
tapable@2.2.1: {}
@@ -4276,6 +4381,8 @@ snapshots:
tslib@2.8.1: {}
+ tw-animate-css@1.2.9: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d2ab6a6..5843c4b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,12 +1,30 @@
-import "@/styles/globals.css";
-
import { type Metadata } from "next";
+import "@/styles/globals.css";
import { Geist } from "next/font/google";
+import { cn } from "@/lib/utils";
+import { ThemeProvider } from '@/components/context/theme'
+
export const metadata: Metadata = {
- title: "Create T3 App",
- description: "Generated by create-t3-app",
- icons: [{ rel: "icon", url: "/favicon.ico" }],
+ title: "T3 Template with Supabase",
+ description:
+ "Generated by create-t3-app",
+ icons: [
+ {
+ rel: "icon",
+ url: "/images/favicon.ico",
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '32x32',
+ url: '/images/favicon.png',
+ },
+ {
+ rel: 'apple-touch-icon',
+ url: '/images/appicon.png'
+ },
+ ],
};
const geist = Geist({
@@ -14,12 +32,36 @@ const geist = Geist({
variable: "--font-geist-sans",
});
-export default function RootLayout({
+const RootLayout = ({
children,
-}: Readonly<{ children: React.ReactNode }>) {
+}: Readonly<{ children: React.ReactNode }>) => {
return (
-
-
{children}
+
+
+
+
+
+
+
+
+
+
);
}
+export default RootLayout;
diff --git a/src/components/context/theme.tsx b/src/components/context/theme.tsx
new file mode 100644
index 0000000..d86f907
--- /dev/null
+++ b/src/components/context/theme.tsx
@@ -0,0 +1,55 @@
+'use client';
+import * as React from 'react';
+import { ThemeProvider as NextThemesProvider } from 'next-themes';
+import { Moon, Sun } from 'lucide-react';
+import { useTheme } from 'next-themes';
+import { Button } from '@/components/ui/button';
+
+export const ThemeProvider = ({
+ children,
+ ...props
+}: React.ComponentProps) => {
+ const [mounted, setMounted] = React.useState(false);
+
+ React.useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) return null;
+
+ return {children};
+};
+
+export const ThemeToggle = () => {
+ const { setTheme, resolvedTheme } = useTheme();
+ const [mounted, setMounted] = React.useState(false);
+
+ React.useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return (
+
+ );
+ }
+
+ const toggleTheme = () => {
+ if (resolvedTheme === 'dark') setTheme('light');
+ else setTheme('dark');
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,59 @@
+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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/env.js b/src/env.js
index 5c2f937..fc98d4c 100644
--- a/src/env.js
+++ b/src/env.js
@@ -3,20 +3,21 @@ import { z } from "zod";
export const env = createEnv({
/**
- * Specify your server-side environment variables schema here. This way you can ensure the app
- * isn't built with invalid env vars.
+ * Specify your server-side environment variables schema here.
+ * This way you can ensure the app isn't built with invalid env vars.
*/
server: {
NODE_ENV: z.enum(["development", "test", "production"]),
},
/**
- * Specify your client-side environment variables schema here. This way you can ensure the app
- * isn't built with invalid env vars. To expose them to the client, prefix them with
- * `NEXT_PUBLIC_`.
+ * Specify your client-side environment variables schema here.
+ * This way you can ensure the app isn't built with invalid env vars.
+ * To expose them to the client, prefix them with `NEXT_PUBLIC_`.
*/
client: {
- // NEXT_PUBLIC_CLIENTVAR: z.string(),
+ NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
},
/**
@@ -25,7 +26,9 @@ export const env = createEnv({
*/
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
- // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
+
+ NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/src/lib/utils.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/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..7b7cb4f
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,20 @@
+import { type NextRequest } from "next/server";
+import { updateSession } from "@/utils/supabase/middleware";
+
+export const middleware = async (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/src/styles/globals.css b/src/styles/globals.css
index 8fe04fa..a4d79be 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -1,6 +1,124 @@
@import "tailwindcss";
+@custom-variant dark (&:is(.dark *));
+
@theme {
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/src/utils/supabase/client.ts b/src/utils/supabase/client.ts
new file mode 100644
index 0000000..e2660d0
--- /dev/null
+++ b/src/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/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts
new file mode 100644
index 0000000..8619ec0
--- /dev/null
+++ b/src/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/src/utils/supabase/server.ts b/src/utils/supabase/server.ts
new file mode 100644
index 0000000..2c00bbc
--- /dev/null
+++ b/src/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/src/utils/utils.ts b/src/utils/utils.ts
new file mode 100644
index 0000000..c9fbbe8
--- /dev/null
+++ b/src/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)}`);
+}