From 4fdbab1bcc47791e468b560d0efc0d7c5e6460ec Mon Sep 17 00:00:00 2001 From: Ugochukwu Nebolisa Date: Wed, 10 Sep 2025 23:15:37 +0100 Subject: [PATCH] feat: extended the proposal POST route to accept file upload of .pdf, .doc or .docx --- DevoteApp/app/api/proposals/route.ts | 97 ++++++++--- DevoteApp/app/voting/[id]/page.tsx | 28 +++ DevoteApp/components/CreateProposalModal.tsx | 174 +++++++++++++------ DevoteApp/interfaces/Proposal.tsx | 1 + 4 files changed, 222 insertions(+), 78 deletions(-) diff --git a/DevoteApp/app/api/proposals/route.ts b/DevoteApp/app/api/proposals/route.ts index c168a16..33c4f4c 100644 --- a/DevoteApp/app/api/proposals/route.ts +++ b/DevoteApp/app/api/proposals/route.ts @@ -1,35 +1,85 @@ import { NextResponse } from "next/server"; import connectToDb from "../../../lib/mongodb/mongodb"; import Proposal from "../../../models/proposal"; +import fs from "fs"; +import path from "path"; export async function POST(req: Request) { - try { - const { title, description, file } = await req.json(); - - if (!title || !description) { + try { + const formData = await req.formData(); + const title = formData.get("title") as string; + const description = formData.get("description") as string; + const file = formData.get("file") as File | null; + + if (!title || !description) { + return NextResponse.json( + { message: "Title and description are required" }, + { status: 400 } + ); + } + + await connectToDb(); + + const newProposal = new Proposal({ title, description }); + await newProposal.save(); + + // Handle file upload if a file is provided + let filePath = null; + if (file) { + // Check MIME type + const validTypes = [ + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ]; + + if (!validTypes.includes(file.type)) { return NextResponse.json( - { message: "Title and description are required" }, + { message: "Invalid file type" }, { status: 400 } ); } - - await connectToDb(); - - const newProposal = new Proposal({ title, description, file }); + + // Check extension + const validExtensions = [".pdf", ".doc", ".docx"]; + const fileExtension = path.extname(file.name).toLowerCase(); + if (!validExtensions.includes(fileExtension)) { + return NextResponse.json( + { message: "Invalid file extension" }, + { status: 400 } + ); + } + + // Upload file + const uploadDir = path.join(process.cwd(), "public/uploads/proposals"); + const proposalDir = path.join(uploadDir, newProposal.id.toString()); + + if (!fs.existsSync(proposalDir)) { + fs.mkdirSync(proposalDir, { recursive: true }); + } + + filePath = path.join(proposalDir, file.name); + const arrayBuffer = await file.arrayBuffer(); + fs.writeFileSync(filePath, Buffer.from(arrayBuffer)); + + // Update proposal with file path (in production, we should use a proper storage service like cloudinary or AWS S3) + + newProposal.file = `/uploads/proposals/${newProposal.id}/${file.name}`; await newProposal.save(); - - return NextResponse.json( - { message: "Proposal created successfully", proposal: newProposal }, - { status: 201 } - ); - } catch (error) { - console.error(error); - return NextResponse.json( - { message: "Internal server error" }, - { status: 500 } - ); } + + return NextResponse.json( + { message: "Proposal created successfully", proposal: newProposal }, + { status: 201 } + ); + } catch (error) { + console.error(error); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 } + ); } +} export async function PUT(req: Request) { try { @@ -43,8 +93,11 @@ export async function PUT(req: Request) { await connectToDb(); - const updateFields: { title?: string; description?: string; file?: string } = - {}; + const updateFields: { + title?: string; + description?: string; + file?: string; + } = {}; if (title) updateFields.title = title; if (description) updateFields.description = description; if (file) updateFields.file = file; diff --git a/DevoteApp/app/voting/[id]/page.tsx b/DevoteApp/app/voting/[id]/page.tsx index 842e10b..e66a502 100644 --- a/DevoteApp/app/voting/[id]/page.tsx +++ b/DevoteApp/app/voting/[id]/page.tsx @@ -95,6 +95,34 @@ export default function VotingStationPage() {
+ {proposal?.file && ( + + + + Proposal Context + + + Download or view the supporting document for this proposal + + + + + View Proposal Context ( + {proposal.file.endsWith(".pdf") + ? ".pdf" + : proposal.file.endsWith(".docx") + ? ".docx" + : ".doc"} + ) + + + + )} diff --git a/DevoteApp/components/CreateProposalModal.tsx b/DevoteApp/components/CreateProposalModal.tsx index 0f59450..e9fc9f7 100644 --- a/DevoteApp/components/CreateProposalModal.tsx +++ b/DevoteApp/components/CreateProposalModal.tsx @@ -1,71 +1,121 @@ -"use client" +"use client"; -import type React from "react" +import type React from "react"; -import { useState } from "react" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { Label } from "@/components/ui/label" -import { useToast } from "@/hooks/use-toast" +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/hooks/use-toast"; interface CreateProposalModalProps { - isOpen: boolean - onClose: () => void + isOpen: boolean; + onClose: () => void; } -export default function CreateProposalModal({ isOpen, onClose }: CreateProposalModalProps) { - const [proposalId, setProposalId] = useState("") - const [proposalTitle, setProposalTitle] = useState("") - const [proposalDescription, setProposalDescription] = useState("") - const [votingOptions, setVotingOptions] = useState([""]) - const [pdfDocument, setPdfDocument] = useState(null) +export default function CreateProposalModal({ + isOpen, + onClose, +}: CreateProposalModalProps) { + const [proposalId, setProposalId] = useState(""); + const [proposalTitle, setProposalTitle] = useState(""); + const [proposalDescription, setProposalDescription] = useState(""); + const [votingOptions, setVotingOptions] = useState([""]); + const [document, setDocument] = useState(null); + const [isLoading, setIsLoading] = useState(false); - const { toast } = useToast() + const { toast } = useToast(); const handleAddOption = () => { - setVotingOptions([...votingOptions, ""]) - } + setVotingOptions([...votingOptions, ""]); + }; const handleRemoveOption = (index: number) => { - const newOptions = votingOptions.filter((_, i) => i !== index) - setVotingOptions(newOptions) - } + const newOptions = votingOptions.filter((_, i) => i !== index); + setVotingOptions(newOptions); + }; const handleOptionChange = (index: number, value: string) => { - const newOptions = [...votingOptions] - newOptions[index] = value - setVotingOptions(newOptions) - } + const newOptions = [...votingOptions]; + newOptions[index] = value; + setVotingOptions(newOptions); + }; - const handlePdfUpload = (e: React.ChangeEvent) => { + const handleDocUpload = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { - setPdfDocument(e.target.files[0]) + setDocument(e.target.files[0]); } - } + }; - const handleCreateProposal = () => { - console.log("Creating proposal:", { proposalId, proposalTitle, proposalDescription, votingOptions, pdfDocument }) - setProposalId("") - setProposalTitle("") - setProposalDescription("") - setVotingOptions([""]) - setPdfDocument(null) - toast({ - title: "Success!", - description: "Your proposal has been created successfully.", - variant: "success", - }) - onClose() - } + const handleCreateProposal = async () => { + console.log("Creating proposal:", { + proposalId, + proposalTitle, + proposalDescription, + votingOptions, + document, + }); + const formData = new FormData(); + // formData.append("proposalId", proposalId); + formData.append("title", proposalTitle); + formData.append("description", proposalDescription); + // votingOptions.forEach((option, index) => + // formData.append(`votingOption${index + 1}`, option) + // ); + if (document) { + formData.append("file", document); + } + + try { + setIsLoading(true); + const response = await fetch("/api/proposals", { + method: "POST", + body: formData, + }); + if (!response.ok) { + throw new Error("Failed to create proposal"); + } + const data = await response.json(); + console.log("Proposal created:", data); + + // If successful, reset form and close modal + toast({ + title: "Success!", + description: "Your proposal has been created successfully.", + variant: "success", + }); + setProposalId(""); + setProposalTitle(""); + setProposalDescription(""); + setVotingOptions([""]); + setDocument(null); + onClose(); + } catch (error) { + toast({ + title: "Error", + description: (error as Error).message, + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; return ( - Create New Proposal + + Create New Proposal +
@@ -88,7 +138,9 @@ export default function CreateProposalModal({ isOpen, onClose }: CreateProposalM
-

Explain the context of your project to the people voting!

+

+ Explain the context of your project to the people voting! +