From 051546f706694b93a99e0855aa7565595a7c81db Mon Sep 17 00:00:00 2001 From: Enzo prados Date: Thu, 19 Jun 2025 00:21:50 +0000 Subject: [PATCH 01/10] et ce que si on fait pour la pluspart des truc plus comme pour les doc e --- .../projects/[id]/announcements/page.tsx | 54 + .../(app)/projects/[id]/codespace/page.tsx | 39 + .../(app)/projects/[id]/documents/page.tsx | 217 +++ src/app/(app)/projects/[id]/layout.tsx | 252 +++ src/app/(app)/projects/[id]/page.tsx | 1650 ++--------------- src/app/(app)/projects/[id]/readme/page.tsx | 104 ++ src/app/(app)/projects/[id]/settings/page.tsx | 332 ++++ src/components/layout/AppSidebar.tsx | 15 +- 8 files changed, 1185 insertions(+), 1478 deletions(-) create mode 100644 src/app/(app)/projects/[id]/announcements/page.tsx create mode 100644 src/app/(app)/projects/[id]/codespace/page.tsx create mode 100644 src/app/(app)/projects/[id]/documents/page.tsx create mode 100644 src/app/(app)/projects/[id]/layout.tsx create mode 100644 src/app/(app)/projects/[id]/readme/page.tsx create mode 100644 src/app/(app)/projects/[id]/settings/page.tsx diff --git a/src/app/(app)/projects/[id]/announcements/page.tsx b/src/app/(app)/projects/[id]/announcements/page.tsx new file mode 100644 index 0000000..a607b23 --- /dev/null +++ b/src/app/(app)/projects/[id]/announcements/page.tsx @@ -0,0 +1,54 @@ + +'use client'; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Megaphone, PlusCircle, Loader2 } from "lucide-react"; +import type { Project, ProjectMemberRole, User } from '@/types'; +// Mock data - replace with actual data fetching and state management +const projectAnnouncements: any[] = []; + +interface ProjectAnnouncementsPageProps { + project: Project; // Passed from layout + currentUserRole: ProjectMemberRole | null; // Passed from layout + projectUuid: string; // Passed from layout + user: User; // Passed from layout +} + +export default function ProjectAnnouncementsPage({ project, currentUserRole, projectUuid }: ProjectAnnouncementsPageProps) { + + const canManageAnnouncements = currentUserRole === 'owner' || currentUserRole === 'co-owner'; + + if (!project) { + return
Loading project data...
; + } + + return ( + + + Project Announcements ({projectAnnouncements.length}) + {canManageAnnouncements && ( + + )} + + + {projectAnnouncements.length > 0 ? projectAnnouncements.map((ann: any) => ( + +

{ann.title}

+

{ann.content}

+

+ By: {ann.authorUuid} on {new Date(ann.createdAt).toLocaleDateString()} +

+
+ )) : ( +
+ +

No announcements for this project yet.

+
+ )} +
+
+ ); +} diff --git a/src/app/(app)/projects/[id]/codespace/page.tsx b/src/app/(app)/projects/[id]/codespace/page.tsx new file mode 100644 index 0000000..f44d3e4 --- /dev/null +++ b/src/app/(app)/projects/[id]/codespace/page.tsx @@ -0,0 +1,39 @@ + +'use client'; + +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { FolderGit2, Code2, Loader2 } from "lucide-react"; +import type { Project, ProjectMemberRole, User } from '@/types'; + +interface ProjectCodeSpacePageProps { + project: Project; // Passed from layout + currentUserRole: ProjectMemberRole | null; // Passed from layout + projectUuid: string; // Passed from layout + user: User; // Passed from layout +} + +export default function ProjectCodeSpacePage({ project }: ProjectCodeSpacePageProps) { + + if (!project) { + return
Loading project data...
; + } + + return ( + + + CodeSpace for {project.name} + Manage your project's code snippets, scripts, and version control links. + + +
+ +

CodeSpace is Coming Soon!

+

+ This area will allow you to link to repositories, manage small scripts, + and keep track of code-related assets for your project. +

+
+
+
+ ); +} diff --git a/src/app/(app)/projects/[id]/documents/page.tsx b/src/app/(app)/projects/[id]/documents/page.tsx new file mode 100644 index 0000000..5620b16 --- /dev/null +++ b/src/app/(app)/projects/[id]/documents/page.tsx @@ -0,0 +1,217 @@ + +'use client'; + +import { useState, useEffect, useCallback, startTransition as ReactStartTransition } from 'react'; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { FileText, PlusCircle, Edit3, Trash2, Loader2, ExternalLink } from 'lucide-react'; +import Link from 'next/link'; +import type { Project, Document as ProjectDocumentType, ProjectMemberRole, User } from '@/types'; +import { useToast } from '@/hooks/use-toast'; +import { fetchDocumentsAction, deleteDocumentAction, type DeleteDocumentFormState } from '../actions'; +import { useActionState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog"; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { Badge } from '@/components/ui/badge'; + +interface ProjectDocumentsPageProps { + project: Project; // Passed from layout + currentUserRole: ProjectMemberRole | null; // Passed from layout + projectUuid: string; // Passed from layout + user: User; // Passed from layout +} + +export default function ProjectDocumentsPage({ project, currentUserRole, projectUuid }: ProjectDocumentsPageProps) { + const { toast } = useToast(); + const [projectDocuments, setProjectDocuments] = useState([]); + const [isLoadingDocuments, setIsLoadingDocuments] = useState(true); + + const [documentToView, setDocumentToView] = useState(null); + const [isViewDocumentDialogOpen, setIsViewDocumentDialogOpen] = useState(false); + const [documentToDelete, setDocumentToDelete] = useState(null); + + const [deleteDocumentState, deleteDocumentFormAction, isDeleteDocumentPending] = useActionState(deleteDocumentAction, { message: "", error: "" }); + + const loadProjectDocuments = useCallback(async () => { + if (projectUuid) { + setIsLoadingDocuments(true); + try { + const docs = await fetchDocumentsAction(projectUuid); + setProjectDocuments(docs || []); + } catch (error) { + console.error("Failed to load documents:", error); + setProjectDocuments([]); + toast({ variant: "destructive", title: "Error", description: "Could not load documents." }); + } finally { + setIsLoadingDocuments(false); + } + } + }, [projectUuid, toast]); + + useEffect(() => { + loadProjectDocuments(); + }, [loadProjectDocuments]); + + useEffect(() => { + if (!isDeleteDocumentPending && deleteDocumentState) { + if (deleteDocumentState.message && !deleteDocumentState.error) { + toast({ title: "Success", description: deleteDocumentState.message }); + setDocumentToDelete(null); + loadProjectDocuments(); + } + if (deleteDocumentState.error) { + toast({ variant: "destructive", title: "Document Deletion Error", description: deleteDocumentState.error }); + } + } + }, [deleteDocumentState, isDeleteDocumentPending, toast, loadProjectDocuments]); + + const openViewDocumentDialog = (doc: ProjectDocumentType) => { + setDocumentToView(doc); + setIsViewDocumentDialogOpen(true); + }; + + const handleDeleteDocumentConfirm = () => { + if (!documentToDelete || !project) return; + const formData = new FormData(); + formData.append('documentUuid', documentToDelete.uuid); + formData.append('projectUuid', project.uuid); + ReactStartTransition(() => { + deleteDocumentFormAction(formData); + }); + }; + + const canManageDocuments = currentUserRole === 'owner' || currentUserRole === 'co-owner' || currentUserRole === 'editor'; + + if (!project) { + return
Loading project data...
; + } + + return ( + <> + + + Documents ({projectDocuments.length}) + {canManageDocuments && ( + + )} + + + {isLoadingDocuments ? ( +
+ ) : projectDocuments.length === 0 ? ( +
+ +

No documents in this project yet.

+ {canManageDocuments && + + } +
+ ) : ( +
+ {projectDocuments.map(doc => ( + +
+
+
+ +

openViewDocumentDialog(doc)}> + {doc.title} +

+ {doc.fileType} +
+

+ Created: {new Date(doc.createdAt).toLocaleDateString()} | Updated: {new Date(doc.updatedAt).toLocaleDateString()} +

+
+
+ {canManageDocuments && (doc.fileType === 'markdown') && ( + + )} + {canManageDocuments && ( + + + + + {documentToDelete?.uuid === doc.uuid && ( + + + Delete Document: "{documentToDelete.title}"? + This action cannot be undone. + + + setDocumentToDelete(null)}>Cancel + + {isDeleteDocumentPending && } Delete + + + + )} + + )} + +
+
+
+ ))} +
+ )} +
+
+ + + + + {documentToView?.title} + Type: {documentToView?.fileType} | Last updated: {documentToView ? new Date(documentToView.updatedAt).toLocaleString() : 'N/A'} + +
+ {documentToView?.fileType === 'markdown' && ( +
+ {documentToView?.content || ''} +
+ )} + {(documentToView?.fileType === 'txt' || documentToView?.fileType === 'html') && ( +
{documentToView?.content || 'No content.'}
+ )} + {documentToView?.fileType === 'pdf' && ( +
+ +

This is a PDF document named: {documentToView?.filePath || documentToView?.title}.

+ +

(Actual file download/storage not implemented in this prototype)

+
+ )} + {documentToView?.fileType === 'other' && ( +

Cannot display this file type directly. File name: {documentToView?.filePath || documentToView?.title}

+ )} +
+ + + +
+
+ + ); +} diff --git a/src/app/(app)/projects/[id]/layout.tsx b/src/app/(app)/projects/[id]/layout.tsx new file mode 100644 index 0000000..1dcb891 --- /dev/null +++ b/src/app/(app)/projects/[id]/layout.tsx @@ -0,0 +1,252 @@ + +'use client'; + +import { useParams, usePathname, useRouter } from 'next/navigation'; +import { useEffect, useState, useCallback, startTransition as ReactStartTransition } from 'react'; +import type { Project, ProjectMemberRole } from '@/types'; +import { useAuth } from '@/hooks/useAuth'; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ArrowLeft, Edit3, Loader2, Flame, ShieldAlert, AlertCircle } from 'lucide-react'; +import Link from 'next/link'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useToast } from '@/hooks/use-toast'; +import { fetchProjectAction, fetchProjectOwnerNameAction, fetchProjectMemberRoleAction, updateProjectAction } from './actions'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { useActionState } from 'react'; + + +const editProjectFormSchema = z.object({ + name: z.string().min(3, { message: 'Project name must be at least 3 characters.' }).max(100), + description: z.string().max(5000, {message: "Description cannot exceed 5000 characters."}).optional().or(z.literal('')), +}); +type EditProjectFormValues = z.infer; + + +export default function ProjectDetailLayout({ children }: { children: React.ReactNode }) { + const params = useParams(); + const pathname = usePathname(); + const router = useRouter(); + const { toast } = useToast(); + const projectUuid = params.id as string; + + const { user, isLoading: authLoading } = useAuth(); + const [project, setProject] = useState(null); + const [projectOwnerName, setProjectOwnerName] = useState(null); + const [currentUserRole, setCurrentUserRole] = useState(null); + const [isLoadingData, setIsLoadingData] = useState(true); + const [accessDenied, setAccessDenied] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + + const editProjectForm = useForm({ + resolver: zodResolver(editProjectFormSchema), + defaultValues: { name: '', description: '' }, + }); + + const [updateProjectFormState, updateProjectFormAction, isUpdateProjectPending] = useActionState(updateProjectAction, { message: "", errors: {} }); + + const loadProjectData = useCallback(async () => { + if (projectUuid && user && !authLoading) { + setIsLoadingData(true); + setAccessDenied(false); + try { + const projectData = await fetchProjectAction(projectUuid); + if (projectData) { + const roleResult = await fetchProjectMemberRoleAction(projectUuid, user.uuid); + const userRoleForProject = roleResult.role; + setCurrentUserRole(userRoleForProject); + + if (projectData.isPrivate && !userRoleForProject) { + setAccessDenied(true); + setProject(null); + toast({variant: "destructive", title: "Access Denied", description: "This project is private and you are not a member."}); + router.push('/projects'); + return; + } + + setProject(projectData); + editProjectForm.reset({ name: projectData.name, description: projectData.description || '' }); + + if (projectData.ownerUuid) { + const ownerName = await fetchProjectOwnerNameAction(projectData.ownerUuid); + setProjectOwnerName(ownerName); + } + } else { + setAccessDenied(true); + setProject(null); + toast({variant: "destructive", title: "Project Not Found", description: "The project could not be loaded or you don't have access."}); + router.push('/projects'); + } + } catch (err) { + console.error("[ProjectLayout] Error fetching project:", err); + setProject(null); + setAccessDenied(true); + toast({variant: "destructive", title: "Error", description: "Could not load project details."}) + router.push('/projects'); + } finally { + setIsLoadingData(false); + } + } else if (!authLoading && !user) { + router.push('/login'); + } + }, [projectUuid, user, authLoading, router, toast, editProjectForm]); + + useEffect(() => { + loadProjectData(); + }, [loadProjectData]); + + useEffect(() => { + if (!isUpdateProjectPending && updateProjectFormState) { + if (updateProjectFormState.message && !updateProjectFormState.error) { + toast({ title: "Success", description: updateProjectFormState.message }); + setIsEditDialogOpen(false); + if(updateProjectFormState.project) { + setProject(updateProjectFormState.project); // Update project state in layout + } + } + if (updateProjectFormState.error) { + toast({ variant: "destructive", title: "Error", description: updateProjectFormState.error }); + } + } + }, [updateProjectFormState, isUpdateProjectPending, toast]); + + const handleEditProjectSubmit = async (values: EditProjectFormValues) => { + if (!project) return; + const formData = new FormData(); + formData.append('name', values.name); + formData.append('description', values.description || ''); + formData.append('projectUuid', project.uuid); + ReactStartTransition(() => { + updateProjectFormAction(formData); + }); + }; + + const getActiveTab = () => { + if (pathname.endsWith('/readme')) return 'readme'; + if (pathname.includes('/documents')) return 'documents'; // Includes /new and /edit + if (pathname.endsWith('/announcements')) return 'announcements'; + if (pathname.endsWith('/codespace')) return 'codespace'; + if (pathname.endsWith('/settings')) return 'settings'; + return 'tasks'; // Default tab + }; + + const canManageProjectSettings = currentUserRole === 'owner' || currentUserRole === 'co-owner'; + + if (authLoading || isLoadingData) { + return ( +
+ + + +
+
+ + +
+ ); + } + + if (accessDenied || !project || !user) { + return ( +
+ + +

Access Denied or Project Not Found

+

+ {accessDenied ? "You do not have permission to view this project." : `Project (ID: ${projectUuid}) not found.`} +

+
+ ); + } + + return ( +
+ + + + +
+
+
+ {project.name} + {project.isUrgent && } + + {project.isPrivate ? "Private" : "Public"} + +
+ {project.description ? ( +
+ {project.description} +
+ ) : ( + No description provided. + )} +
+
+ + + + + + + Edit Project + Update the name and description (Markdown supported) of your project. + +
+ + (Project Name)}/> + (Project Description