diff --git a/app/(dashboard)/projects/[id]/page.tsx b/app/(dashboard)/projects/[id]/page.tsx
index beb7ab9..0555673 100644
--- a/app/(dashboard)/projects/[id]/page.tsx
+++ b/app/(dashboard)/projects/[id]/page.tsx
@@ -1,11 +1,19 @@
'use client';
-import { use } from 'react';
+import { use, useState, useEffect } from 'react';
import Link from 'next/link';
-import { ArrowLeft, Loader2, KanbanSquare } from 'lucide-react';
+import { useRouter } from 'next/navigation';
+import { ArrowLeft, Loader2, KanbanSquare, Pencil, Archive } from 'lucide-react';
+import { useQueryClient } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useProject } from '@/lib/hooks/use-projects';
+import { useProjectHub } from '@/lib/hooks/useProjectHub';
+import { EditProjectDialog } from '@/components/features/projects/EditProjectDialog';
+import { ArchiveProjectDialog } from '@/components/features/projects/ArchiveProjectDialog';
+import type { Project } from '@/types/project';
+import { toast } from 'sonner';
interface ProjectDetailPageProps {
params: Promise<{ id: string }>;
@@ -13,7 +21,29 @@ interface ProjectDetailPageProps {
export default function ProjectDetailPage({ params }: ProjectDetailPageProps) {
const { id } = use(params);
+ const router = useRouter();
+ const queryClient = useQueryClient();
const { data: project, isLoading, error } = useProject(id);
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+ const [isArchiveDialogOpen, setIsArchiveDialogOpen] = useState(false);
+
+ // SignalR real-time updates
+ const { connectionState } = useProjectHub(id, {
+ onProjectUpdated: (updatedProject) => {
+ if (updatedProject.id === id) {
+ console.log('[ProjectDetail] Project updated via SignalR:', updatedProject);
+ queryClient.setQueryData(['projects', id], updatedProject);
+ toast.info('Project updated');
+ }
+ },
+ onProjectArchived: (data) => {
+ if (data.ProjectId === id) {
+ console.log('[ProjectDetail] Project archived via SignalR:', data);
+ toast.info('Project has been archived');
+ router.push('/projects');
+ }
+ },
+ });
if (isLoading) {
return (
@@ -44,48 +74,97 @@ export default function ProjectDetailPage({ params }: ProjectDetailPageProps) {
{project.name}
-
+
{project.status}
-
+
-
Key: {project.key}
+
Key: {project.key}
+
+
+
+
+
+ {project.status === 'Active' && (
+ <>
+
+
+ >
+ )}
-
-
-
-
-
- Project Details
- Information about this project
-
-
-
-
Description
-
{project.description || 'No description provided'}
-
-
-
Created
-
{new Date(project.createdAt).toLocaleDateString()}
-
- {project.updatedAt && (
-
-
Last Updated
-
{new Date(project.updatedAt).toLocaleDateString()}
+
+
+
+ Description
+
+
+ {project.description || 'No description provided'}
+
+
+
+
+
+ Details
+
+
+
+ Created
+ {new Date(project.createdAt).toLocaleDateString()}
- )}
-
-
+ {project.updatedAt && (
+
+ Updated
+ {new Date(project.updatedAt).toLocaleDateString()}
+
+ )}
+
+ Status
+
+ {project.status}
+
+
+
+
+
+
+ {/* SignalR Connection Status */}
+
+
+
+ {connectionState === 'connected' ? 'Real-time updates enabled' : 'Connecting...'}
+
+
+
+ {/* Dialogs */}
+ {project && (
+ <>
+
+
+ >
+ )}
);
}
diff --git a/components/features/projects/ArchiveProjectDialog.tsx b/components/features/projects/ArchiveProjectDialog.tsx
new file mode 100644
index 0000000..fd0bf23
--- /dev/null
+++ b/components/features/projects/ArchiveProjectDialog.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useDeleteProject } from '@/lib/hooks/use-projects';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { toast } from 'sonner';
+
+interface ArchiveProjectDialogProps {
+ projectId: string;
+ projectName: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function ArchiveProjectDialog({
+ projectId,
+ projectName,
+ open,
+ onOpenChange,
+}: ArchiveProjectDialogProps) {
+ const router = useRouter();
+ const deleteProject = useDeleteProject();
+
+ const handleArchive = async () => {
+ try {
+ await deleteProject.mutateAsync(projectId);
+ toast.success('Project archived successfully');
+ router.push('/projects');
+ } catch (error) {
+ toast.error('Failed to archive project');
+ console.error('Archive error:', error);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/components/features/projects/EditProjectDialog.tsx b/components/features/projects/EditProjectDialog.tsx
new file mode 100644
index 0000000..c764c61
--- /dev/null
+++ b/components/features/projects/EditProjectDialog.tsx
@@ -0,0 +1,139 @@
+'use client';
+
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import * as z from 'zod';
+import { useUpdateProject } from '@/lib/hooks/use-projects';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import type { Project } from '@/types/project';
+import { toast } from 'sonner';
+
+const updateProjectSchema = z.object({
+ name: z
+ .string()
+ .min(1, 'Project name is required')
+ .max(200, 'Project name cannot exceed 200 characters'),
+ description: z
+ .string()
+ .max(2000, 'Description cannot exceed 2000 characters'),
+});
+
+type UpdateProjectFormData = z.infer;
+
+interface EditProjectDialogProps {
+ project: Project;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function EditProjectDialog({
+ project,
+ open,
+ onOpenChange,
+}: EditProjectDialogProps) {
+ const updateProject = useUpdateProject(project.id);
+
+ const form = useForm({
+ resolver: zodResolver(updateProjectSchema),
+ defaultValues: {
+ name: project.name,
+ description: project.description,
+ },
+ });
+
+ const onSubmit = async (data: UpdateProjectFormData) => {
+ try {
+ await updateProject.mutateAsync(data);
+ toast.success('Project updated successfully');
+ onOpenChange(false);
+ } catch (error) {
+ toast.error('Failed to update project');
+ console.error('Update error:', error);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/lib/hooks/useProjectHub.ts b/lib/hooks/useProjectHub.ts
index 923ffec..0418e66 100644
--- a/lib/hooks/useProjectHub.ts
+++ b/lib/hooks/useProjectHub.ts
@@ -4,8 +4,21 @@ 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';
-export function useProjectHub(projectId?: string) {
+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'
@@ -25,42 +38,49 @@ export function useProjectHub(projectId?: string) {
// 监听项目事件
manager.on('ProjectUpdated', (data: any) => {
console.log('[ProjectHub] Project updated:', data);
- // TODO: 触发项目数据重新加载
+ 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);
- // TODO: 添加到看板
+ options?.onIssueCreated?.(issue);
});
manager.on('IssueUpdated', (issue: any) => {
console.log('[ProjectHub] Issue updated:', issue);
- // TODO: 更新看板
+ options?.onIssueUpdated?.(issue);
});
manager.on('IssueDeleted', (data: { IssueId: string }) => {
console.log('[ProjectHub] Issue deleted:', data);
- // TODO: 从看板移除
+ options?.onIssueDeleted?.(data);
});
manager.on('IssueStatusChanged', (data: any) => {
console.log('[ProjectHub] Issue status changed:', data);
- // TODO: 移动看板卡片
+ 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);
- // TODO: 显示正在输入提示
+ options?.onTypingIndicator?.(data);
}
);
@@ -70,7 +90,7 @@ export function useProjectHub(projectId?: string) {
unsubscribe();
manager.stop();
};
- }, [isAuthenticated]);
+ }, [isAuthenticated, options]);
// 加入项目房间
const joinProject = useCallback(async (projectId: string) => {