semblance-dev/src/components/focus-group-session/ParticipantsTab.tsx
michael 22b3ec19a5 Refactor FocusGroupModerator into smaller components and hooks
Extract business logic and UI into reusable pieces:

Custom Hooks:
- useFocusGroupAutoSave: debounced auto-save with retry logic
- useFolderManagement: folder CRUD operations
- usePersonaFiltering: filter state and persona filtering
- useDiscussionGuideGeneration: guide generation and progress

UI Components:
- SaveStatusIndicator: auto-save status display
- FolderSidebar: folder list and management
- PersonaFilterDialog: persona filter modal
- CopyGuideDialog: copy guide from other focus groups

Tab Components:
- SetupTab: form and asset uploader
- ReviewTab: discussion guide viewer
- ParticipantsTab: persona selection grid

Reduces FocusGroupModerator from 2,396 to ~600 lines (75% reduction).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 09:11:21 -06:00

159 lines
5.5 KiB
TypeScript

import React from 'react';
import {
Search,
Filter,
Users,
Loader2,
Play,
} from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import UserCard from "@/components/UserCard";
import { Persona } from "@/types/persona";
import { FilterState } from '@/hooks/usePersonaFiltering';
interface ParticipantsTabProps {
personas: any[];
filteredPersonas: any[];
selectedParticipants: string[];
onParticipantToggle: (id: string) => void;
onViewDetails: (persona: Persona) => void;
searchTerm: string;
onSearchChange: (term: string) => void;
activeFilters: FilterState;
onOpenFilter: () => void;
isLoading: boolean;
// Folder sidebar passed as children or render prop
folderSidebar: React.ReactNode;
// Navigation
onNavigateToReview: () => void;
onStartFocusGroup: () => void;
canStart: boolean;
}
export function ParticipantsTab({
filteredPersonas,
selectedParticipants,
onParticipantToggle,
onViewDetails,
searchTerm,
onSearchChange,
activeFilters,
onOpenFilter,
isLoading,
folderSidebar,
onNavigateToReview,
onStartFocusGroup,
canStart,
}: ParticipantsTabProps) {
// Count active filters
const activeFilterCount = Object.values(activeFilters).reduce((count, arr) => count + arr.length, 0);
return (
<div className="flex flex-col md:flex-row gap-6">
{/* Folder sidebar */}
{folderSidebar}
{/* Main content */}
<div className="flex-1">
<Card className="mb-4">
<CardContent className="p-6">
<div className="flex flex-col space-y-6">
<div className="flex items-center justify-between">
<h3 className="font-sf text-lg font-medium">Select Participants</h3>
<div className="flex items-center">
<Users className="h-5 w-5 mr-2 text-muted-foreground" />
<span className="text-sm font-medium">
{selectedParticipants.length} of {filteredPersonas.length} selected
</span>
</div>
</div>
{/* Search and filter bar */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search personas by name, occupation, or location..."
className="pl-10 bg-white"
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
/>
</div>
<Button
variant="outline"
className="flex items-center gap-2"
onClick={onOpenFilter}
>
<Filter className="h-4 w-4" />
<span>Filter{activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}</span>
</Button>
</div>
{isLoading ? (
<div className="flex justify-center items-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : filteredPersonas.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-4">
{filteredPersonas.map((persona) => {
const personaId = persona._id || persona.id;
return (
<UserCard
key={personaId}
user={{
id: personaId,
_id: persona._id,
name: persona.name,
age: persona.age,
gender: persona.gender,
occupation: persona.occupation,
location: persona.location || 'Unknown',
techSavviness: persona.techSavviness || 50,
personality: persona.personality || 'No description available',
oceanTraits: persona.oceanTraits,
qualitativeAttributes: persona.qualitativeAttributes,
topPersonalityTraits: persona.topPersonalityTraits,
aiSynthesizedBio: persona.aiSynthesizedBio,
}}
selected={selectedParticipants.includes(personaId)}
onSelectionToggle={() => onParticipantToggle(personaId)}
onViewDetails={onViewDetails}
/>
);
})}
</div>
) : (
<div className="text-center py-12">
<p className="text-muted-foreground">No personas available matching your criteria.</p>
</div>
)}
</div>
</CardContent>
</Card>
<div className="flex justify-between">
<Button
variant="outline"
onClick={onNavigateToReview}
>
Back to Review
</Button>
<Button
onClick={onStartFocusGroup}
disabled={!canStart}
>
<Play className="mr-2 h-4 w-4" />
Start Focus Group Session
</Button>
</div>
</div>
</div>
);
}
export default ParticipantsTab;