feat: implement Supabase authentication with Google OAuth
Add Supabase client, middleware, and auth pages; update header for auth state. Co-authored-by: ASOwnerYT <[email protected]>
This commit is contained in:
@@ -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`)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
{/* Background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* Back to home */}
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to home
|
||||
</Link>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-card border border-border rounded-2xl p-8 text-center">
|
||||
{/* Icon */}
|
||||
<div className="w-16 h-16 bg-destructive/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<AlertCircle className="w-8 h-8 text-destructive" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h1 className="text-2xl font-bold mb-2">Authentication Error</h1>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Something went wrong during authentication. This could be due to an expired link or a configuration issue.
|
||||
</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="space-y-3">
|
||||
<Button asChild className="w-full h-12 bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||
<Link href="/auth/login">
|
||||
Try again
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If the problem persists, please{' '}
|
||||
<Link href="/#contact" className="text-primary hover:underline">
|
||||
contact us
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<string | null>(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 (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
{/* Background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* Back to home */}
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to home
|
||||
</Link>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-card border border-border rounded-2xl p-8">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-block mb-6">
|
||||
<span className="text-2xl font-bold">
|
||||
Cloud<span className="text-primary">rite</span>
|
||||
</span>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold mb-2">Welcome back</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Sign in to your account to continue
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Google OAuth Button */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full h-12 mb-6 relative"
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isGoogleLoading}
|
||||
>
|
||||
{isGoogleLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card px-2 text-muted-foreground">
|
||||
Or continue with email
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email/Password Form */}
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="pl-10 h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="pl-10 h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-destructive/10 border border-destructive/50 rounded-lg">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 bg-primary hover:bg-primary/90 text-primary-foreground"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-sm text-muted-foreground mt-6">
|
||||
Don't have an account?{' '}
|
||||
<Link
|
||||
href="/auth/sign-up"
|
||||
className="text-primary hover:underline font-medium"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
{/* Background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* Back to home */}
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to home
|
||||
</Link>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-card border border-border rounded-2xl p-8 text-center">
|
||||
{/* Icon */}
|
||||
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Mail className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h1 className="text-2xl font-bold mb-2">Check your email</h1>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
We've sent you a confirmation link. Please check your email and click the link to verify your account.
|
||||
</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="space-y-3">
|
||||
<Button asChild className="w-full h-12 bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||
<Link href="/auth/login">
|
||||
Back to sign in
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Didn't receive the email? Check your spam folder or{' '}
|
||||
<Link href="/auth/sign-up" className="text-primary hover:underline">
|
||||
try again
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<string | null>(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 (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
{/* Background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* Back to home */}
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to home
|
||||
</Link>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-card border border-border rounded-2xl p-8">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-block mb-6">
|
||||
<span className="text-2xl font-bold">
|
||||
Cloud<span className="text-primary">rite</span>
|
||||
</span>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold mb-2">Create an account</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Get started with Cloudrite today
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Google OAuth Button */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full h-12 mb-6 relative"
|
||||
onClick={handleGoogleSignUp}
|
||||
disabled={isGoogleLoading}
|
||||
>
|
||||
{isGoogleLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card px-2 text-muted-foreground">
|
||||
Or continue with email
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email/Password Form */}
|
||||
<form onSubmit={handleSignUp} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="pl-10 h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="pl-10 h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Create a password"
|
||||
required
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="pl-10 h-12"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Must be at least 6 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-destructive/10 border border-destructive/50 rounded-lg">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 bg-primary hover:bg-primary/90 text-primary-foreground"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
'Create account'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-sm text-muted-foreground mt-6">
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="text-primary hover:underline font-medium"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<span className="text-xl font-bold">
|
||||
Cloud<span className="text-primary">rite</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<User className="w-4 h-4" />
|
||||
<span>{userName}</span>
|
||||
</div>
|
||||
<form action={signOut}>
|
||||
<Button type="submit" variant="outline" size="sm" className="gap-2">
|
||||
<LogOut className="w-4 h-4" />
|
||||
Sign out
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="container mx-auto px-4 py-12">
|
||||
{/* Welcome section */}
|
||||
<div className="mb-12">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to home
|
||||
</Link>
|
||||
<h1 className="text-4xl font-bold mb-2">
|
||||
Welcome back, <span className="text-primary">{userName}</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Your Cloudrite dashboard is coming soon. We're working on exciting features for you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Service cards */}
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-12">
|
||||
<div className="bg-card border border-border rounded-2xl p-6 hover:border-primary/50 transition-colors">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mb-4">
|
||||
<Globe className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">Web Development</h3>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Manage your websites, view analytics, and request updates.
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">Coming Soon</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-border rounded-2xl p-6 hover:border-primary/50 transition-colors">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mb-4">
|
||||
<Cloud className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">Cloud Hosting</h3>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Monitor your hosting, check uptime, and manage your servers.
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">Coming Soon</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-border rounded-2xl p-6 hover:border-primary/50 transition-colors">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mb-4">
|
||||
<Wrench className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">I.T Support</h3>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Submit support tickets and track the status of your requests.
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">Coming Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account info */}
|
||||
<div className="bg-card border border-border rounded-2xl p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Account Information</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between py-2 border-b border-border">
|
||||
<span className="text-muted-foreground">Email</span>
|
||||
<span className="font-medium">{user.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-border">
|
||||
<span className="text-muted-foreground">Account created</span>
|
||||
<span className="font-medium">
|
||||
{new Date(user.created_at).toLocaleDateString('en-NZ', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-muted-foreground">Auth provider</span>
|
||||
<span className="font-medium capitalize">
|
||||
{user.app_metadata?.provider || 'Email'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+113
-17
@@ -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<SupabaseUser | null>(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() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<div className="hidden md:block">
|
||||
<Button
|
||||
asChild
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-primary/25"
|
||||
>
|
||||
<Link href="#contact">Get in Touch</Link>
|
||||
</Button>
|
||||
{/* CTA Button / Auth */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
{isLoading ? (
|
||||
<div className="w-24 h-10 bg-muted animate-pulse rounded-md" />
|
||||
) : user ? (
|
||||
<>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="transition-all duration-300"
|
||||
>
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleSignOut}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
aria-label="Sign out"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Link href="/auth/login">Sign in</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-primary/25"
|
||||
>
|
||||
<Link href="#contact">Get in Touch</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -109,14 +171,48 @@ export function Header() {
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<Button
|
||||
asChild
|
||||
className="w-full mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<Link href="#contact" onClick={() => setIsMobileMenuOpen(false)}>
|
||||
Get in Touch
|
||||
</Link>
|
||||
</Button>
|
||||
{user ? (
|
||||
<>
|
||||
<Button
|
||||
asChild
|
||||
className="w-full mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<Link href="/dashboard" onClick={() => setIsMobileMenuOpen(false)}>
|
||||
Dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full mt-2"
|
||||
onClick={() => {
|
||||
handleSignOut()
|
||||
setIsMobileMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full mt-4"
|
||||
>
|
||||
<Link href="/auth/login" onClick={() => setIsMobileMenuOpen(false)}>
|
||||
Sign in
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
className="w-full mt-2 bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<Link href="#contact" onClick={() => setIsMobileMenuOpen(false)}>
|
||||
Get in Touch
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -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!,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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)$).*)',
|
||||
],
|
||||
}
|
||||
+3
-1
@@ -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",
|
||||
|
||||
Generated
+116
@@ -92,6 +92,12 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: 1.2.8
|
||||
version: 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@supabase/ssr':
|
||||
specifier: ^0.9.0
|
||||
version: 0.9.0(@supabase/[email protected])
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.99.3
|
||||
version: 2.99.3
|
||||
'@vercel/analytics':
|
||||
specifier: 1.6.1
|
||||
version: 1.6.1([email protected]([email protected]([email protected]))([email protected]))([email protected])
|
||||
@@ -1097,6 +1103,35 @@ packages:
|
||||
'@radix-ui/[email protected]':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@supabase/[email protected]':
|
||||
resolution: {integrity: sha512-vMEVLA1kGGYd/kdsJSwtjiFUZM1nGfrz2DWmgMBZtocV48qL+L2+4QpIkueXyBEumMQZFEyhz57i/5zGHjvdBw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/[email protected]':
|
||||
resolution: {integrity: sha512-6tk2zrcBkzKaaBXPOG5nshn30uJNFGOH9LxOnE8i850eQmsX+jVm7vql9kTPyvUzEHwU4zdjSOkXS9M+9ukMVA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/[email protected]':
|
||||
resolution: {integrity: sha512-8HxEf+zNycj7Z8+ONhhlu+7J7Ha+L6weyCtdEeK2mN5OWJbh6n4LPU4iuJ5UlCvvNnbSXMoutY7piITEEAgl2g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/[email protected]':
|
||||
resolution: {integrity: sha512-c1azgZ2nZPczbY5k5u5iFrk1InpxN81IvNE+UBAkjrBz3yc5ALLJNkeTQwbJZT4PZBuYXEzqYGLMuh9fdTtTMg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/[email protected]':
|
||||
resolution: {integrity: sha512-UFY6otYV3yqCgV+AyHj80vNkTvbf1Gas2LW4dpbQ4ap6p6v3eB2oaDfcI99jsuJzwVBCFU4BJI+oDYyhNk1z0Q==}
|
||||
peerDependencies:
|
||||
'@supabase/supabase-js': ^2.97.0
|
||||
|
||||
'@supabase/[email protected]':
|
||||
resolution: {integrity: sha512-lOfIm4hInNcd8x0i1LWphnLKxec42wwbjs+vhaVAvR801Vda0UAMbTooUY6gfqgQb8v29GofqKuQMMTAsl6w/w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/[email protected]':
|
||||
resolution: {integrity: sha512-GuPbzoEaI51AkLw9VGhLNvnzw4PHbS3p8j2/JlvLeZNQMKwZw4aEYQIDBRtFwL5Nv7/275n9m4DHtakY8nCvgg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@swc/[email protected]':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@@ -1222,6 +1257,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
peerDependencies:
|
||||
@@ -1230,6 +1268,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@vercel/[email protected]':
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@@ -1403,6 +1448,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
|
||||
peerDependencies:
|
||||
@@ -1752,6 +1801,18 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
@@ -2626,6 +2687,49 @@ snapshots:
|
||||
|
||||
'@radix-ui/[email protected]': {}
|
||||
|
||||
'@supabase/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/[email protected]':
|
||||
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/[email protected](@supabase/[email protected])':
|
||||
dependencies:
|
||||
'@supabase/supabase-js': 2.99.3
|
||||
cookie: 1.1.1
|
||||
|
||||
'@supabase/[email protected]':
|
||||
dependencies:
|
||||
iceberg-js: 0.8.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/[email protected]':
|
||||
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/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -2727,6 +2831,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected](@types/[email protected])':
|
||||
dependencies:
|
||||
'@types/react': 19.2.14
|
||||
@@ -2735,6 +2841,10 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 22.19.15
|
||||
|
||||
'@vercel/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected])':
|
||||
optionalDependencies:
|
||||
next: 16.2.0([email protected]([email protected]))([email protected])
|
||||
@@ -2785,6 +2895,8 @@ snapshots:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -2871,6 +2983,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]([email protected]))([email protected]):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -3206,4 +3320,6 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
Reference in New Issue
Block a user