diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts new file mode 100644 index 0000000..d3c85a9 --- /dev/null +++ b/app/auth/callback/route.ts @@ -0,0 +1,19 @@ +import { createClient } from '@/lib/supabase/server' +import { NextResponse } from 'next/server' + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url) + const code = searchParams.get('code') + const next = searchParams.get('next') ?? '/dashboard' + + if (code) { + const supabase = await createClient() + const { error } = await supabase.auth.exchangeCodeForSession(code) + if (!error) { + return NextResponse.redirect(`${origin}${next}`) + } + } + + // Return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth/error`) +} diff --git a/app/auth/error/page.tsx b/app/auth/error/page.tsx new file mode 100644 index 0000000..48b279c --- /dev/null +++ b/app/auth/error/page.tsx @@ -0,0 +1,55 @@ +import Link from 'next/link' +import { AlertCircle, ArrowLeft } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function AuthErrorPage() { + return ( +
+ {/* Background elements */} +
+
+
+
+ +
+ {/* Back to home */} + + + Back to home + + + {/* Card */} +
+ {/* Icon */} +
+ +
+ + {/* Content */} +

Authentication Error

+

+ Something went wrong during authentication. This could be due to an expired link or a configuration issue. +

+ + {/* Actions */} +
+ +

+ If the problem persists, please{' '} + + contact us + +

+
+
+
+
+ ) +} diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx new file mode 100644 index 0000000..67b76d3 --- /dev/null +++ b/app/auth/login/page.tsx @@ -0,0 +1,206 @@ +'use client' + +import { createClient } from '@/lib/supabase/client' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useState } from 'react' +import { Mail, Lock, Loader2, ArrowLeft } from 'lucide-react' + +export default function LoginPage() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isGoogleLoading, setIsGoogleLoading] = useState(false) + const router = useRouter() + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + const supabase = createClient() + setIsLoading(true) + setError(null) + + try { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + if (error) throw error + router.push('/dashboard') + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'An error occurred') + } finally { + setIsLoading(false) + } + } + + const handleGoogleLogin = async () => { + const supabase = createClient() + setIsGoogleLoading(true) + setError(null) + + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/auth/callback`, + }, + }) + if (error) throw error + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'An error occurred') + setIsGoogleLoading(false) + } + } + + return ( +
+ {/* Background elements */} +
+
+
+
+ +
+ {/* Back to home */} + + + Back to home + + + {/* Card */} +
+ {/* Header */} +
+ + + Cloudrite + + +

Welcome back

+

+ Sign in to your account to continue +

+
+ + {/* Google OAuth Button */} + + + {/* Divider */} +
+
+
+
+
+ + Or continue with email + +
+
+ + {/* Email/Password Form */} +
+
+ +
+ + setEmail(e.target.value)} + className="pl-10 h-12" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10 h-12" + /> +
+
+ + {error && ( +
+

{error}

+
+ )} + + +
+ + {/* Footer */} +

+ Don't have an account?{' '} + + Sign up + +

+
+
+
+ ) +} diff --git a/app/auth/sign-up-success/page.tsx b/app/auth/sign-up-success/page.tsx new file mode 100644 index 0000000..6f04e44 --- /dev/null +++ b/app/auth/sign-up-success/page.tsx @@ -0,0 +1,55 @@ +import Link from 'next/link' +import { Mail, ArrowLeft } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function SignUpSuccessPage() { + return ( +
+ {/* Background elements */} +
+
+
+
+ +
+ {/* Back to home */} + + + Back to home + + + {/* Card */} +
+ {/* Icon */} +
+ +
+ + {/* Content */} +

Check your email

+

+ We've sent you a confirmation link. Please check your email and click the link to verify your account. +

+ + {/* Actions */} +
+ +

+ Didn't receive the email? Check your spam folder or{' '} + + try again + +

+
+
+
+
+ ) +} diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx new file mode 100644 index 0000000..964f60d --- /dev/null +++ b/app/auth/sign-up/page.tsx @@ -0,0 +1,233 @@ +'use client' + +import { createClient } from '@/lib/supabase/client' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useState } from 'react' +import { Mail, Lock, Loader2, ArrowLeft, User } from 'lucide-react' + +export default function SignUpPage() { + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isGoogleLoading, setIsGoogleLoading] = useState(false) + const router = useRouter() + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault() + const supabase = createClient() + setIsLoading(true) + setError(null) + + try { + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${window.location.origin}/auth/callback`, + data: { + full_name: name, + }, + }, + }) + if (error) throw error + router.push('/auth/sign-up-success') + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'An error occurred') + } finally { + setIsLoading(false) + } + } + + const handleGoogleSignUp = async () => { + const supabase = createClient() + setIsGoogleLoading(true) + setError(null) + + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/auth/callback`, + }, + }) + if (error) throw error + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'An error occurred') + setIsGoogleLoading(false) + } + } + + return ( +
+ {/* Background elements */} +
+
+
+
+ +
+ {/* Back to home */} + + + Back to home + + + {/* Card */} +
+ {/* Header */} +
+ + + Cloudrite + + +

Create an account

+

+ Get started with Cloudrite today +

+
+ + {/* Google OAuth Button */} + + + {/* Divider */} +
+
+
+
+
+ + Or continue with email + +
+
+ + {/* Email/Password Form */} +
+
+ +
+ + setName(e.target.value)} + className="pl-10 h-12" + /> +
+
+ +
+ +
+ + setEmail(e.target.value)} + className="pl-10 h-12" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10 h-12" + /> +
+

+ Must be at least 6 characters +

+
+ + {error && ( +
+

{error}

+
+ )} + + +
+ + {/* Footer */} +

+ Already have an account?{' '} + + Sign in + +

+
+
+
+ ) +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..74eb952 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,134 @@ +import { createClient } from '@/lib/supabase/server' +import { redirect } from 'next/navigation' +import Link from 'next/link' +import { LogOut, User, ArrowLeft, Cloud, Globe, Wrench } from 'lucide-react' +import { Button } from '@/components/ui/button' + +async function signOut() { + 'use server' + const supabase = await createClient() + await supabase.auth.signOut() + redirect('/') +} + +export default async function DashboardPage() { + const supabase = await createClient() + const { data: { user } } = await supabase.auth.getUser() + + if (!user) { + redirect('/auth/login') + } + + const userName = user.user_metadata?.full_name || user.email?.split('@')[0] || 'User' + + return ( +
+ {/* Header */} +
+
+ + + Cloudrite + + + +
+
+ + {userName} +
+
+ +
+
+
+
+ + {/* Main content */} +
+ {/* Welcome section */} +
+ + + Back to home + +

+ Welcome back, {userName} +

+

+ Your Cloudrite dashboard is coming soon. We're working on exciting features for you. +

+
+ + {/* Service cards */} +
+
+
+ +
+

Web Development

+

+ Manage your websites, view analytics, and request updates. +

+ Coming Soon +
+ +
+
+ +
+

Cloud Hosting

+

+ Monitor your hosting, check uptime, and manage your servers. +

+ Coming Soon +
+ +
+
+ +
+

I.T Support

+

+ Submit support tickets and track the status of your requests. +

+ Coming Soon +
+
+ + {/* Account info */} +
+

Account Information

+
+
+ Email + {user.email} +
+
+ Account created + + {new Date(user.created_at).toLocaleDateString('en-NZ', { + day: 'numeric', + month: 'long', + year: 'numeric' + })} + +
+
+ Auth provider + + {user.app_metadata?.provider || 'Email'} + +
+
+
+
+
+ ) +} diff --git a/components/header.tsx b/components/header.tsx index bd655d7..3721967 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -2,12 +2,16 @@ import { useState, useEffect } from "react" import Link from "next/link" -import { Menu, X, Cloud } from "lucide-react" +import { Menu, X, Cloud, User, LogOut } from "lucide-react" import { Button } from "@/components/ui/button" +import { createClient } from "@/lib/supabase/client" +import type { User as SupabaseUser } from "@supabase/supabase-js" export function Header() { const [isScrolled, setIsScrolled] = useState(false) const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) + const [user, setUser] = useState(null) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { const handleScroll = () => { @@ -17,6 +21,29 @@ export function Header() { return () => window.removeEventListener("scroll", handleScroll) }, []) + useEffect(() => { + const supabase = createClient() + + // Get initial session + supabase.auth.getUser().then(({ data: { user } }) => { + setUser(user) + setIsLoading(false) + }) + + // Listen for auth changes + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null) + }) + + return () => subscription.unsubscribe() + }, []) + + const handleSignOut = async () => { + const supabase = createClient() + await supabase.auth.signOut() + setUser(null) + } + const navLinks = [ { href: "#services", label: "Services" }, { href: "#features", label: "Why Us" }, @@ -59,14 +86,49 @@ export function Header() { ))}
- {/* CTA Button */} -
- + {/* CTA Button / Auth */} +
+ {isLoading ? ( +
+ ) : user ? ( + <> + + + + ) : ( + <> + + + + )}
{/* Mobile Menu Button */} @@ -109,14 +171,48 @@ export function Header() { {link.label} ))} - + {user ? ( + <> + + + + ) : ( + <> + + + + )}
diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts new file mode 100644 index 0000000..c48a435 --- /dev/null +++ b/lib/supabase/client.ts @@ -0,0 +1,8 @@ +import { createBrowserClient } from '@supabase/ssr' + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + ) +} diff --git a/lib/supabase/middleware.ts b/lib/supabase/middleware.ts new file mode 100644 index 0000000..d09ab14 --- /dev/null +++ b/lib/supabase/middleware.ts @@ -0,0 +1,77 @@ +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }) + + // With Fluid compute, don't put this client in a global environment + // variable. Always create a new one on each request. + 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), + ) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options), + ) + }, + }, + }, + ) + + // Do not run code between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + // IMPORTANT: If you remove getUser() and you use server-side rendering + // with the Supabase client, your users may be randomly logged out. + const { + data: { user }, + } = await supabase.auth.getUser() + + // Protected routes - redirect to login if not authenticated + const protectedRoutes = ['/protected', '/dashboard'] + const isProtectedRoute = protectedRoutes.some(route => + request.nextUrl.pathname.startsWith(route) + ) + + if (isProtectedRoute && !user) { + const url = request.nextUrl.clone() + url.pathname = '/auth/login' + return NextResponse.redirect(url) + } + + // Redirect authenticated users away from auth pages + if (user && request.nextUrl.pathname.startsWith('/auth/')) { + const url = request.nextUrl.clone() + url.pathname = '/dashboard' + return NextResponse.redirect(url) + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. + // If you're creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: + // const myNewResponse = NextResponse.next({ request }) + // 2. Copy over the cookies, like so: + // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) + // 3. Change the myNewResponse object to fit your needs, but avoid changing + // the cookies! + // 4. Finally: + // return myNewResponse + // If this is not done, you may be causing the browser and server to go out + // of sync and terminate the user's session prematurely! + + return supabaseResponse +} diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts new file mode 100644 index 0000000..a249098 --- /dev/null +++ b/lib/supabase/server.ts @@ -0,0 +1,34 @@ +import { createServerClient } from '@supabase/ssr' +import { cookies } from 'next/headers' + +/** + * Especially important if using Fluid compute: Don't put this client in a + * global variable. Always create a new client within each function when using + * it. + */ +export async function createClient() { + 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 { + // The "setAll" method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + }, + ) +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..de79b4c --- /dev/null +++ b/middleware.ts @@ -0,0 +1,20 @@ +import { updateSession } from '@/lib/supabase/middleware' +import { type NextRequest } from 'next/server' + +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/package.json b/package.json index 7e43b1b..b3c65f9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.1", - "@vercel/analytics": "1.6.1", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-aspect-ratio": "1.1.8", @@ -38,6 +37,9 @@ "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", + "@supabase/ssr": "^0.9.0", + "@supabase/supabase-js": "^2.99.3", + "@vercel/analytics": "1.6.1", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61eeab2..4a17b65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,12 @@ importers: '@radix-ui/react-tooltip': specifier: 1.2.8 version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@supabase/ssr': + specifier: ^0.9.0 + version: 0.9.0(@supabase/supabase-js@2.99.3) + '@supabase/supabase-js': + specifier: ^2.99.3 + version: 2.99.3 '@vercel/analytics': specifier: 1.6.1 version: 1.6.1(next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) @@ -1097,6 +1103,35 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@supabase/auth-js@2.99.3': + resolution: {integrity: sha512-vMEVLA1kGGYd/kdsJSwtjiFUZM1nGfrz2DWmgMBZtocV48qL+L2+4QpIkueXyBEumMQZFEyhz57i/5zGHjvdBw==} + engines: {node: '>=20.0.0'} + + '@supabase/functions-js@2.99.3': + resolution: {integrity: sha512-6tk2zrcBkzKaaBXPOG5nshn30uJNFGOH9LxOnE8i850eQmsX+jVm7vql9kTPyvUzEHwU4zdjSOkXS9M+9ukMVA==} + engines: {node: '>=20.0.0'} + + '@supabase/postgrest-js@2.99.3': + resolution: {integrity: sha512-8HxEf+zNycj7Z8+ONhhlu+7J7Ha+L6weyCtdEeK2mN5OWJbh6n4LPU4iuJ5UlCvvNnbSXMoutY7piITEEAgl2g==} + engines: {node: '>=20.0.0'} + + '@supabase/realtime-js@2.99.3': + resolution: {integrity: sha512-c1azgZ2nZPczbY5k5u5iFrk1InpxN81IvNE+UBAkjrBz3yc5ALLJNkeTQwbJZT4PZBuYXEzqYGLMuh9fdTtTMg==} + engines: {node: '>=20.0.0'} + + '@supabase/ssr@0.9.0': + resolution: {integrity: sha512-UFY6otYV3yqCgV+AyHj80vNkTvbf1Gas2LW4dpbQ4ap6p6v3eB2oaDfcI99jsuJzwVBCFU4BJI+oDYyhNk1z0Q==} + peerDependencies: + '@supabase/supabase-js': ^2.97.0 + + '@supabase/storage-js@2.99.3': + resolution: {integrity: sha512-lOfIm4hInNcd8x0i1LWphnLKxec42wwbjs+vhaVAvR801Vda0UAMbTooUY6gfqgQb8v29GofqKuQMMTAsl6w/w==} + engines: {node: '>=20.0.0'} + + '@supabase/supabase-js@2.99.3': + resolution: {integrity: sha512-GuPbzoEaI51AkLw9VGhLNvnzw4PHbS3p8j2/JlvLeZNQMKwZw4aEYQIDBRtFwL5Nv7/275n9m4DHtakY8nCvgg==} + engines: {node: '>=20.0.0'} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1222,6 +1257,9 @@ packages: '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + '@types/phoenix@1.6.7': + resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1230,6 +1268,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vercel/analytics@1.6.1': resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==} peerDependencies: @@ -1296,6 +1337,10 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1403,6 +1448,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + iceberg-js@0.8.1: + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -1752,6 +1801,18 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -2626,6 +2687,49 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@supabase/auth-js@2.99.3': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.99.3': + dependencies: + tslib: 2.8.1 + + '@supabase/postgrest-js@2.99.3': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.99.3': + dependencies: + '@types/phoenix': 1.6.7 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/ssr@0.9.0(@supabase/supabase-js@2.99.3)': + dependencies: + '@supabase/supabase-js': 2.99.3 + cookie: 1.1.1 + + '@supabase/storage-js@2.99.3': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.99.3': + dependencies: + '@supabase/auth-js': 2.99.3 + '@supabase/functions-js': 2.99.3 + '@supabase/postgrest-js': 2.99.3 + '@supabase/realtime-js': 2.99.3 + '@supabase/storage-js': 2.99.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2727,6 +2831,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/phoenix@1.6.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -2735,6 +2841,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.15 + '@vercel/analytics@1.6.1(next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': optionalDependencies: next: 16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2785,6 +2895,8 @@ snapshots: - '@types/react' - '@types/react-dom' + cookie@1.1.1: {} + csstype@3.2.3: {} d3-array@3.2.4: @@ -2871,6 +2983,8 @@ snapshots: graceful-fs@4.2.11: {} + iceberg-js@0.8.1: {} + input-otp@1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -3206,4 +3320,6 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + ws@8.20.0: {} + zod@3.25.76: {}