From 2d57e6d33736e9cd6f71c60378719875e55d37e2 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Thu, 22 Jan 2026 23:21:36 -0300 Subject: [PATCH] fix: wrap useSearchParams() in Suspense boundary for login and signup pages Next.js App Router requires components using useSearchParams() to be wrapped in a Suspense boundary for static generation. Split login and signup pages into server component wrappers with Suspense and client form components. Co-Authored-By: Claude Opus 4.5 --- src/app/login/LoginForm.tsx | 128 ++++++++++++++++++ src/app/login/page.tsx | 137 +++---------------- src/app/signup/SignupForm.tsx | 236 ++++++++++++++++++++++++++++++++ src/app/signup/page.tsx | 245 +++------------------------------- 4 files changed, 394 insertions(+), 352 deletions(-) create mode 100644 src/app/login/LoginForm.tsx create mode 100644 src/app/signup/SignupForm.tsx diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx new file mode 100644 index 0000000..29663df --- /dev/null +++ b/src/app/login/LoginForm.tsx @@ -0,0 +1,128 @@ +'use client' + +import { useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Link from 'next/link' +import { createClient } from '@/lib/supabase/client' + +export default function LoginForm() { + const router = useRouter() + const searchParams = useSearchParams() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + // Check for success message from password reset + const message = searchParams.get('message') + const successMessage = message === 'password_reset_success' + ? 'Your password has been reset successfully. Please sign in with your new password.' + : null + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + setLoading(true) + + const supabase = createClient() + + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + + if (error) { + setError(error.message) + setLoading(false) + return + } + + router.push('/dashboard') + } + + return ( +
+
+

+ WebVNWrite +

+

+ Sign in to your account +

+
+ +
+ {successMessage && ( +
+

{successMessage}

+
+ )} + + {error && ( +
+

{error}

+
+ )} + +
+
+ + setEmail(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="••••••••" + /> +
+
+ +
+ + Forgot your password? + +
+ + +
+
+ ) +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 6a67346..f9033de 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,130 +1,23 @@ -'use client' - -import { useState } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import Link from 'next/link' -import { createClient } from '@/lib/supabase/client' +import { Suspense } from 'react' +import LoginForm from './LoginForm' export default function LoginPage() { - const router = useRouter() - const searchParams = useSearchParams() - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState(null) - const [loading, setLoading] = useState(false) - - // Check for success message from password reset - const message = searchParams.get('message') - const successMessage = message === 'password_reset_success' - ? 'Your password has been reset successfully. Please sign in with your new password.' - : null - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setError(null) - setLoading(true) - - const supabase = createClient() - - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }) - - if (error) { - setError(error.message) - setLoading(false) - return - } - - router.push('/dashboard') - } - return (
-
-
-

- WebVNWrite -

-

- Sign in to your account -

-
- -
- {successMessage && ( -
-

{successMessage}

-
- )} - - {error && ( -
-

{error}

-
- )} - -
-
- - setEmail(e.target.value)} - className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" - placeholder="you@example.com" - /> -
- -
- - setPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" - placeholder="••••••••" - /> -
+ +

+ WebVNWrite +

+

+ Loading... +

- -
- - Forgot your password? - -
- - -
-
+ } + > + +
) } diff --git a/src/app/signup/SignupForm.tsx b/src/app/signup/SignupForm.tsx new file mode 100644 index 0000000..e839439 --- /dev/null +++ b/src/app/signup/SignupForm.tsx @@ -0,0 +1,236 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Link from 'next/link' +import { createClient } from '@/lib/supabase/client' + +export default function SignupForm() { + const router = useRouter() + const searchParams = useSearchParams() + // Pre-fill email if provided in URL (from invite link) + const [email, setEmail] = useState(searchParams.get('email') ?? '') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + // Handle invite/signup token from URL hash + // Supabase adds tokens to the URL hash after redirect + const handleTokenFromUrl = async () => { + const hash = window.location.hash + if (hash) { + const params = new URLSearchParams(hash.substring(1)) + const accessToken = params.get('access_token') + const refreshToken = params.get('refresh_token') + const type = params.get('type') + + if (accessToken && refreshToken && (type === 'invite' || type === 'signup')) { + const supabase = createClient() + const { error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }) + + if (error) { + setError('Invalid or expired invite link. Please request a new invitation.') + return + } + + // Get the user's email from the session + const { data: { user } } = await supabase.auth.getUser() + if (user?.email) { + setEmail(user.email) + } + } + } + } + + handleTokenFromUrl() + }, [searchParams]) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + + // Validate passwords match + if (password !== confirmPassword) { + setError('Passwords do not match') + return + } + + // Validate password length + if (password.length < 6) { + setError('Password must be at least 6 characters') + return + } + + setLoading(true) + + const supabase = createClient() + + // Check if user already has a session (from invite link) + const { data: { session } } = await supabase.auth.getSession() + + if (session) { + // User was invited and has a session - update their password + const { error: updateError } = await supabase.auth.updateUser({ + password, + }) + + if (updateError) { + setError(updateError.message) + setLoading(false) + return + } + + // Create profile record + const { error: profileError } = await supabase.from('profiles').upsert({ + id: session.user.id, + email: session.user.email, + display_name: session.user.email?.split('@')[0] || 'User', + is_admin: false, + }) + + if (profileError) { + setError(profileError.message) + setLoading(false) + return + } + + router.push('/dashboard') + } else { + // Regular signup flow (if allowed) + const { data, error } = await supabase.auth.signUp({ + email, + password, + }) + + if (error) { + setError(error.message) + setLoading(false) + return + } + + if (data.user) { + // Create profile record + const { error: profileError } = await supabase.from('profiles').upsert({ + id: data.user.id, + email: data.user.email, + display_name: email.split('@')[0] || 'User', + is_admin: false, + }) + + if (profileError) { + setError(profileError.message) + setLoading(false) + return + } + + router.push('/dashboard') + } + } + } + + return ( +
+
+

+ WebVNWrite +

+

+ Complete your account setup +

+
+ +
+ {error && ( +
+

{error}

+
+ )} + +
+
+ + setEmail(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="••••••••" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="••••••••" + /> +
+
+ + + +

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

+
+
+ ) +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index fb1a733..de79472 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,238 +1,23 @@ -'use client' - -import { useState, useEffect } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import Link from 'next/link' -import { createClient } from '@/lib/supabase/client' +import { Suspense } from 'react' +import SignupForm from './SignupForm' export default function SignupPage() { - const router = useRouter() - const searchParams = useSearchParams() - // Pre-fill email if provided in URL (from invite link) - const [email, setEmail] = useState(searchParams.get('email') ?? '') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [error, setError] = useState(null) - const [loading, setLoading] = useState(false) - - useEffect(() => { - // Handle invite/signup token from URL hash - // Supabase adds tokens to the URL hash after redirect - const handleTokenFromUrl = async () => { - const hash = window.location.hash - if (hash) { - const params = new URLSearchParams(hash.substring(1)) - const accessToken = params.get('access_token') - const refreshToken = params.get('refresh_token') - const type = params.get('type') - - if (accessToken && refreshToken && (type === 'invite' || type === 'signup')) { - const supabase = createClient() - const { error } = await supabase.auth.setSession({ - access_token: accessToken, - refresh_token: refreshToken, - }) - - if (error) { - setError('Invalid or expired invite link. Please request a new invitation.') - return - } - - // Get the user's email from the session - const { data: { user } } = await supabase.auth.getUser() - if (user?.email) { - setEmail(user.email) - } - } - } - } - - handleTokenFromUrl() - }, [searchParams]) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setError(null) - - // Validate passwords match - if (password !== confirmPassword) { - setError('Passwords do not match') - return - } - - // Validate password length - if (password.length < 6) { - setError('Password must be at least 6 characters') - return - } - - setLoading(true) - - const supabase = createClient() - - // Check if user already has a session (from invite link) - const { data: { session } } = await supabase.auth.getSession() - - if (session) { - // User was invited and has a session - update their password - const { error: updateError } = await supabase.auth.updateUser({ - password, - }) - - if (updateError) { - setError(updateError.message) - setLoading(false) - return - } - - // Create profile record - const { error: profileError } = await supabase.from('profiles').upsert({ - id: session.user.id, - email: session.user.email, - display_name: session.user.email?.split('@')[0] || 'User', - is_admin: false, - }) - - if (profileError) { - setError(profileError.message) - setLoading(false) - return - } - - router.push('/dashboard') - } else { - // Regular signup flow (if allowed) - const { data, error } = await supabase.auth.signUp({ - email, - password, - }) - - if (error) { - setError(error.message) - setLoading(false) - return - } - - if (data.user) { - // Create profile record - const { error: profileError } = await supabase.from('profiles').upsert({ - id: data.user.id, - email: data.user.email, - display_name: email.split('@')[0] || 'User', - is_admin: false, - }) - - if (profileError) { - setError(profileError.message) - setLoading(false) - return - } - - router.push('/dashboard') - } - } - } - return (
-
-
-

- WebVNWrite -

-

- Complete your account setup -

-
- -
- {error && ( -
-

{error}

-
- )} - -
-
- - setEmail(e.target.value)} - className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" - placeholder="you@example.com" - /> -
- -
- - setPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" - placeholder="••••••••" - /> -
- -
- - setConfirmPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" - placeholder="••••••••" - /> -
+ +

+ WebVNWrite +

+

+ Loading... +

- - - -

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

-
-
+ } + > + +
) }