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>
157 lines
4.4 KiB
TypeScript
157 lines
4.4 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { foldersApi } from '@/lib/api';
|
|
|
|
// Define folder interface (database-compatible)
|
|
export interface Folder {
|
|
_id: string;
|
|
id?: string; // Legacy compatibility
|
|
name: string;
|
|
created_at?: string;
|
|
created_by?: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
// Default folder ID for "All Personas"
|
|
export const DEFAULT_FOLDER_ID = 'all';
|
|
|
|
interface UseFolderManagementReturn {
|
|
folders: Folder[];
|
|
selectedFolder: string;
|
|
setSelectedFolder: (id: string) => void;
|
|
isCreatingFolder: boolean;
|
|
setIsCreatingFolder: (creating: boolean) => void;
|
|
newFolderName: string;
|
|
setNewFolderName: (name: string) => void;
|
|
folderToRename: Folder | null;
|
|
renameFolderName: string;
|
|
setRenameFolderName: (name: string) => void;
|
|
fetchFolders: () => Promise<Folder[]>;
|
|
createNewFolder: () => Promise<void>;
|
|
cancelFolderCreation: () => void;
|
|
startRenameFolder: (folder: Folder) => void;
|
|
completeRenameFolder: () => Promise<void>;
|
|
cancelRenameFolder: () => void;
|
|
}
|
|
|
|
export function useFolderManagement(): UseFolderManagementReturn {
|
|
const [folders, setFolders] = useState<Folder[]>([]);
|
|
const [selectedFolder, setSelectedFolder] = useState<string>(DEFAULT_FOLDER_ID);
|
|
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
|
const [newFolderName, setNewFolderName] = useState('');
|
|
const [folderToRename, setFolderToRename] = useState<Folder | null>(null);
|
|
const [renameFolderName, setRenameFolderName] = useState('');
|
|
|
|
// Function to fetch folders from database
|
|
const fetchFolders = useCallback(async (): Promise<Folder[]> => {
|
|
try {
|
|
const response = await foldersApi.getAll();
|
|
const serverFolders = response.data;
|
|
|
|
// Convert server folder format to match frontend expectations
|
|
const processedFolders: Folder[] = serverFolders.map((folder: any) => ({
|
|
...folder,
|
|
id: folder._id // Add legacy id field for compatibility
|
|
}));
|
|
|
|
setFolders(processedFolders);
|
|
return processedFolders;
|
|
} catch (error) {
|
|
console.error("Error fetching folders:", error);
|
|
toast.error("Failed to load folders");
|
|
setFolders([]);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
// Create new folder
|
|
const createNewFolder = useCallback(async () => {
|
|
if (!newFolderName.trim()) {
|
|
toast.error("Please enter a folder name");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await foldersApi.create({
|
|
name: newFolderName.trim()
|
|
});
|
|
|
|
// Refresh folders from server
|
|
await fetchFolders();
|
|
|
|
setNewFolderName('');
|
|
setIsCreatingFolder(false);
|
|
|
|
toast.success(`Folder "${newFolderName}" created`);
|
|
} catch (error) {
|
|
console.error("Error creating folder:", error);
|
|
toast.error("Failed to create folder");
|
|
}
|
|
}, [newFolderName, fetchFolders]);
|
|
|
|
// Cancel folder creation
|
|
const cancelFolderCreation = useCallback(() => {
|
|
setNewFolderName('');
|
|
setIsCreatingFolder(false);
|
|
}, []);
|
|
|
|
// Start renaming a folder
|
|
const startRenameFolder = useCallback((folder: Folder) => {
|
|
setFolderToRename(folder);
|
|
setRenameFolderName(folder.name);
|
|
}, []);
|
|
|
|
// Complete folder rename
|
|
const completeRenameFolder = useCallback(async () => {
|
|
if (!folderToRename || !renameFolderName.trim()) {
|
|
setFolderToRename(null);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await foldersApi.update(folderToRename._id, {
|
|
name: renameFolderName.trim()
|
|
});
|
|
|
|
// Refresh folders from server
|
|
await fetchFolders();
|
|
|
|
setFolderToRename(null);
|
|
toast.success(`Folder renamed to "${renameFolderName}"`);
|
|
} catch (error) {
|
|
console.error("Error renaming folder:", error);
|
|
toast.error("Failed to rename folder");
|
|
setFolderToRename(null);
|
|
}
|
|
}, [folderToRename, renameFolderName, fetchFolders]);
|
|
|
|
// Cancel folder rename
|
|
const cancelRenameFolder = useCallback(() => {
|
|
setFolderToRename(null);
|
|
setRenameFolderName('');
|
|
}, []);
|
|
|
|
// Fetch folders on mount
|
|
useEffect(() => {
|
|
fetchFolders();
|
|
}, [fetchFolders]);
|
|
|
|
return {
|
|
folders,
|
|
selectedFolder,
|
|
setSelectedFolder,
|
|
isCreatingFolder,
|
|
setIsCreatingFolder,
|
|
newFolderName,
|
|
setNewFolderName,
|
|
folderToRename,
|
|
renameFolderName,
|
|
setRenameFolderName,
|
|
fetchFolders,
|
|
createNewFolder,
|
|
cancelFolderCreation,
|
|
startRenameFolder,
|
|
completeRenameFolder,
|
|
cancelRenameFolder,
|
|
};
|
|
}
|