improvement(waitlist): add signed_up status to indicate who signed up after being approved off of waitlist

This commit is contained in:
Waleed Latif
2025-03-29 13:32:51 -07:00
parent 0eb99f7291
commit 272a486bcc
6 changed files with 249 additions and 65 deletions
+31
View File
@@ -56,6 +56,7 @@ function SignupFormContent({
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const [email, setEmail] = useState('')
const [waitlistToken, setWaitlistToken] = useState('')
useEffect(() => {
setMounted(true)
@@ -63,8 +64,38 @@ function SignupFormContent({
if (emailParam) {
setEmail(emailParam)
}
// Check for waitlist token
const tokenParam = searchParams.get('token')
if (tokenParam) {
setWaitlistToken(tokenParam)
// Verify the token and get the email
verifyWaitlistToken(tokenParam)
}
}, [searchParams])
// Verify waitlist token and pre-fill email
const verifyWaitlistToken = async (token: string) => {
try {
const response = await fetch('/api/auth/verify-waitlist-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
})
const data = await response.json()
if (data.success && data.email) {
setEmail(data.email)
}
} catch (error) {
console.error('Error verifying waitlist token:', error)
// Continue regardless of errors - we don't want to block sign up
}
}
// Validate password and return array of error messages
const validatePassword = (passwordValue: string): string[] => {
const errors: string[] = []
+1 -1
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand'
// Define types inline since types.ts was deleted
export type WaitlistStatus = 'pending' | 'approved' | 'rejected'
export type WaitlistStatus = 'pending' | 'approved' | 'rejected' | 'signed_up'
export interface WaitlistEntry {
id: string
+99 -62
View File
@@ -346,50 +346,66 @@ export function WaitlistTable() {
{/* Filter bar - similar to logs.tsx */}
<div className="border-b px-6">
<div className="flex flex-wrap items-center gap-2 py-3">
<FilterButton
active={status === 'all'}
onClick={() => handleStatusChange('all')}
icon={<UserIcon className="h-4 w-4" />}
label="All"
className={
status === 'all'
? 'bg-blue-100 text-blue-900 hover:bg-blue-200 hover:text-blue-900'
: ''
}
/>
<FilterButton
active={status === 'pending'}
onClick={() => handleStatusChange('pending')}
icon={<UserIcon className="h-4 w-4" />}
label="Pending"
className={
status === 'pending'
? 'bg-amber-100 text-amber-900 hover:bg-amber-200 hover:text-amber-900'
: ''
}
/>
<FilterButton
active={status === 'approved'}
onClick={() => handleStatusChange('approved')}
icon={<UserCheckIcon className="h-4 w-4" />}
label="Approved"
className={
status === 'approved'
? 'bg-green-100 text-green-900 hover:bg-green-200 hover:text-green-900'
: ''
}
/>
<FilterButton
active={status === 'rejected'}
onClick={() => handleStatusChange('rejected')}
icon={<UserXIcon className="h-4 w-4" />}
label="Rejected"
className={
status === 'rejected'
? 'bg-red-100 text-red-900 hover:bg-red-200 hover:text-red-900'
: ''
}
/>
<div className="flex justify-between items-center">
{/* Filter buttons */}
<div className="flex items-center gap-2 overflow-x-auto">
<FilterButton
active={status === 'all'}
onClick={() => handleStatusChange('all')}
icon={<UserIcon className="h-4 w-4" />}
label="All"
className={
status === 'all'
? 'bg-blue-100 text-blue-900 hover:bg-blue-200 hover:text-blue-900'
: ''
}
/>
<FilterButton
active={status === 'pending'}
onClick={() => handleStatusChange('pending')}
icon={<UserIcon className="h-4 w-4" />}
label="Pending"
className={
status === 'pending'
? 'bg-amber-100 text-amber-900 hover:bg-amber-200 hover:text-amber-900'
: ''
}
/>
<FilterButton
active={status === 'approved'}
onClick={() => handleStatusChange('approved')}
icon={<UserCheckIcon className="h-4 w-4" />}
label="Approved"
className={
status === 'approved'
? 'bg-green-100 text-green-900 hover:bg-green-200 hover:text-green-900'
: ''
}
/>
<FilterButton
active={status === 'rejected'}
onClick={() => handleStatusChange('rejected')}
icon={<UserXIcon className="h-4 w-4" />}
label="Rejected"
className={
status === 'rejected'
? 'bg-red-100 text-red-900 hover:bg-red-200 hover:text-red-900'
: ''
}
/>
<FilterButton
active={status === 'signed_up'}
onClick={() => handleStatusChange('signed_up')}
icon={<CheckIcon className="h-4 w-4" />}
label="Signed Up"
className={
status === 'signed_up'
? 'bg-purple-100 text-purple-900 hover:bg-purple-200 hover:text-purple-900'
: ''
}
/>
</div>
</div>
</div>
</div>
@@ -557,24 +573,45 @@ export function WaitlistTable() {
</TooltipProvider>
</TableCell>
<TableCell>
<Badge
variant={
entry.status === 'approved'
? 'default'
: entry.status === 'rejected'
? 'destructive'
: 'secondary'
}
className={
entry.status === 'approved'
? 'bg-green-100 text-green-800 border border-green-200 hover:bg-green-200'
: entry.status === 'rejected'
? 'bg-red-100 text-red-800 border border-red-200 hover:bg-red-200'
: 'bg-amber-100 text-amber-800 border border-amber-200 hover:bg-amber-200'
}
>
{entry.status}
</Badge>
{/* Status badge */}
<div className="py-2.5 px-4">
{entry.status === 'pending' && (
<Badge
variant="outline"
className="bg-amber-100 text-amber-800 border border-amber-200 hover:bg-amber-200"
>
<InfoIcon className="mr-1 h-3 w-3" />
Pending
</Badge>
)}
{entry.status === 'approved' && (
<Badge
variant="outline"
className="bg-green-100 text-green-800 border border-green-200 hover:bg-green-200"
>
<UserCheckIcon className="mr-1 h-3 w-3" />
Approved
</Badge>
)}
{entry.status === 'rejected' && (
<Badge
variant="outline"
className="bg-red-100 text-red-800 border border-red-200 hover:bg-red-200"
>
<UserXIcon className="mr-1 h-3 w-3" />
Rejected
</Badge>
)}
{entry.status === 'signed_up' && (
<Badge
variant="outline"
className="bg-purple-100 text-purple-800 border border-purple-200 hover:bg-purple-200"
>
<CheckIcon className="mr-1 h-3 w-3" />
Signed Up
</Badge>
)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end space-x-2">
+1 -1
View File
@@ -9,7 +9,7 @@ const logger = new Logger('WaitlistAPI')
const getQuerySchema = z.object({
page: z.coerce.number().optional().default(1),
limit: z.coerce.number().optional().default(20),
status: z.enum(['all', 'pending', 'approved', 'rejected']).optional(),
status: z.enum(['all', 'pending', 'approved', 'rejected', 'signed_up']).optional(),
search: z.string().optional(),
})
@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server'
import { Logger } from '@/lib/logs/console-logger'
import { markWaitlistUserAsSignedUp } from '@/lib/waitlist/service'
import { verifyToken } from '@/lib/waitlist/token'
const logger = new Logger('VerifyWaitlistToken')
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { token } = body
if (!token) {
return NextResponse.json(
{
success: false,
message: 'Token is required',
},
{ status: 400 }
)
}
// Verify token
const decodedToken = await verifyToken(token)
if (!decodedToken) {
return NextResponse.json(
{
success: false,
message: 'Invalid or expired token',
},
{ status: 400 }
)
}
// Check if it's a waitlist approval token
if (decodedToken.type !== 'waitlist-approval') {
return NextResponse.json(
{
success: false,
message: 'Invalid token type',
},
{ status: 400 }
)
}
const email = decodedToken.email
// Mark the user as signed up
const result = await markWaitlistUserAsSignedUp(email)
if (!result.success) {
logger.warn(`Failed to mark user as signed up: ${result.message}`, { email })
// Continue even if this fails - we still want to allow the user to sign up
} else {
logger.info(`Successfully marked waitlist user as signed up`, { email })
}
return NextResponse.json({
success: true,
email: email,
})
} catch (error) {
logger.error('Error verifying waitlist token:', error)
return NextResponse.json(
{
success: false,
message: 'An error occurred while verifying the token',
},
{ status: 500 }
)
}
}
+44 -1
View File
@@ -11,7 +11,7 @@ import { db } from '@/db'
import { waitlist } from '@/db/schema'
// Define types for better type safety
export type WaitlistStatus = 'pending' | 'approved' | 'rejected'
export type WaitlistStatus = 'pending' | 'approved' | 'rejected' | 'signed_up'
export interface WaitlistEntry {
id: string
@@ -310,3 +310,46 @@ export async function verifyWaitlistToken(
return { valid: false }
}
}
// Mark a user as signed up after they create an account
export async function markWaitlistUserAsSignedUp(
email: string
): Promise<{ success: boolean; message: string }> {
try {
const { user, normalizedEmail } = await findUserByEmail(email)
if (!user) {
return {
success: false,
message: 'User not found in waitlist',
}
}
if (user.status !== 'approved') {
return {
success: false,
message: 'User is not in approved status',
}
}
// Update status to signed_up
await db
.update(waitlist)
.set({
status: 'signed_up',
updatedAt: new Date(),
})
.where(eq(waitlist.email, normalizedEmail))
return {
success: true,
message: 'User marked as signed up',
}
} catch (error) {
console.error('Error marking waitlist user as signed up:', error)
return {
success: false,
message: 'An error occurred while updating user status',
}
}
}