From 9e2215b70ab778cb06fbd5742c6a41fab700a7c0 Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sat, 15 Feb 2025 22:12:14 +0100 Subject: [PATCH 1/4] feat: table as dropdown for information about input and output file formats --- frontend/src/app/doc-converter/layout.tsx | 2 +- frontend/src/app/doc-converter/page.tsx | 127 +++++++++++++--------- 2 files changed, 78 insertions(+), 51 deletions(-) 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..8255217 100644 --- a/frontend/src/app/doc-converter/page.tsx +++ b/frontend/src/app/doc-converter/page.tsx @@ -7,7 +7,7 @@ import Button from "../../components/Button"; import Link from "next/link"; import { ChevronDown, ChevronUp } from "lucide-react"; -export default function Home() { +export default function DocConverter() { const [file, setFile] = useState(null); const [downloadUrl, setDownloadUrl] = useState(""); const [loading, setLoading] = useState(false); @@ -20,7 +20,7 @@ export default function Home() { } }; - const convertToPdf = async () => { + const convertDoc = async () => { if (!file) { alert("No file selected"); return; @@ -56,9 +56,9 @@ export default function Home() { }; return ( -
+
-
+

doc-converter

)}
{ // 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 && ( - +
+
- - + - - + - - - - - - + - - + - - + - - + + - - + + - - + - - + - - + + - - + + - - + - +
.doc (MS Word) + + .doc (MS Word) + .pdf, .docx, .odt, .txt, .rtf, .html, .epub
.docx (MS Word) + + .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) + .txt (Text) .pdf, .doc, .docx, .odt, .txt, .html
.html (Webseite) + + .html (Webseite) + .pdf, .doc, .docx, .odt, .rtf, .txt
.epub (E-Book) + + .epub (E-Book) + .pdf, .doc, .docx, .odt, .rtf, .txt
.xls (MS Excel).pdf, .xlsx, .ods, .csv + .xls (MS Excel) + + .pdf, .xlsx, .ods, .csv +
.xlsx (MS Excel).pdf, .xls, .ods, .csv + .xlsx (MS Excel) + .pdf, .xls, .ods, .csv
+ .ods (OpenDocument Spreadsheet) .pdf, .xls, .xlsx, .csv + .pdf, .xls, .xlsx, .csv +
+ .csv (Comma-Separated Values) .pdf, .xls, .xlsx, .ods + .pdf, .xls, .xlsx, .ods +
.ppt (MS PowerPoint).pdf, .pptx, .odp + .ppt (MS PowerPoint) + .pdf, .pptx, .odp
.pptx (MS PowerPoint).pdf, .ppt, .odp + .pptx (MS PowerPoint) + .pdf, .ppt, .odp
+
.odp (OpenDocument Presentation) .pdf, .ppt, .pptx.pdf, .ppt, .pptx
- )} +
-
+
); } From 2c582aaa60bcb78809af565fbc105e3031965cfc Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sat, 15 Feb 2025 23:11:07 +0100 Subject: [PATCH 2/4] feat: new dropdown menu for output file format as standalone component --- frontend/src/app/doc-converter/page.tsx | 21 +++--- frontend/src/components/Dropdown.tsx | 93 +++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/Dropdown.tsx diff --git a/frontend/src/app/doc-converter/page.tsx b/frontend/src/app/doc-converter/page.tsx index 8255217..469e550 100644 --- a/frontend/src/app/doc-converter/page.tsx +++ b/frontend/src/app/doc-converter/page.tsx @@ -6,6 +6,7 @@ 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"; export default function DocConverter() { const [file, setFile] = useState(null); @@ -60,12 +61,16 @@ export default function DocConverter() {

doc-converter

- +
+ + {/* TODO: Fix Dropdown menu placement */} + +
{downloadUrl ? ( @@ -84,10 +89,6 @@ export default function DocConverter() { /> )}
- { - // 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) - }
{ + const [isOpen, setIsOpen] = useState(false); + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const closeDropdown = () => { + setIsOpen(false); + }; + + return ( +
+
+ +
+
    +
  • + + Option 1 + +
  • +
  • + + Option 2 + +
  • +
  • + + Option 3 + +
  • +
+
+
+
+ ); +}; + +export default Dropdown; From 43abb6d5fd0202714b24ce674cb70194d45a6156 Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sun, 16 Feb 2025 17:52:08 +0100 Subject: [PATCH 3/4] feat: extended backend functionality for converting files to the selected file extension sent to the backend via api --- backend/server.ts | 2 +- backend/src/routes/libreconvert.route.ts | 57 +++++++++++++++++++----- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/backend/server.ts b/backend/server.ts index 03bb2e9..5156540 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -5,7 +5,7 @@ import { libreConvert } from "./src/routes/libreconvert.route"; const app = Fastify({ logger: true }); -app.register(cors, { origin: "*" }); +app.register(cors, { origin: "*", exposedHeaders: 'Content-Disposition' }); app.register(multipart); app.register(libreConvert); diff --git a/backend/src/routes/libreconvert.route.ts b/backend/src/routes/libreconvert.route.ts index ac4c8af..746be93 100644 --- a/backend/src/routes/libreconvert.route.ts +++ b/backend/src/routes/libreconvert.route.ts @@ -1,30 +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!" }); + 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(); + 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 pdfBuffer = await libreConvertAsync(fileBuffer, ext, undefined); + const format = outputFileExt.substring(1); + const mimeType = mimeTypes[format] || 'application/octet-stream'; + + const convertedBuffer = await libreConvertAsync(fileBuffer, outputFileExt, undefined); reply - .header("Content-Type", "application/" + format) - .header("Content-Disposition", "attachment; filename=converted" + ext) - .send(pdfBuffer); + .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!" }); } }); -} +} \ No newline at end of file From f149472975dbaf6124ced329a7294a2bf36a727a Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sun, 16 Feb 2025 17:53:19 +0100 Subject: [PATCH 4/4] feat: new dropdown menu for output file format selection + dynamic rendering of the information table --- frontend/src/app/doc-converter/page.tsx | 161 ++++++++---------------- frontend/src/components/Dropdown.tsx | 58 ++++----- frontend/src/constants/index.ts | 118 +++++++++++++++++ 3 files changed, 193 insertions(+), 144 deletions(-) diff --git a/frontend/src/app/doc-converter/page.tsx b/frontend/src/app/doc-converter/page.tsx index 469e550..a09ee3d 100644 --- a/frontend/src/app/doc-converter/page.tsx +++ b/frontend/src/app/doc-converter/page.tsx @@ -1,5 +1,4 @@ "use client"; - import React, { useState } from "react"; import Navbar from "../../components/Navbar"; import Footer from "../../components/Footer"; @@ -7,17 +6,41 @@ 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 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 : []); } }; @@ -29,6 +52,7 @@ export default function DocConverter() { const formData = new FormData(); formData.append("file", file); + formData.append("outputFormat", selectedOutputFormat); setLoading(true); @@ -46,7 +70,9 @@ export default function DocConverter() { } 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); @@ -61,15 +87,26 @@ export default function DocConverter() {

doc-converter

-
+
- {/* TODO: Fix Dropdown menu placement */} - + 0 ? filteredOptions : [""]} + onClick={(event) => { + const selectedFormat = + event.currentTarget.textContent?.trim() || ""; + setSelectedOutputFormat(selectedFormat); + }} + />
{downloadUrl ? ( @@ -121,108 +158,16 @@ export default function DocConverter() { > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {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 index 8a9d93c..23e0951 100644 --- a/frontend/src/components/Dropdown.tsx +++ b/frontend/src/components/Dropdown.tsx @@ -2,12 +2,11 @@ import React, { useState } from "react"; import Link from "next/link"; -// TODO: Adjust the width of the option list to match the width of the button -// TODO: Dynamic rendering of options - interface DropdownProps { content: string; + options: string[]; className?: string; + onClick?: (event: React.MouseEvent) => void; } const Dropdown = (props: DropdownProps) => { @@ -17,16 +16,17 @@ const Dropdown = (props: DropdownProps) => { setIsOpen(!isOpen); }; - const closeDropdown = () => { + const closeDropdown = (event: React.MouseEvent) => { setIsOpen(false); + if (props.onClick) props.onClick(event); }; return ( -
-
+
+
    -
  • - - Option 1 - -
  • -
  • - - Option 2 - -
  • -
  • - - Option 3 - -
  • + {props.options?.map((option) => ( +
  • + + {option} + +
  • + ))}
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"], + }, +];