feat: UX
This commit is contained in:
122
frontend/src/components/ParticipantSelector.tsx
Normal file
122
frontend/src/components/ParticipantSelector.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useState } from 'react';
|
||||
import { Participant } from '@/types/calendar';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { X, Plus, Search, User } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ParticipantSelectorProps {
|
||||
participants: Participant[];
|
||||
selectedParticipants: Participant[];
|
||||
onSelectionChange: (participants: Participant[]) => void;
|
||||
}
|
||||
|
||||
export const ParticipantSelector = ({
|
||||
participants,
|
||||
selectedParticipants,
|
||||
onSelectionChange,
|
||||
}: ParticipantSelectorProps) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const filteredParticipants = participants.filter(
|
||||
(p) =>
|
||||
!selectedParticipants.find((sp) => sp.id === p.id) &&
|
||||
(p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.email.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
const addParticipant = (participant: Participant) => {
|
||||
onSelectionChange([...selectedParticipants, participant]);
|
||||
setSearchQuery('');
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const removeParticipant = (participantId: string) => {
|
||||
onSelectionChange(selectedParticipants.filter((p) => p.id !== participantId));
|
||||
};
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search people..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setIsDropdownOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsDropdownOpen(true)}
|
||||
className="pl-10 h-12 bg-background border-border"
|
||||
/>
|
||||
|
||||
{isDropdownOpen && filteredParticipants.length > 0 && (
|
||||
<div className="absolute z-10 w-full mt-2 bg-popover border border-border rounded-lg shadow-popover animate-scale-in overflow-hidden">
|
||||
{filteredParticipants.map((participant) => (
|
||||
<button
|
||||
key={participant.id}
|
||||
onClick={() => addParticipant(participant)}
|
||||
className="w-full px-4 py-3 flex items-center gap-3 hover:bg-accent transition-colors text-left"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-medium text-primary">
|
||||
{getInitials(participant.name)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{participant.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{participant.email}</div>
|
||||
</div>
|
||||
{!participant.connected && (
|
||||
<span className="ml-auto text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded">
|
||||
Not connected
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedParticipants.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedParticipants.map((participant, index) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 bg-accent text-accent-foreground px-3 py-2 rounded-full text-sm animate-scale-in",
|
||||
"border border-primary/20"
|
||||
)}
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs font-medium">
|
||||
{getInitials(participant.name)}
|
||||
</div>
|
||||
<span className="font-medium">{participant.name.split(' ')[0]}</span>
|
||||
<button
|
||||
onClick={() => removeParticipant(participant.id)}
|
||||
className="w-5 h-5 rounded-full hover:bg-primary/20 flex items-center justify-center transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setIsDropdownOpen(true)}
|
||||
className="flex items-center gap-1 px-3 py-2 rounded-full text-sm border-2 border-dashed border-border text-muted-foreground hover:border-primary hover:text-primary transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Add</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user