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:
v0
2026-03-22 23:07:11 +00:00
co-authored by ASOwnerYT
parent 84200ac175
commit 1386f6e549
13 changed files with 1073 additions and 18 deletions
+19
View File
@@ -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`)
}
+55
View File
@@ -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>
)
}
+206
View File
@@ -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&apos;t have an account?{' '}
<Link
href="/auth/sign-up"
className="text-primary hover:underline font-medium"
>
Sign up
</Link>
</p>
</div>
</div>
</div>
)
}
+55
View File
@@ -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&apos;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&apos;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>
)
}
+233
View File
@@ -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>
)
}
+134
View File
@@ -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&apos;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>
)
}
+99 -3
View File
@@ -2,12 +2,16 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import Link from "next/link" 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 { Button } from "@/components/ui/button"
import { createClient } from "@/lib/supabase/client"
import type { User as SupabaseUser } from "@supabase/supabase-js"
export function Header() { export function Header() {
const [isScrolled, setIsScrolled] = useState(false) const [isScrolled, setIsScrolled] = useState(false)
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const [user, setUser] = useState<SupabaseUser | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
@@ -17,6 +21,29 @@ export function Header() {
return () => window.removeEventListener("scroll", handleScroll) 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 = [ const navLinks = [
{ href: "#services", label: "Services" }, { href: "#services", label: "Services" },
{ href: "#features", label: "Why Us" }, { href: "#features", label: "Why Us" },
@@ -59,14 +86,49 @@ export function Header() {
))} ))}
</div> </div>
{/* CTA Button */} {/* CTA Button / Auth */}
<div className="hidden md:block"> <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 <Button
asChild 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" 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> <Link href="#contact">Get in Touch</Link>
</Button> </Button>
</>
)}
</div> </div>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
@@ -109,14 +171,48 @@ export function Header() {
{link.label} {link.label}
</Link> </Link>
))} ))}
{user ? (
<>
<Button <Button
asChild asChild
className="w-full mt-4 bg-primary text-primary-foreground hover:bg-primary/90" 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)}> <Link href="#contact" onClick={() => setIsMobileMenuOpen(false)}>
Get in Touch Get in Touch
</Link> </Link>
</Button> </Button>
</>
)}
</div> </div>
</div> </div>
</header> </header>
+8
View File
@@ -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!,
)
}
+77
View File
@@ -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
}
+34
View File
@@ -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.
}
},
},
},
)
}
+20
View File
@@ -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
View File
@@ -10,7 +10,6 @@
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@vercel/analytics": "1.6.1",
"@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-accordion": "1.2.12",
"@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-aspect-ratio": "1.1.8", "@radix-ui/react-aspect-ratio": "1.1.8",
@@ -38,6 +37,9 @@
"@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle": "1.1.10",
"@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toggle-group": "1.1.11",
"@radix-ui/react-tooltip": "1.2.8", "@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", "autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
+116
View File
@@ -92,6 +92,12 @@ importers:
'@radix-ui/react-tooltip': '@radix-ui/react-tooltip':
specifier: 1.2.8 specifier: 1.2.8
version: 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]) 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': '@vercel/analytics':
specifier: 1.6.1 specifier: 1.6.1
version: 1.6.1([email protected]([email protected]([email protected]))([email protected]))([email protected]) version: 1.6.1([email protected]([email protected]([email protected]))([email protected]))([email protected])
@@ -1097,6 +1103,35 @@ packages:
'@radix-ui/[email protected]': '@radix-ui/[email protected]':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} 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]': '@swc/[email protected]':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -1222,6 +1257,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
'@types/[email protected]':
resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies: peerDependencies:
@@ -1230,6 +1268,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
'@types/[email protected]':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@vercel/[email protected]': '@vercel/[email protected]':
resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==} resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==}
peerDependencies: peerDependencies:
@@ -1296,6 +1337,10 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^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]: [email protected]:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1403,6 +1448,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 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]: [email protected]:
resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
peerDependencies: peerDependencies:
@@ -1752,6 +1801,18 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} 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]: [email protected]:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -2626,6 +2687,49 @@ snapshots:
'@radix-ui/[email protected]': {} '@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]': '@swc/[email protected]':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -2727,6 +2831,8 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/[email protected]': {}
'@types/[email protected](@types/[email protected])': '@types/[email protected](@types/[email protected])':
dependencies: dependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
@@ -2735,6 +2841,10 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 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])': '@vercel/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected])':
optionalDependencies: optionalDependencies:
next: 16.2.0([email protected]([email protected]))([email protected]) next: 16.2.0([email protected]([email protected]))([email protected])
@@ -2785,6 +2895,8 @@ snapshots:
- '@types/react' - '@types/react'
- '@types/react-dom' - '@types/react-dom'
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -2871,6 +2983,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]([email protected]([email protected]))([email protected]): [email protected]([email protected]([email protected]))([email protected]):
dependencies: dependencies:
react: 19.2.4 react: 19.2.4
@@ -3206,4 +3320,6 @@ snapshots:
d3-time: 3.1.0 d3-time: 3.1.0
d3-timer: 3.0.1 d3-timer: 3.0.1
[email protected]: {}
[email protected]: {} [email protected]: {}