Files
ColaFlow-Web/lib/hooks/useProjectHub.ts
Yaojia Wang 149bb9bd88 feat(frontend): Implement Project Detail Page with edit and archive functionality
Add complete project detail page with real-time updates via SignalR.

Changes:
- Updated project detail page with edit and archive buttons
- Created EditProjectDialog component for updating projects
- Created ArchiveProjectDialog component for archiving projects
- Integrated SignalR real-time updates (onProjectUpdated, onProjectArchived)
- Added SignalR connection status indicator
- Enhanced useProjectHub hook to support callback options
- Improved UI layout with two-column card grid
- Added toast notifications for user feedback

Features:
- View project details (name, description, status, timestamps)
- Edit project name and description
- Archive active projects
- Real-time updates when project is modified by other users
- Automatic redirect when project is archived

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:40:58 +01:00

157 lines
4.7 KiB
TypeScript

'use client';
import { useEffect, useState, useCallback, useRef } from 'react';
import { SignalRConnectionManager } from '@/lib/signalr/ConnectionManager';
import { SIGNALR_CONFIG } from '@/lib/signalr/config';
import { useAuthStore } from '@/stores/authStore';
import type { Project } from '@/types/project';
interface UseProjectHubOptions {
onProjectUpdated?: (project: Project) => void;
onProjectArchived?: (data: { ProjectId: string }) => void;
onIssueCreated?: (issue: any) => void;
onIssueUpdated?: (issue: any) => void;
onIssueDeleted?: (data: { IssueId: string }) => void;
onIssueStatusChanged?: (data: any) => void;
onUserJoinedProject?: (data: any) => void;
onUserLeftProject?: (data: any) => void;
onTypingIndicator?: (data: { UserId: string; IssueId: string; IsTyping: boolean }) => void;
}
export function useProjectHub(projectId?: string, options?: UseProjectHubOptions) {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const [connectionState, setConnectionState] = useState<
'disconnected' | 'connecting' | 'connected' | 'reconnecting'
>('disconnected');
const managerRef = useRef<SignalRConnectionManager | null>(null);
useEffect(() => {
if (!isAuthenticated) return;
const manager = new SignalRConnectionManager(
SIGNALR_CONFIG.HUB_URLS.PROJECT
);
managerRef.current = manager;
const unsubscribe = manager.onStateChange(setConnectionState);
// 监听项目事件
manager.on('ProjectUpdated', (data: any) => {
console.log('[ProjectHub] Project updated:', data);
options?.onProjectUpdated?.(data);
});
manager.on('ProjectArchived', (data: { ProjectId: string }) => {
console.log('[ProjectHub] Project archived:', data);
options?.onProjectArchived?.(data);
});
manager.on('IssueCreated', (issue: any) => {
console.log('[ProjectHub] Issue created:', issue);
options?.onIssueCreated?.(issue);
});
manager.on('IssueUpdated', (issue: any) => {
console.log('[ProjectHub] Issue updated:', issue);
options?.onIssueUpdated?.(issue);
});
manager.on('IssueDeleted', (data: { IssueId: string }) => {
console.log('[ProjectHub] Issue deleted:', data);
options?.onIssueDeleted?.(data);
});
manager.on('IssueStatusChanged', (data: any) => {
console.log('[ProjectHub] Issue status changed:', data);
options?.onIssueStatusChanged?.(data);
});
manager.on('UserJoinedProject', (data: any) => {
console.log('[ProjectHub] User joined:', data);
options?.onUserJoinedProject?.(data);
});
manager.on('UserLeftProject', (data: any) => {
console.log('[ProjectHub] User left:', data);
options?.onUserLeftProject?.(data);
});
manager.on(
'TypingIndicator',
(data: { UserId: string; IssueId: string; IsTyping: boolean }) => {
console.log('[ProjectHub] Typing indicator:', data);
options?.onTypingIndicator?.(data);
}
);
manager.start();
return () => {
unsubscribe();
manager.stop();
};
}, [isAuthenticated, options]);
// 加入项目房间
const joinProject = useCallback(async (projectId: string) => {
if (!managerRef.current) return;
try {
await managerRef.current.invoke('JoinProject', projectId);
console.log(`[ProjectHub] Joined project ${projectId}`);
} catch (error) {
console.error('[ProjectHub] Error joining project:', error);
}
}, []);
// 离开项目房间
const leaveProject = useCallback(async (projectId: string) => {
if (!managerRef.current) return;
try {
await managerRef.current.invoke('LeaveProject', projectId);
console.log(`[ProjectHub] Left project ${projectId}`);
} catch (error) {
console.error('[ProjectHub] Error leaving project:', error);
}
}, []);
// 发送正在输入指示器
const sendTypingIndicator = useCallback(
async (projectId: string, issueId: string, isTyping: boolean) => {
if (!managerRef.current) return;
try {
await managerRef.current.invoke(
'SendTypingIndicator',
projectId,
issueId,
isTyping
);
} catch (error) {
console.error('[ProjectHub] Error sending typing indicator:', error);
}
},
[]
);
// 当 projectId 变化时自动加入/离开
useEffect(() => {
if (connectionState === 'connected' && projectId) {
joinProject(projectId);
return () => {
leaveProject(projectId);
};
}
}, [connectionState, projectId, joinProject, leaveProject]);
return {
connectionState,
joinProject,
leaveProject,
sendTypingIndicator,
isConnected: connectionState === 'connected',
};
}