Skip to content
Snippets Groups Projects
Commit 43d92f3f authored by klamas's avatar klamas
Browse files

feat: Added modal with document details #18

parent 142f6e73
No related branches found
No related tags found
1 merge request!39feat: Added modal with document details #18
import React from "react";
const CloseIcon = ({ className = "", style = {} }) => {
return (
<svg
className={`h-6 w-6 ${className}`}
style={{ fill: "none", stroke: "currentColor", ...style }}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
};
export default CloseIcon;
import React, { useMemo } from "react";
import Link from "next/link";
import KeyValueList from "./KeyValueList";
import Section from "./Section";
import Modal from "./Modal";
import CloseIcon from "@/app/components/assets/CloseIcon";
import data from "../data/info.json";
const InfoModal = ({ show, onClose, details }) => {
const questionTypes = useMemo(() => {
if (
!details?.questionConfigDetails ||
Object.keys(details.questionConfigDetails).length === 0
) {
return null;
}
return (
<KeyValueList
data={details.questionConfigDetails}
renderValue={(question, config) => (
<>
<div className="flex">
<div>{question.trim()}:</div>
<div className="pl-1">
<div>{config}</div>
</div>
</div>
</>
)}
/>
);
}, [details.questionConfigDetails]);
const questionErrors = useMemo(() => {
if (
!details?.questionErrors ||
Object.keys(details.questionErrors).length === 0
) {
return null;
}
return (
<KeyValueList
data={details.questionErrors}
renderValue={(question, errors) => (
<div className="flex">
<div>{question.trim()}</div>
<div className="pl-1">
<KeyValueList data={errors} />
</div>
</div>
)}
/>
);
}, [details.questionErrors]);
const questionWarnings = useMemo(() => {
if (
!details?.questionWarnings ||
Object.keys(details.questionWarnings).length === 0
) {
return null;
}
return (
<KeyValueList
data={details.questionWarnings}
renderValue={(question, errors) => (
<>
<div>{question.trim()}</div>
{errors.map((error, index) => (
<div key={index} className="flex">
<div>{error}</div>
</div>
))}
</>
)}
/>
);
}, [details.questionWarnings]);
return (
<Modal show={show} onClose={onClose}>
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">{data.modal.title}</h2>
<button onClick={onClose}>
<CloseIcon />
</button>
</div>
<div className="pb-4 text-sm">
{data.modal.documentation}
<Link href="/documentation" className="pl-1 text-blue">
{data.modal.click}
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<Section title={data.modal.questionCount}>
<span className="pl-1">{details?.questionCount || "0"}</span>
</Section>
<Section title={data.modal.answersCount}>
<span className="pl-1">{details?.answersCount || "0"}</span>
</Section>
<Section title={data.modal.questionPicCount}>
<span className="pl-1">
{details?.questionPicturesCount || "0"}
</span>
</Section>
<Section title={data.modal.answerPicCount}>
<span className="pl-1">{details?.answerPictureCount || "0"}</span>
</Section>
<Section title={data.modal.skippedQuestions}>
<span className="pl-1">{details?.skippedQuestions || "0"}</span>
</Section>
<Section title={data.modal.questionTypes}>
{questionTypes || <span className="pl-1">{data.modal.none}</span>}
</Section>
<Section title={data.modal.questionErrors}>
{questionErrors || <span className="pl-1">{data.modal.none}</span>}
</Section>
<Section title={data.modal.questionWarnings}>
{questionWarnings || (
<span className="pl-1">{data.modal.none}</span>
)}
</Section>
</div>
</div>
</Modal>
);
};
export default InfoModal;
const KeyValueList = ({ data, renderValue }) => (
<div className="list-disc list-inside text-xs">
{Object.entries(data).map(([key, value], index) => (
<div key={index}>
{renderValue ? renderValue(key, value) : `${value}`}
</div>
))}
</div>
);
export default KeyValueList;
import React, { useEffect } from "react";
const Modal = ({ show, onClose, children }) => {
useEffect(() => {
const handleEscapeKey = (event) => {
if (event.key === "Escape") {
onClose();
}
};
if (show) {
document.addEventListener("keydown", handleEscapeKey);
}
return () => {
document.removeEventListener("keydown", handleEscapeKey);
};
}, [show, onClose]);
if (!show) {
return null;
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="bg-black bg-opacity-50 fixed inset-0" onClick={onClose} />
<div className="bg-white rounded-lg shadow-md p-6 max-h-[550px] overflow-y-auto relative z-10">
{children}
</div>
</div>
);
};
export default Modal;
import React from "react";
const Section = ({ title, children }) => (
<div className="pb-4">
<span className="font-bold">{title}</span>
{children}
</div>
);
export default Section;
...@@ -3,12 +3,16 @@ ...@@ -3,12 +3,16 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Button from "@/app/components/utils/Button"; import Button from "@/app/components/utils/Button";
import FileUpload from "@/app/components/utils/FileUpload"; import FileUpload from "@/app/components/utils/FileUpload";
import InfoModal from "@/app/components/modal/InfoModal";
import { sendFileToBackend } from "../utils/SendFileToBackend"; import { sendFileToBackend } from "../utils/SendFileToBackend";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import data from "../../data/en.json"; import data from "../../data/en.json";
export default function Landingpage() { export default function Landingpage() {
const [file, setFiles] = useState(null); const [file, setFiles] = useState(null);
const [details, setDetails] = useState([]);
const [showModal, setShowModal] = useState(false);
const [showDetailsButtons, setShowDetailsButtons] = useState(false);
const handleFileUpload = (uploadedFile) => { const handleFileUpload = (uploadedFile) => {
setFiles(uploadedFile); setFiles(uploadedFile);
...@@ -24,46 +28,72 @@ export default function Landingpage() { ...@@ -24,46 +28,72 @@ export default function Landingpage() {
return; return;
} }
toast.promise(sendFileToBackend(file, outputFormat), { try {
pending: { const { details } = await toast.promise(
render: data.landing.uploading, sendFileToBackend(file, outputFormat),
position: "bottom-right", {
autoClose: false, pending: {
hideProgressBar: false, render: data.landing.uploading,
}, position: "bottom-right",
success: { autoClose: false,
render: data.landing.uploaded, hideProgressBar: false,
position: "bottom-right", },
autoClose: 2000, success: {
hideProgressBar: true, render: data.landing.uploaded,
}, position: "bottom-right",
error: { autoClose: 2000,
render: data.landing.uploadError, hideProgressBar: true,
position: "bottom-right", },
autoClose: 2000, error: {
hideProgressBar: true, render: data.landing.uploadError,
}, position: "bottom-right",
}); autoClose: 2000,
hideProgressBar: true,
},
},
);
setShowDetailsButtons(true);
setDetails(details);
setShowModal(true);
} catch (error) {
throw error;
}
};
const closeModal = () => {
setShowModal(false);
};
const openModal = () => {
setShowModal(true);
}; };
return ( return (
<main> <main>
<div className="flex flex-col gap-5 justify-center items-center pt-10"> <div className="flex flex-col gap-3 justify-center items-center pt-10">
<FileUpload onFileUpload={handleFileUpload} /> <FileUpload onFileUpload={handleFileUpload} />
<div className="text-3xl text-center font-bold text-blue pt-10"> {showDetailsButtons && (
<p>{data.landing.download}</p> <Button
onClick={openModal}
variant="primary"
name={data.landing.details}
/>
)}
<div className="text-3xl text-center font-bold text-blue pt-5">
{data.landing.download}
</div> </div>
<div className="flex sm:flex-row flex-col sm:gap-32"> <div className="flex sm:flex-row flex-col sm:gap-32">
<Button <Button
name={data.landing.moodle} name={data.landing.moodle}
handleSubmit={() => handleSubmit("moodle")} onClick={() => handleSubmit("moodleXML")}
/> />
<Button <Button
name={data.landing.Coursera} name={data.landing.Coursera}
handleSubmit={() => handleSubmit("coursera")} onClick={() => handleSubmit("courseraDocx")}
/> />
</div> </div>
</div> </div>
<InfoModal show={showModal} onClose={closeModal} details={details} />
</main> </main>
); );
} }
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
export default function Button({ name, href, handleSubmit, className }) { export default function Button({ name, href, onClick, className }) {
const safeHref = href || "#"; const safeHref = href || "#";
return ( return (
<Link href={safeHref} passHref legacyBehavior> <Link href={safeHref} passHref legacyBehavior>
<a <a
className={`px-7 p-2 text-blue rounded border-2 border-blue hover:bg-blue hover:text-white transition duration-300 ease-in-out ${className}`} // Add dynamic className className={`px-7 p-2 text-blue rounded border-2 border-blue hover:bg-blue hover:text-white transition duration-300 ease-in-out ${className}`}
onClick={handleSubmit} onClick={onClick}
> >
{name} {name}
</a> </a>
......
...@@ -2,7 +2,7 @@ import React, { useCallback } from "react"; ...@@ -2,7 +2,7 @@ import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import data from "../../data/en.json"; import data from "../../data/en.json";
const FileUploadMessage = ({ isFileAccepted }) => { const FileUploadMessage = ({ isFileAccepted, details }) => {
if (isFileAccepted === null) { if (isFileAccepted === null) {
return <p>{data.fileupload.defaultMessage}</p>; return <p>{data.fileupload.defaultMessage}</p>;
} }
......
...@@ -4,42 +4,39 @@ export const sendFileToBackend = async (file, outputFormat) => { ...@@ -4,42 +4,39 @@ export const sendFileToBackend = async (file, outputFormat) => {
try { try {
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/file/convert`, `${process.env.NEXT_PUBLIC_BACKEND_URL}/file/convert/${outputFormat}`,
{ {
method: "POST", method: "POST",
body: formData, body: formData,
headers: {
Accept: "application/json",
},
}, },
); );
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error ${response.status}`); throw new Error(`Server responded with a status of ${response.status}`);
} }
const fileName = file[0].name.split(".")[0]; const data = await response.json();
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
const fileExtension = const mimeType =
outputFormat === "moodle" outputFormat === "moodleXML"
? "xml" ? "application/xml"
: outputFormat === "coursera" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
? "docx"
: "";
if (fileExtension) { const fileData = data.file;
link.download = `${fileName}.${fileExtension}`; const fileName = data.fileName;
} else { if (data.details.questionCount > 0) {
throw new Error("Invalid output format"); const link = document.createElement("a");
link.href = `data:${mimeType};base64,${fileData}`;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} }
document.body.appendChild(link); return { details: data.details };
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
return { success: true };
} catch (error) { } catch (error) {
throw error; throw error;
} }
......
...@@ -11,11 +11,26 @@ ...@@ -11,11 +11,26 @@
"uploadError": "There was an error uploading your file. Please try again.", "uploadError": "There was an error uploading your file. Please try again.",
"download": "Download as", "download": "Download as",
"moodle": "Moodle XML", "moodle": "Moodle XML",
"Coursera": "Coursera .docx" "Coursera": "Coursera .docx",
"details": "Open details"
}, },
"navbar": { "navbar": {
"documentation": "Documentation" "documentation": "Documentation"
}, },
"modal": {
"title": "Document Details",
"documentation": "Read more about documentation:",
"none": "None",
"click": "Click here!",
"questionCount": "Question Count:",
"answerPicCount": "Answer Picture Count:",
"answersCount": "Answers Count:",
"questionPicCount": "Question Picture Count:",
"questionTypes": "Question Types:",
"skippedQuestions": "Skipped Questions:",
"questionErrors": "Question Errors:",
"questionWarnings": "Question Warnings:"
},
"documentation": { "documentation": {
"title": "Welcome to Documentation Page", "title": "Welcome to Documentation Page",
"howTo": "How to make a perfect template?", "howTo": "How to make a perfect template?",
...@@ -24,5 +39,5 @@ ...@@ -24,5 +39,5 @@
"templateName": "UniTartuCS Template", "templateName": "UniTartuCS Template",
"templateLink": "https://docs.google.com/document/d/106xTDS6IG7fofL6Ju4JnqntjxLeBfBM_LIyjKtskFb8/edit", "templateLink": "https://docs.google.com/document/d/106xTDS6IG7fofL6Ju4JnqntjxLeBfBM_LIyjKtskFb8/edit",
"docLink": "https://docs.google.com/document/d/1IZWmkxqYYqBAYqW8uumiZ1k94muKS4IjpYQuOQ8-9WU/edit " "docLink": "https://docs.google.com/document/d/1IZWmkxqYYqBAYqW8uumiZ1k94muKS4IjpYQuOQ8-9WU/edit "
} }
} }
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
"@/*": ["./*"] "@/*": ["./*"]
} }
} }
} }
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment