Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
168
src/components/admin/jury/add-member-dialog.tsx
Normal file
168
src/components/admin/jury/add-member-dialog.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
||||
interface AddMemberDialogProps {
|
||||
juryGroupId: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>('')
|
||||
const [role, setRole] = useState<'CHAIR' | 'MEMBER' | 'OBSERVER'>('MEMBER')
|
||||
const [maxAssignments, setMaxAssignments] = useState<string>('')
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
const { data: userResponse, isLoading: isSearching } = trpc.user.list.useQuery(
|
||||
{ search: searchQuery, perPage: 20 },
|
||||
{ enabled: searchQuery.length > 0 }
|
||||
)
|
||||
|
||||
const users = userResponse?.users || []
|
||||
|
||||
const { mutate: addMember, isPending } = trpc.juryGroup.addMember.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.juryGroup.getById.invalidate({ id: juryGroupId })
|
||||
toast.success('Member added successfully')
|
||||
onOpenChange(false)
|
||||
resetForm()
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err.message)
|
||||
},
|
||||
})
|
||||
|
||||
const resetForm = () => {
|
||||
setSearchQuery('')
|
||||
setSelectedUserId('')
|
||||
setRole('MEMBER')
|
||||
setMaxAssignments('')
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!selectedUserId) {
|
||||
toast.error('Please select a user')
|
||||
return
|
||||
}
|
||||
|
||||
addMember({
|
||||
juryGroupId,
|
||||
userId: selectedUserId,
|
||||
role,
|
||||
maxAssignmentsOverride: maxAssignments ? parseInt(maxAssignments, 10) : null,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Member to Jury Group</DialogTitle>
|
||||
<DialogDescription>
|
||||
Search for a user and assign them to this jury group
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="search">Search User</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search"
|
||||
placeholder="Search by name or email..."
|
||||
className="pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{isSearching && (
|
||||
<p className="text-sm text-muted-foreground">Searching...</p>
|
||||
)}
|
||||
{users && users.length > 0 && (
|
||||
<div className="max-h-40 overflow-y-auto border rounded-md">
|
||||
{users.map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
type="button"
|
||||
className={`w-full px-3 py-2 text-left text-sm hover:bg-accent ${
|
||||
selectedUserId === user.id ? 'bg-accent' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedUserId(user.id)
|
||||
setSearchQuery(user.email)
|
||||
}}
|
||||
>
|
||||
<div className="font-medium">{user.name || 'Unnamed User'}</div>
|
||||
<div className="text-muted-foreground text-xs">{user.email}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select value={role} onValueChange={(val) => setRole(val as any)}>
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MEMBER">Member</SelectItem>
|
||||
<SelectItem value="CHAIR">Chair</SelectItem>
|
||||
<SelectItem value="OBSERVER">Observer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxAssignments">Max Assignments Override (optional)</Label>
|
||||
<Input
|
||||
id="maxAssignments"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Leave empty to use group default"
|
||||
value={maxAssignments}
|
||||
onChange={(e) => setMaxAssignments(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending || !selectedUserId}>
|
||||
{isPending ? 'Adding...' : 'Add Member'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user