Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 75 additions & 22 deletions DevoteApp/app/api/proposals/route.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions DevoteApp/app/voting/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,34 @@ export default function VotingStationPage() {
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<div className="grid gap-6 md:grid-cols-1">
{proposal?.file && (
<Card className="bg-gray-900 border-[#f7cf1d] max-w-2xl mx-auto">
<CardHeader>
<CardTitle className="text-[#f7cf1d]">
Proposal Context
</CardTitle>
<CardDescription className="text-gray-400">
Download or view the supporting document for this proposal
</CardDescription>
</CardHeader>
<CardContent>
<a
href={proposal.file}
target="_blank"
rel="noopener noreferrer"
className="inline-block bg-[#f7cf1d] text-black font-medium px-4 py-2 rounded hover:bg-[#e5bd0e] transition"
>
View Proposal Context (
{proposal.file.endsWith(".pdf")
? ".pdf"
: proposal.file.endsWith(".docx")
? ".docx"
: ".doc"}
)
</a>
</CardContent>
</Card>
)}
<Card className="bg-gray-900 border-[#f7cf1d] max-w-2xl mx-auto">
<CardHeader>
<CardTitle className="text-[#f7cf1d]">
Expand Down
174 changes: 118 additions & 56 deletions DevoteApp/components/CreateProposalModal.tsx
Original file line number Diff line number Diff line change
@@ -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<File | null>(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<File | null>(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<HTMLInputElement>) => {
const handleDocUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="bg-gray-900 text-white">
<DialogHeader>
<DialogTitle className="text-[#f7cf1d]">Create New Proposal</DialogTitle>
<DialogTitle className="text-[#f7cf1d]">
Create New Proposal
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
Expand All @@ -88,7 +138,9 @@ export default function CreateProposalModal({ isOpen, onClose }: CreateProposalM
</div>
<div className="space-y-2">
<Label htmlFor="proposalDescription">Description</Label>
<p className="text-sm text-gray-400">Explain the context of your project to the people voting!</p>
<p className="text-sm text-gray-400">
Explain the context of your project to the people voting!
</p>
<Textarea
id="proposalDescription"
value={proposalDescription}
Expand All @@ -105,7 +157,10 @@ export default function CreateProposalModal({ isOpen, onClose }: CreateProposalM
onChange={(e) => handleOptionChange(index, e.target.value)}
className="bg-gray-800 border-gray-700 text-white"
/>
<Button onClick={() => handleRemoveOption(index)} variant="destructive">
<Button
onClick={() => handleRemoveOption(index)}
variant="destructive"
>
Remove
</Button>
</div>
Expand All @@ -115,22 +170,29 @@ export default function CreateProposalModal({ isOpen, onClose }: CreateProposalM
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="pdfUpload">Upload PDF Document</Label>
<Label htmlFor="docUpload">Upload Document</Label>
<Input
id="pdfUpload"
id="docUpload"
type="file"
accept=".pdf"
onChange={handlePdfUpload}
accept=".pdf, .doc, .docx"
onChange={handleDocUpload}
className="bg-gray-800 border-gray-700 text-white"
/>
{pdfDocument && <p className="text-sm text-gray-400">File selected: {pdfDocument.name}</p>}
{document && (
<p className="text-sm text-gray-400">
File selected: {document.name}
</p>
)}
</div>
<Button onClick={handleCreateProposal} className="w-full bg-[#f7cf1d] text-black hover:bg-[#e5bd0e]">
Create Proposal
<Button
onClick={handleCreateProposal}
disabled={isLoading}
className="w-full bg-[#f7cf1d] text-black hover:bg-[#e5bd0e]"
>
{isLoading ? "Creating..." : "Create Proposal"}
</Button>
</div>
</DialogContent>
</Dialog>
)
);
}

1 change: 1 addition & 0 deletions DevoteApp/interfaces/Proposal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export interface ProposalPublic {
has_voted: number;
type_votes: ProposalVoteTypeStruct[];
voter: ProposalVoterStruct;
file?: string;
}