diff --git a/backend/server.ts b/backend/server.ts index 220d0e3..5deaac0 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -6,7 +6,7 @@ import { colorConvert } from "./src/routes/colorconvert.route"; const app = Fastify({ logger: true }); -app.register(cors, { origin: "*" }); +app.register(cors, { origin: "*", exposedHeaders: 'Content-Disposition' }); app.register(multipart); app.register(libreConvert); app.register(colorConvert); diff --git a/backend/src/routes/libreconvert.route.ts b/backend/src/routes/libreconvert.route.ts index d28705e..0d726ac 100644 --- a/backend/src/routes/libreconvert.route.ts +++ b/backend/src/routes/libreconvert.route.ts @@ -1,33 +1,65 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import * as libre from "libreoffice-convert"; import { promisify } from "util"; +import { MultipartValue } from "@fastify/multipart"; const libreConvertAsync = promisify(libre.convert); +const mimeTypes: { [key: string]: string } = { + 'pdf': 'application/pdf', + 'html': 'text/html', + 'doc': 'application/msword', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'txt': 'text/plain', + 'rtf': 'application/rtf', + 'odt': 'application/vnd.oasis.opendocument.text', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xls': 'application/vnd.ms-excel', + 'ods': 'application/vnd.oasis.opendocument.spreadsheet', + 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ppt': 'application/vnd.ms-powerpoint', + 'odp': 'application/vnd.oasis.opendocument.presentation' +}; + export async function libreConvert(app: FastifyInstance) { - app.post( - "/api/libre-convert", - async (request: FastifyRequest, reply: FastifyReply) => { - try { - const data = await request.file(); - if (!data) { - return reply.status(400).send({ error: "No file uploaded!" }); + app.post("/api/libre-convert", async (request: FastifyRequest, reply: FastifyReply) => { + try { + const parts = request.parts(); + + let fileBuffer: Buffer | null = null; + let outputFileExt = ""; + + for await (const part of parts) { + if (part.type === "file") { + fileBuffer = await part.toBuffer(); + } else if (part.fieldname === "outputFormat" && part.type === "field") { + outputFileExt = (part as MultipartValue).value; + console.log("Output format:", outputFileExt); } - - const ext = ".pdf"; - const format = ext.substring(1); - const fileBuffer = await data.toBuffer(); - - const pdfBuffer = await libreConvertAsync(fileBuffer, ext, undefined); - - reply - .header("Content-Type", "application/" + format) - .header("Content-Disposition", "attachment; filename=converted" + ext) - .send(pdfBuffer); - } catch (error) { - console.error("Convert error:", error); - reply.status(500).send({ error: "Error while converting!" }); } - }, - ); + + if (!fileBuffer) { + return reply.status(400).send({ error: "No file uploaded!" }); + } + if (!outputFileExt) { + return reply.status(400).send({ error: "No output format provided!" }); + } + if (!outputFileExt.startsWith(".")) { + outputFileExt = "." + outputFileExt; + } + + const format = outputFileExt.substring(1); + const mimeType = mimeTypes[format] || 'application/octet-stream'; + + const convertedBuffer = await libreConvertAsync(fileBuffer, outputFileExt, undefined); + + reply + .header("Content-Type", mimeType) + .header("Content-Disposition", `attachment; filename="converted${outputFileExt}"`) + .send(convertedBuffer); + } catch (error) { + console.error("Convert error:", error); + reply.status(500).send({ error: "Error while converting!" }); + } + }); } diff --git a/frontend/src/app/doc-converter/layout.tsx b/frontend/src/app/doc-converter/layout.tsx index e813701..68aa695 100644 --- a/frontend/src/app/doc-converter/layout.tsx +++ b/frontend/src/app/doc-converter/layout.tsx @@ -4,7 +4,7 @@ import { toolLinks } from "@/constants"; export const metadata: Metadata = { title: toolLinks[0].title, - description: "Converter for documents to pdf format!", + description: "Converter for documents!", }; export default function RootLayout({ diff --git a/frontend/src/app/doc-converter/page.tsx b/frontend/src/app/doc-converter/page.tsx index c275ce7..a09ee3d 100644 --- a/frontend/src/app/doc-converter/page.tsx +++ b/frontend/src/app/doc-converter/page.tsx @@ -1,26 +1,50 @@ "use client"; - import React, { useState } from "react"; import Navbar from "../../components/Navbar"; import Footer from "../../components/Footer"; import Button from "../../components/Button"; import Link from "next/link"; import { ChevronDown, ChevronUp } from "lucide-react"; +import Dropdown from "@/components/Dropdown"; +import { FileFormatsTable, outputFileFormats } from "@/constants"; -export default function Home() { +export default function DocConverter() { const [file, setFile] = useState(null); const [downloadUrl, setDownloadUrl] = useState(""); const [loading, setLoading] = useState(false); const [tableOpen, setTableOpen] = useState(false); + const [filteredOptions, setFilteredOptions] = useState([]); + const [selectedOutputFormat, setSelectedOutputFormat] = useState( + "", + ); const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { - setFile(event.target.files[0]); + const selectedFile = event.target.files[0]; + const fileExtension = selectedFile.name.split(".").pop()?.toLowerCase(); + + const isSupported = outputFileFormats.some((format) => + format.input.toLowerCase().includes(fileExtension || ""), + ); + + if (!isSupported) { + console.error("Not supported file uploaded!"); + alert("File format not supported!"); + event.target.value = ""; + return; + } + + setFile(selectedFile); setDownloadUrl(""); + + const matchedFormat = outputFileFormats.find((format) => + format.input.toLowerCase().includes(fileExtension || ""), + ); + setFilteredOptions(matchedFormat ? matchedFormat.output : []); } }; - const convertToPdf = async () => { + const convertDoc = async () => { if (!file) { alert("No file selected"); return; @@ -28,6 +52,7 @@ export default function Home() { const formData = new FormData(); formData.append("file", file); + formData.append("outputFormat", selectedOutputFormat); setLoading(true); @@ -45,7 +70,9 @@ export default function Home() { } const blob = await response.blob(); + console.log("Blob:", blob); const url = window.URL.createObjectURL(blob); + console.log("Download URL:", url); setDownloadUrl(url); } catch (error) { console.error("Error while converting:", error); @@ -56,16 +83,31 @@ export default function Home() { }; return ( -
+
-
+

doc-converter

- +
+ + 0 ? filteredOptions : [""]} + onClick={(event) => { + const selectedFormat = + event.currentTarget.textContent?.trim() || ""; + setSelectedOutputFormat(selectedFormat); + }} + /> +
{downloadUrl ? ( @@ -80,127 +122,58 @@ export default function Home() { "convert" ) } - onClick={convertToPdf} + onClick={convertDoc} /> )}
- { - // TODO: Fix Table Head widths (fixed and the same as the max body width) - // TODO: Add hover animation to table head (scale-110, blue text, blue border) - // TODO: Fix general site height and add scrolling - // TODO: Add animated pop in for the table body - } -
+
setTableOpen(!tableOpen)} > - +
- - +
📥 Input Format -

📤 Output Format

- {tableOpen ? ( - - ) : ( - - )} +
+ 📥 Input Format + +

📤 Output Format

+
+ {tableOpen ? ( + + ) : ( + + )} +
- {tableOpen && ( - +
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {FileFormatsTable.map((format) => ( + + + + + ))}
.doc (MS Word) - .pdf, .docx, .odt, .txt, .rtf, .html, .epub -
.docx (MS Word) - .pdf, .odt, .txt, .rtf, .html, .epub -
- .odt (OpenDocument Text) - - .pdf, .doc, .docx, .txt, .rtf, .html, .epub -
- .rtf (Rich Text Format) - - .pdf, .doc, .docx, .odt, .txt, .html -
.txt (Text) - .pdf, .doc, .docx, .odt, .txt, .html -
.html (Webseite) - .pdf, .doc, .docx, .odt, .rtf, .txt -
.epub (E-Book) - .pdf, .doc, .docx, .odt, .rtf, .txt -
.xls (MS Excel).pdf, .xlsx, .ods, .csv
.xlsx (MS Excel).pdf, .xls, .ods, .csv
- .ods (OpenDocument Spreadsheet) - .pdf, .xls, .xlsx, .csv
- .csv (Comma-Separated Values) - .pdf, .xls, .xlsx, .ods
.ppt (MS PowerPoint).pdf, .pptx, .odp
.pptx (MS PowerPoint).pdf, .ppt, .odp
- .odp (OpenDocument Presentation) - .pdf, .ppt, .pptx
+ {format.input} + + {format.output.join(", ")} +
- )} +
-
+
); } diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx new file mode 100644 index 0000000..23e0951 --- /dev/null +++ b/frontend/src/components/Dropdown.tsx @@ -0,0 +1,79 @@ +"use client"; +import React, { useState } from "react"; +import Link from "next/link"; + +interface DropdownProps { + content: string; + options: string[]; + className?: string; + onClick?: (event: React.MouseEvent) => void; +} + +const Dropdown = (props: DropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const closeDropdown = (event: React.MouseEvent) => { + setIsOpen(false); + if (props.onClick) props.onClick(event); + }; + + return ( +
+
+ +
+
    + {props.options?.map((option) => ( +
  • + + {option} + +
  • + ))} +
+
+
+
+ ); +}; + +export default Dropdown; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 62ea718..344ac59 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -36,3 +36,121 @@ export const toolLinks = [ link: "/pomodoro-timer", }, ]; + +export const outputFileFormats = [ + { + input: ".doc", + output: [".pdf", ".docx", ".odt", ".txt", ".rtf", ".html", ".epub"], + }, + { + input: ".docx", + output: [".pdf", ".odt", ".txt", ".rtf", ".html", ".epub"], + }, + { + input: ".odt", + output: [".pdf", ".doc", ".docx", ".txt", ".rtf", ".html", ".epub"], + }, + { + input: ".rtf", + output: [".pdf", ".doc", ".docx", ".odt", ".txt", ".html"], + }, + { + input: ".txt", + output: [".pdf", ".doc", ".docx", ".odt", ".txt", ".html"], + }, + { + input: ".html", + output: [".pdf", ".doc", ".docx", ".odt", ".rtf", ".txt"], + }, + { + input: ".epub", + output: [".pdf", ".doc", ".docx", ".odt", ".rtf", ".txt"], + }, + { + input: ".xls", + output: [".pdf", ".xlsx", ".ods", ".csv"], + }, + { + input: ".xlsx", + output: [".pdf", ".xls", ".ods", ".csv"], + }, + { + input: ".ods", + output: [".pdf", ".xls", ".xlsx", ".csv"], + }, + { + input: ".csv", + output: [".pdf", ".xls", ".xlsx", ".ods"], + }, + { + input: ".ppt", + output: [".pdf", ".pptx", ".odp"], + }, + { + input: ".pptx", + output: [".pdf", ".ppt", ".odp"], + }, + { + input: ".odp", + output: [".pdf", ".ppt", ".pptx"], + }, +]; + +export const FileFormatsTable = [ + { + input: ".doc (MS Word)", + output: [".pdf", ".docx", ".odt", ".txt", ".rtf", ".html", ".epub"], + }, + { + input: ".docx (MS Word)", + output: [".pdf", ".odt", ".txt", ".rtf", ".html", ".epub"], + }, + { + input: ".odt (OpenDocument Text)", + output: [".pdf", ".doc", ".docx", ".txt", ".rtf", ".html", ".epub"], + }, + { + input: ".rtf (Rich Text Format)", + output: [".pdf", ".doc", ".docx", ".odt", ".txt", ".html"], + }, + { + input: ".txt (Text)", + output: [".pdf", ".doc", ".docx", ".odt", ".txt", ".html"], + }, + { + input: ".html (Webseite)", + output: [".pdf", ".doc", ".docx", ".odt", ".rtf", ".txt"], + }, + { + input: ".epub (E-Book)", + output: [".pdf", ".doc", ".docx", ".odt", ".rtf", ".txt"], + }, + { + input: ".xls (MS Excel)", + output: [".pdf", ".xlsx", ".ods", ".csv"], + }, + { + input: ".xlsx (MS Excel)", + output: [".pdf", ".xls", ".ods", ".csv"], + }, + { + input: ".ods (OpenDocument Spreadsheet)", + output: [".pdf", ".xls", ".xlsx", ".csv"], + }, + { + input: ".csv (Comma-Separated Values)", + output: [".pdf", ".xls", ".xlsx", ".ods"], + }, + { + input: ".ppt (MS PowerPoint)", + output: [".pdf", ".pptx", ".odp"], + }, + { + input: ".pptx (MS PowerPoint)", + output: [".pdf", ".ppt", ".odp"], + }, + { + input: ".odp (OpenDocument Presentation)", + output: [".pdf", ".ppt", ".pptx"], + }, +];