Skip to content

Commit 22036b0

Browse files
authored
improvement(waitlist): added batch send requests, fixed UI (#305)
* improvement(waitlist): added batch send requests, fixed UI * updated package lock * added protection against rate limts for resend with a delay + exponential backoff
1 parent 08c4dc9 commit 22036b0

15 files changed

Lines changed: 1918 additions & 1004 deletions

File tree

sim/app/admin/page.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,25 @@ import PasswordAuth from './password-auth'
33
export default function AdminPage() {
44
return (
55
<PasswordAuth>
6-
<div>
7-
<h1>Admin Page</h1>
6+
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 md:px-8 py-6">
7+
<div className="mb-6 px-1">
8+
<h1 className="text-2xl font-bold tracking-tight">Admin Dashboard</h1>
9+
<p className="text-muted-foreground mt-1 text-sm">
10+
Manage Sim Studio platform settings and users.
11+
</p>
12+
</div>
13+
14+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
15+
<a
16+
href="/admin/waitlist"
17+
className="border border-gray-200 dark:border-gray-800 rounded-md p-4 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"
18+
>
19+
<h2 className="text-lg font-medium">Waitlist Management</h2>
20+
<p className="text-sm text-muted-foreground mt-1">
21+
Review and manage users on the waitlist
22+
</p>
23+
</a>
24+
</div>
825
</div>
926
</PasswordAuth>
1027
)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Button } from '@/components/ui/button'
2+
import { CheckSquareIcon, SquareIcon, UserCheckIcon, XIcon } from 'lucide-react'
3+
4+
interface BatchActionsProps {
5+
hasSelectedEmails: boolean
6+
selectedCount: number
7+
loading: boolean
8+
onToggleSelectAll: () => void
9+
onClearSelections: () => void
10+
onBatchApprove: () => void
11+
entriesExist: boolean
12+
someSelected: boolean
13+
}
14+
15+
export function BatchActions({
16+
hasSelectedEmails,
17+
selectedCount,
18+
loading,
19+
onToggleSelectAll,
20+
onClearSelections,
21+
onBatchApprove,
22+
entriesExist,
23+
someSelected,
24+
}: BatchActionsProps) {
25+
if (!entriesExist) return null;
26+
27+
return (
28+
<div className="flex flex-wrap items-center gap-2 mb-2">
29+
<Button
30+
size="sm"
31+
variant={hasSelectedEmails ? "default" : "outline"}
32+
onClick={onToggleSelectAll}
33+
disabled={loading || !entriesExist}
34+
className="whitespace-nowrap h-8 px-2.5 text-xs"
35+
>
36+
{someSelected ? (
37+
<CheckSquareIcon className="h-3.5 w-3.5 mr-1.5" />
38+
) : (
39+
<SquareIcon className="h-3.5 w-3.5 mr-1.5" />
40+
)}
41+
{someSelected ? "Deselect All" : "Select All"}
42+
</Button>
43+
44+
{hasSelectedEmails && (
45+
<>
46+
<Button
47+
size="sm"
48+
variant="outline"
49+
onClick={onClearSelections}
50+
className="whitespace-nowrap h-8 px-2.5 text-xs"
51+
>
52+
<XIcon className="h-3.5 w-3.5 mr-1.5" />
53+
Clear Selection
54+
</Button>
55+
56+
<Button
57+
size="sm"
58+
variant="default"
59+
onClick={onBatchApprove}
60+
disabled={!hasSelectedEmails || loading}
61+
className="whitespace-nowrap h-8 px-2.5 text-xs"
62+
>
63+
<UserCheckIcon className="h-3.5 w-3.5 mr-1.5" />
64+
{loading ? "Processing..." : `Approve Selected (${selectedCount})`}
65+
</Button>
66+
</>
67+
)}
68+
</div>
69+
)
70+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { CheckIcon, XIcon } from 'lucide-react'
2+
import { Button } from '@/components/ui/button'
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from '@/components/ui/dialog'
11+
12+
type BatchResult = {
13+
email: string
14+
success: boolean
15+
message: string
16+
}
17+
18+
interface BatchResultsModalProps {
19+
open: boolean
20+
onOpenChange: (open: boolean) => void
21+
results: Array<BatchResult> | null
22+
onClose: () => void
23+
}
24+
25+
export function BatchResultsModal({
26+
open,
27+
onOpenChange,
28+
results,
29+
onClose,
30+
}: BatchResultsModalProps) {
31+
return (
32+
<Dialog open={open} onOpenChange={onOpenChange}>
33+
<DialogContent className="sm:max-w-[600px]">
34+
<DialogHeader>
35+
<DialogTitle>Batch Approval Results</DialogTitle>
36+
<DialogDescription>
37+
Results of the batch approval operation.
38+
</DialogDescription>
39+
</DialogHeader>
40+
<div className="max-h-[400px] overflow-y-auto">
41+
{results && results.length > 0 ? (
42+
<div className="space-y-2 pt-2">
43+
<div className="flex justify-between mb-2">
44+
<span>Total: {results.length}</span>
45+
<span>
46+
Success: {results.filter(r => r.success).length} /
47+
Failed: {results.filter(r => !r.success).length}
48+
</span>
49+
</div>
50+
{results.map((result, idx) => (
51+
<div
52+
key={idx}
53+
className={`p-2 rounded text-sm ${
54+
result.success
55+
? 'bg-green-50 border border-green-200 text-green-800'
56+
: 'bg-red-50 border border-red-200 text-red-800'
57+
}`}
58+
>
59+
<div className="flex items-center gap-2">
60+
{result.success ? <CheckIcon className="h-4 w-4" /> : <XIcon className="h-4 w-4" />}
61+
<span className="font-medium">{result.email}</span>
62+
</div>
63+
<div className="ml-6 text-xs mt-1">{result.message}</div>
64+
</div>
65+
))}
66+
</div>
67+
) : (
68+
<div className="py-4 text-center text-gray-500">No results to display</div>
69+
)}
70+
</div>
71+
<DialogFooter>
72+
<Button onClick={onClose}>
73+
Close
74+
</Button>
75+
</DialogFooter>
76+
</DialogContent>
77+
</Dialog>
78+
)
79+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Button } from '@/components/ui/button'
2+
import { ReactNode } from 'react'
3+
4+
interface FilterButtonProps {
5+
active: boolean
6+
onClick: () => void
7+
icon: ReactNode
8+
label: string
9+
className?: string
10+
}
11+
12+
export function FilterButton({ active, onClick, icon, label, className }: FilterButtonProps) {
13+
return (
14+
<Button
15+
variant={active ? 'default' : 'ghost'}
16+
size="sm"
17+
onClick={onClick}
18+
className={`flex items-center gap-1.5 h-9 px-3 ${className || ''}`}
19+
>
20+
{icon}
21+
<span>{label}</span>
22+
</Button>
23+
)
24+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
UserIcon,
3+
UserCheckIcon,
4+
UserXIcon,
5+
CheckIcon
6+
} from 'lucide-react'
7+
import { FilterButton } from './components/filter-button'
8+
9+
interface FilterBarProps {
10+
currentStatus: string
11+
onStatusChange: (status: string) => void
12+
}
13+
14+
export function FilterBar({ currentStatus, onStatusChange }: FilterBarProps) {
15+
return (
16+
<div className="flex flex-wrap items-center gap-1.5">
17+
<FilterButton
18+
active={currentStatus === 'all'}
19+
onClick={() => onStatusChange('all')}
20+
icon={<UserIcon className="h-3.5 w-3.5" />}
21+
label="All"
22+
className={
23+
currentStatus === 'all'
24+
? 'bg-blue-100 text-blue-900 hover:bg-blue-200 hover:text-blue-900'
25+
: ''
26+
}
27+
/>
28+
<FilterButton
29+
active={currentStatus === 'pending'}
30+
onClick={() => onStatusChange('pending')}
31+
icon={<UserIcon className="h-3.5 w-3.5" />}
32+
label="Pending"
33+
className={
34+
currentStatus === 'pending'
35+
? 'bg-amber-100 text-amber-900 hover:bg-amber-200 hover:text-amber-900'
36+
: ''
37+
}
38+
/>
39+
<FilterButton
40+
active={currentStatus === 'approved'}
41+
onClick={() => onStatusChange('approved')}
42+
icon={<UserCheckIcon className="h-3.5 w-3.5" />}
43+
label="Approved"
44+
className={
45+
currentStatus === 'approved'
46+
? 'bg-green-100 text-green-900 hover:bg-green-200 hover:text-green-900'
47+
: ''
48+
}
49+
/>
50+
<FilterButton
51+
active={currentStatus === 'rejected'}
52+
onClick={() => onStatusChange('rejected')}
53+
icon={<UserXIcon className="h-3.5 w-3.5" />}
54+
label="Rejected"
55+
className={
56+
currentStatus === 'rejected'
57+
? 'bg-red-100 text-red-900 hover:bg-red-200 hover:text-red-900'
58+
: ''
59+
}
60+
/>
61+
<FilterButton
62+
active={currentStatus === 'signed_up'}
63+
onClick={() => onStatusChange('signed_up')}
64+
icon={<CheckIcon className="h-3.5 w-3.5" />}
65+
label="Signed Up"
66+
className={
67+
currentStatus === 'signed_up'
68+
? 'bg-purple-100 text-purple-900 hover:bg-purple-200 hover:text-purple-900'
69+
: ''
70+
}
71+
/>
72+
</div>
73+
)
74+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Button } from '@/components/ui/button'
2+
import {
3+
ChevronLeftIcon,
4+
ChevronRightIcon,
5+
ChevronsLeftIcon,
6+
ChevronsRightIcon,
7+
} from 'lucide-react'
8+
9+
interface PaginationProps {
10+
page: number
11+
totalItems: number
12+
itemsPerPage: number
13+
loading: boolean
14+
onFirstPage: () => void
15+
onPrevPage: () => void
16+
onNextPage: () => void
17+
onLastPage: () => void
18+
}
19+
20+
export function Pagination({
21+
page,
22+
totalItems,
23+
itemsPerPage,
24+
loading,
25+
onFirstPage,
26+
onPrevPage,
27+
onNextPage,
28+
onLastPage,
29+
}: PaginationProps) {
30+
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage))
31+
32+
return (
33+
<div className="flex items-center justify-center gap-1.5 my-3 pb-1">
34+
<div className="flex items-center gap-1">
35+
<Button
36+
variant="outline"
37+
size="sm"
38+
onClick={onFirstPage}
39+
disabled={page === 1 || loading}
40+
title="First Page"
41+
className="h-8 w-8 p-0"
42+
>
43+
<ChevronsLeftIcon className="h-3.5 w-3.5" />
44+
</Button>
45+
<Button
46+
variant="outline"
47+
size="sm"
48+
onClick={onPrevPage}
49+
disabled={page === 1 || loading}
50+
className="h-8 px-2 text-xs"
51+
>
52+
<ChevronLeftIcon className="h-3.5 w-3.5 mr-1" />
53+
Prev
54+
</Button>
55+
</div>
56+
57+
<span className="text-xs text-muted-foreground mx-2">
58+
Page {page} of {totalPages}
59+
&nbsp;•&nbsp;
60+
{totalItems} total entries
61+
</span>
62+
63+
<div className="flex items-center gap-1">
64+
<Button
65+
variant="outline"
66+
size="sm"
67+
onClick={onNextPage}
68+
disabled={page >= totalPages || loading}
69+
className="h-8 px-2 text-xs"
70+
>
71+
Next
72+
<ChevronRightIcon className="h-3.5 w-3.5 ml-1" />
73+
</Button>
74+
<Button
75+
variant="outline"
76+
size="sm"
77+
onClick={onLastPage}
78+
disabled={page >= totalPages || loading}
79+
title="Last Page"
80+
className="h-8 w-8 p-0"
81+
>
82+
<ChevronsRightIcon className="h-3.5 w-3.5" />
83+
</Button>
84+
</div>
85+
</div>
86+
)
87+
}

0 commit comments

Comments
 (0)