185 lines
6.9 KiB
TypeScript
185 lines
6.9 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { useState } from 'react'
|
||
|
|
import { motion, AnimatePresence } from 'motion/react'
|
||
|
|
import { UseFormReturn, useFieldArray } from 'react-hook-form'
|
||
|
|
import { Plus, Trash2, Users } from 'lucide-react'
|
||
|
|
import { WizardStepContent } from '@/components/forms/form-wizard'
|
||
|
|
import { Input } from '@/components/ui/input'
|
||
|
|
import { Label } from '@/components/ui/label'
|
||
|
|
import { Button } from '@/components/ui/button'
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from '@/components/ui/select'
|
||
|
|
import type { ApplicationFormData } from '@/server/routers/application'
|
||
|
|
import { TeamMemberRole } from '@prisma/client'
|
||
|
|
|
||
|
|
const roleOptions: { value: TeamMemberRole; label: string }[] = [
|
||
|
|
{ value: 'MEMBER', label: 'Team Member' },
|
||
|
|
{ value: 'ADVISOR', label: 'Advisor' },
|
||
|
|
]
|
||
|
|
|
||
|
|
interface StepTeamProps {
|
||
|
|
form: UseFormReturn<ApplicationFormData>
|
||
|
|
}
|
||
|
|
|
||
|
|
export function StepTeam({ form }: StepTeamProps) {
|
||
|
|
const { control, register, formState: { errors } } = form
|
||
|
|
const { fields, append, remove } = useFieldArray({
|
||
|
|
control,
|
||
|
|
name: 'teamMembers',
|
||
|
|
})
|
||
|
|
|
||
|
|
const addMember = () => {
|
||
|
|
append({ name: '', email: '', role: 'MEMBER', title: '' })
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<WizardStepContent
|
||
|
|
title="Your team members"
|
||
|
|
description="Add the other members of your team. They will receive an invitation to create their account."
|
||
|
|
>
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
className="mx-auto max-w-lg space-y-6"
|
||
|
|
>
|
||
|
|
{fields.length === 0 ? (
|
||
|
|
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 py-12 text-center">
|
||
|
|
<Users className="mb-4 h-12 w-12 text-muted-foreground/50" />
|
||
|
|
<p className="text-muted-foreground">
|
||
|
|
No team members added yet.
|
||
|
|
</p>
|
||
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
||
|
|
You can add team members here, or skip this step if you're applying solo.
|
||
|
|
</p>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
onClick={addMember}
|
||
|
|
className="mt-4"
|
||
|
|
>
|
||
|
|
<Plus className="mr-2 h-4 w-4" />
|
||
|
|
Add Team Member
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-4">
|
||
|
|
<AnimatePresence mode="popLayout">
|
||
|
|
{fields.map((field, index) => (
|
||
|
|
<motion.div
|
||
|
|
key={field.id}
|
||
|
|
initial={{ opacity: 0, height: 0 }}
|
||
|
|
animate={{ opacity: 1, height: 'auto' }}
|
||
|
|
exit={{ opacity: 0, height: 0 }}
|
||
|
|
className="rounded-lg border bg-card p-4"
|
||
|
|
>
|
||
|
|
<div className="flex items-start justify-between mb-4">
|
||
|
|
<h4 className="font-medium text-sm text-muted-foreground">
|
||
|
|
Team Member {index + 1}
|
||
|
|
</h4>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="ghost"
|
||
|
|
size="icon"
|
||
|
|
onClick={() => remove(index)}
|
||
|
|
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
||
|
|
>
|
||
|
|
<Trash2 className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid gap-4 md:grid-cols-2">
|
||
|
|
{/* Name */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor={`teamMembers.${index}.name`}>
|
||
|
|
Full Name <span className="text-destructive">*</span>
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id={`teamMembers.${index}.name`}
|
||
|
|
placeholder="Jane Doe"
|
||
|
|
{...register(`teamMembers.${index}.name`)}
|
||
|
|
/>
|
||
|
|
{errors.teamMembers?.[index]?.name && (
|
||
|
|
<p className="text-sm text-destructive">
|
||
|
|
{errors.teamMembers[index]?.name?.message}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Email */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor={`teamMembers.${index}.email`}>
|
||
|
|
Email <span className="text-destructive">*</span>
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id={`teamMembers.${index}.email`}
|
||
|
|
type="email"
|
||
|
|
placeholder="jane@example.com"
|
||
|
|
{...register(`teamMembers.${index}.email`)}
|
||
|
|
/>
|
||
|
|
{errors.teamMembers?.[index]?.email && (
|
||
|
|
<p className="text-sm text-destructive">
|
||
|
|
{errors.teamMembers[index]?.email?.message}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Role */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>Role</Label>
|
||
|
|
<Select
|
||
|
|
value={form.watch(`teamMembers.${index}.role`)}
|
||
|
|
onValueChange={(value) =>
|
||
|
|
form.setValue(`teamMembers.${index}.role`, value as TeamMemberRole)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue placeholder="Select role" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{roleOptions.map((option) => (
|
||
|
|
<SelectItem key={option.value} value={option.value}>
|
||
|
|
{option.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Title/Position */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor={`teamMembers.${index}.title`}>
|
||
|
|
Title/Position <span className="text-muted-foreground text-xs">(optional)</span>
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id={`teamMembers.${index}.title`}
|
||
|
|
placeholder="CTO, Designer, etc."
|
||
|
|
{...register(`teamMembers.${index}.title`)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
))}
|
||
|
|
</AnimatePresence>
|
||
|
|
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
onClick={addMember}
|
||
|
|
className="w-full"
|
||
|
|
>
|
||
|
|
<Plus className="mr-2 h-4 w-4" />
|
||
|
|
Add Another Team Member
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</motion.div>
|
||
|
|
</WizardStepContent>
|
||
|
|
)
|
||
|
|
}
|