Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions sim/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,25 @@ import PasswordAuth from './password-auth'
export default function AdminPage() {
return (
<PasswordAuth>
<div>
<h1>Admin Page</h1>
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 md:px-8 py-6">
<div className="mb-6 px-1">
<h1 className="text-2xl font-bold tracking-tight">Admin Dashboard</h1>
<p className="text-muted-foreground mt-1 text-sm">
Manage Sim Studio platform settings and users.
</p>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<a
href="/admin/waitlist"
className="border border-gray-200 dark:border-gray-800 rounded-md p-4 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"
>
<h2 className="text-lg font-medium">Waitlist Management</h2>
<p className="text-sm text-muted-foreground mt-1">
Review and manage users on the waitlist
</p>
</a>
</div>
</div>
</PasswordAuth>
)
Expand Down
70 changes: 70 additions & 0 deletions sim/app/admin/waitlist/components/batch-actions/batch-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Button } from '@/components/ui/button'
import { CheckSquareIcon, SquareIcon, UserCheckIcon, XIcon } from 'lucide-react'

interface BatchActionsProps {
hasSelectedEmails: boolean
selectedCount: number
loading: boolean
onToggleSelectAll: () => void
onClearSelections: () => void
onBatchApprove: () => void
entriesExist: boolean
someSelected: boolean
}

export function BatchActions({
hasSelectedEmails,
selectedCount,
loading,
onToggleSelectAll,
onClearSelections,
onBatchApprove,
entriesExist,
someSelected,
}: BatchActionsProps) {
if (!entriesExist) return null;

return (
<div className="flex flex-wrap items-center gap-2 mb-2">
<Button
size="sm"
variant={hasSelectedEmails ? "default" : "outline"}
onClick={onToggleSelectAll}
disabled={loading || !entriesExist}
className="whitespace-nowrap h-8 px-2.5 text-xs"
>
{someSelected ? (
<CheckSquareIcon className="h-3.5 w-3.5 mr-1.5" />
) : (
<SquareIcon className="h-3.5 w-3.5 mr-1.5" />
)}
{someSelected ? "Deselect All" : "Select All"}
</Button>

{hasSelectedEmails && (
<>
<Button
size="sm"
variant="outline"
onClick={onClearSelections}
className="whitespace-nowrap h-8 px-2.5 text-xs"
>
<XIcon className="h-3.5 w-3.5 mr-1.5" />
Clear Selection
</Button>

<Button
size="sm"
variant="default"
onClick={onBatchApprove}
disabled={!hasSelectedEmails || loading}
className="whitespace-nowrap h-8 px-2.5 text-xs"
>
<UserCheckIcon className="h-3.5 w-3.5 mr-1.5" />
{loading ? "Processing..." : `Approve Selected (${selectedCount})`}
</Button>
</>
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { CheckIcon, XIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'

type BatchResult = {
email: string
success: boolean
message: string
}

interface BatchResultsModalProps {
open: boolean
onOpenChange: (open: boolean) => void
results: Array<BatchResult> | null
onClose: () => void
}

export function BatchResultsModal({
open,
onOpenChange,
results,
onClose,
}: BatchResultsModalProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Batch Approval Results</DialogTitle>
<DialogDescription>
Results of the batch approval operation.
</DialogDescription>
</DialogHeader>
<div className="max-h-[400px] overflow-y-auto">
{results && results.length > 0 ? (
<div className="space-y-2 pt-2">
<div className="flex justify-between mb-2">
<span>Total: {results.length}</span>
<span>
Success: {results.filter(r => r.success).length} /
Failed: {results.filter(r => !r.success).length}
</span>
</div>
{results.map((result, idx) => (
<div
key={idx}
className={`p-2 rounded text-sm ${
result.success
? 'bg-green-50 border border-green-200 text-green-800'
: 'bg-red-50 border border-red-200 text-red-800'
}`}
>
<div className="flex items-center gap-2">
{result.success ? <CheckIcon className="h-4 w-4" /> : <XIcon className="h-4 w-4" />}
<span className="font-medium">{result.email}</span>
</div>
<div className="ml-6 text-xs mt-1">{result.message}</div>
</div>
))}
</div>
) : (
<div className="py-4 text-center text-gray-500">No results to display</div>
)}
</div>
<DialogFooter>
<Button onClick={onClose}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Button } from '@/components/ui/button'
import { ReactNode } from 'react'

interface FilterButtonProps {
active: boolean
onClick: () => void
icon: ReactNode
label: string
className?: string
}

export function FilterButton({ active, onClick, icon, label, className }: FilterButtonProps) {
return (
<Button
variant={active ? 'default' : 'ghost'}
size="sm"
onClick={onClick}
className={`flex items-center gap-1.5 h-9 px-3 ${className || ''}`}
>
{icon}
<span>{label}</span>
</Button>
)
}
74 changes: 74 additions & 0 deletions sim/app/admin/waitlist/components/filter-bar/filter-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
UserIcon,
UserCheckIcon,
UserXIcon,
CheckIcon
} from 'lucide-react'
import { FilterButton } from './components/filter-button'

interface FilterBarProps {
currentStatus: string
onStatusChange: (status: string) => void
}

export function FilterBar({ currentStatus, onStatusChange }: FilterBarProps) {
return (
<div className="flex flex-wrap items-center gap-1.5">
<FilterButton
active={currentStatus === 'all'}
onClick={() => onStatusChange('all')}
icon={<UserIcon className="h-3.5 w-3.5" />}
label="All"
className={
currentStatus === 'all'
? 'bg-blue-100 text-blue-900 hover:bg-blue-200 hover:text-blue-900'
: ''
}
/>
<FilterButton
active={currentStatus === 'pending'}
onClick={() => onStatusChange('pending')}
icon={<UserIcon className="h-3.5 w-3.5" />}
label="Pending"
className={
currentStatus === 'pending'
? 'bg-amber-100 text-amber-900 hover:bg-amber-200 hover:text-amber-900'
: ''
}
/>
<FilterButton
active={currentStatus === 'approved'}
onClick={() => onStatusChange('approved')}
icon={<UserCheckIcon className="h-3.5 w-3.5" />}
label="Approved"
className={
currentStatus === 'approved'
? 'bg-green-100 text-green-900 hover:bg-green-200 hover:text-green-900'
: ''
}
/>
<FilterButton
active={currentStatus === 'rejected'}
onClick={() => onStatusChange('rejected')}
icon={<UserXIcon className="h-3.5 w-3.5" />}
label="Rejected"
className={
currentStatus === 'rejected'
? 'bg-red-100 text-red-900 hover:bg-red-200 hover:text-red-900'
: ''
}
/>
<FilterButton
active={currentStatus === 'signed_up'}
onClick={() => onStatusChange('signed_up')}
icon={<CheckIcon className="h-3.5 w-3.5" />}
label="Signed Up"
className={
currentStatus === 'signed_up'
? 'bg-purple-100 text-purple-900 hover:bg-purple-200 hover:text-purple-900'
: ''
}
/>
</div>
)
}
87 changes: 87 additions & 0 deletions sim/app/admin/waitlist/components/pagination/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Button } from '@/components/ui/button'
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronsLeftIcon,
ChevronsRightIcon,
} from 'lucide-react'

interface PaginationProps {
page: number
totalItems: number
itemsPerPage: number
loading: boolean
onFirstPage: () => void
onPrevPage: () => void
onNextPage: () => void
onLastPage: () => void
}

export function Pagination({
page,
totalItems,
itemsPerPage,
loading,
onFirstPage,
onPrevPage,
onNextPage,
onLastPage,
}: PaginationProps) {
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage))

return (
<div className="flex items-center justify-center gap-1.5 my-3 pb-1">
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={onFirstPage}
disabled={page === 1 || loading}
title="First Page"
className="h-8 w-8 p-0"
>
<ChevronsLeftIcon className="h-3.5 w-3.5" />
</Button>
<Button
variant="outline"
size="sm"
onClick={onPrevPage}
disabled={page === 1 || loading}
className="h-8 px-2 text-xs"
>
<ChevronLeftIcon className="h-3.5 w-3.5 mr-1" />
Prev
</Button>
</div>

<span className="text-xs text-muted-foreground mx-2">
Page {page} of {totalPages}
&nbsp;•&nbsp;
{totalItems} total entries
</span>

<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={onNextPage}
disabled={page >= totalPages || loading}
className="h-8 px-2 text-xs"
>
Next
<ChevronRightIcon className="h-3.5 w-3.5 ml-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={onLastPage}
disabled={page >= totalPages || loading}
title="Last Page"
className="h-8 w-8 p-0"
>
<ChevronsRightIcon className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)
}
Loading