diff --git a/README.md b/README.md index cc730c7..4148aab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Firebase Studio +# FlowUp This is a NextJS starter in Firebase Studio. diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 3417dfc..44d5d58 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -52,7 +52,7 @@ export default function DashboardPage() { Welcome back, {user.name}! - Here's what's happening in your NationQuest Hub workspace today. + Here's what's happening in your FlowUp workspace today. diff --git a/src/app/(app)/documentation/page.tsx b/src/app/(app)/documentation/page.tsx index 6e863ae..c548843 100644 --- a/src/app/(app)/documentation/page.tsx +++ b/src/app/(app)/documentation/page.tsx @@ -6,10 +6,10 @@ import Link from "next/link"; // Mock data for documents const mockDocuments = [ - { id: "doc-1", title: "Getting Started Guide", project: "NationQuest Hub", lastUpdated: "2023-10-20", category: "General" }, + { id: "doc-1", title: "Getting Started Guide", project: "FlowUp", lastUpdated: "2023-10-20", category: "General" }, { id: "doc-2", title: "Project Management Best Practices", project: "General", lastUpdated: "2023-10-15", category: "Guides" }, { id: "doc-3", title: "API Documentation", project: "Project Alpha", lastUpdated: "2023-10-18", category: "Technical" }, - { id: "doc-4", title: "User Roles and Permissions", project: "NationQuest Hub", lastUpdated: "2023-10-12", category: "Security" }, + { id: "doc-4", title: "User Roles and Permissions", project: "FlowUp", lastUpdated: "2023-10-12", category: "Security" }, ]; export default function DocumentationPage() { diff --git a/src/app/(app)/projects/[id]/documents/[docId]/edit/page.tsx b/src/app/(app)/projects/[id]/documents/[docId]/edit/page.tsx index aaebdce..b4c4ce8 100644 --- a/src/app/(app)/projects/[id]/documents/[docId]/edit/page.tsx +++ b/src/app/(app)/projects/[id]/documents/[docId]/edit/page.tsx @@ -5,7 +5,7 @@ import { DocumentEditor } from '@/components/project/DocumentEditor'; import { useAuth } from '@/hooks/useAuth'; import { useRouter, useParams } from 'next/navigation'; import { useToast } from '@/hooks/use-toast'; -import { fetchDocumentAction, updateDocumentAction, fetchProjectMemberRoleAction, fetchProjectAction } from '../../../actions'; +import { fetchDocumentAction, fetchProjectMemberRoleAction, fetchProjectAction } from '../../../actions'; import { useEffect, useState } from 'react'; import type { ProjectDocumentType, Project } from '@/types'; import { Loader2, ArrowLeft, ShieldAlert, FileText } from 'lucide-react'; @@ -92,7 +92,7 @@ export default function EditDocumentPage() { ); } - + if (!canEdit && !isLoadingDocument) { return (
@@ -131,7 +131,7 @@ export default function EditDocumentPage() { router.push(`/projects/${projectUuid}?tab=documents`)} onCancel={handleCancel} /> diff --git a/src/app/(app)/projects/[id]/documents/new/page.tsx b/src/app/(app)/projects/[id]/documents/new/page.tsx index 56a8dbf..0c6e5ff 100644 --- a/src/app/(app)/projects/[id]/documents/new/page.tsx +++ b/src/app/(app)/projects/[id]/documents/new/page.tsx @@ -5,7 +5,7 @@ import { DocumentEditor } from '@/components/project/DocumentEditor'; import { useAuth } from '@/hooks/useAuth'; import { useRouter, useParams } from 'next/navigation'; import { useToast } from '@/hooks/use-toast'; -import { createDocumentAction, fetchProjectAction, fetchProjectMemberRoleAction } from '../../actions'; +import { fetchProjectAction, fetchProjectMemberRoleAction } from '../../actions'; import { useEffect, useState } from 'react'; import type { Project } from '@/types'; import { Loader2, ArrowLeft, ShieldAlert } from 'lucide-react'; @@ -35,7 +35,7 @@ export default function NewDocumentPage() { router.push(`/projects/${projectUuid}`); return; } - + const roleResult = await fetchProjectMemberRoleAction(projectUuid, user.uuid); if (roleResult.role && ['owner', 'co-owner', 'editor'].includes(roleResult.role)) { diff --git a/src/app/(app)/projects/[id]/layout.tsx b/src/app/(app)/projects/[id]/layout.tsx new file mode 100644 index 0000000..4b08230 --- /dev/null +++ b/src/app/(app)/projects/[id]/layout.tsx @@ -0,0 +1,231 @@ + +'use client'; + +import { useParams, useRouter } from 'next/navigation'; +import React, { useEffect, useState, useCallback, startTransition as ReactStartTransition, useActionState, cloneElement } from 'react'; +import type { Project, ProjectMemberRole, User } from '@/types'; +import { useAuth } from '@/hooks/useAuth'; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +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, DialogTrigger } 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'; + +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 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 && user.role !== 'admin') { + 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); + } + loadProjectData(); // Re-fetch to ensure UI consistency + } + if (updateProjectFormState.error) { + toast({ variant: "destructive", title: "Error", description: updateProjectFormState.error }); + } + } + }, [updateProjectFormState, isUpdateProjectPending, toast, loadProjectData]); + + 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 canManageProjectSettings = currentUserRole === 'owner' || currentUserRole === 'co-owner'; + + if (authLoading || isLoadingData) { + return ( +
+ + + +
+
+ {/* For TabsList */} + {/* For Tab Content */} +
+ ); + } + + 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