From afc303674a87f4be4d34106800e3f9c6e5650367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorien=20Gr=C3=B6nwald?= <57871803+doriengr@users.noreply.github.com> Date: Sun, 6 Oct 2024 19:02:14 +0200 Subject: [PATCH] Fix/make file-imput more accessible and use semantic html (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: cleanup code * feat: make form more accessible and use semantic html elements * chore: delete not needed axios * fix: just paste error to the frontend * fix: show error message if the selected file is not a csv file * fix: hide delete button if no file is selected --------- Co-authored-by: Dorien Grönwald --- frontend/package.json | 3 +- .../general/buttons/PrimaryButton.tsx | 15 +- .../general/fileUpload/FileUpload.tsx | 248 ++++-------------- .../src/components/general/filter/Dialog.tsx | 6 +- .../src/components/general/form/Modal.tsx | 78 +++--- frontend/src/components/ui/input.tsx | 25 ++ .../src/routes/_protected/settings/import.tsx | 81 +++++- yarn.lock | 9 - 8 files changed, 196 insertions(+), 269 deletions(-) create mode 100644 frontend/src/components/ui/input.tsx diff --git a/frontend/package.json b/frontend/package.json index 3ce3a872..d218e5dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,8 +47,7 @@ "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8", - "zustand": "^4.5.4", - "axios": "^1.7.7" + "zustand": "^4.5.4" }, "devDependencies": { "@tanstack/react-query-devtools": "^5.50.1", diff --git a/frontend/src/components/general/buttons/PrimaryButton.tsx b/frontend/src/components/general/buttons/PrimaryButton.tsx index 731993a7..19faa753 100644 --- a/frontend/src/components/general/buttons/PrimaryButton.tsx +++ b/frontend/src/components/general/buttons/PrimaryButton.tsx @@ -1,19 +1,18 @@ +import { MoveRight } from 'lucide-react'; import React from 'react'; interface PrimaryButtonProps extends React.ButtonHTMLAttributes { label: string; - icon?: React.ReactNode; // Optional icon prop + isDanger?: boolean; } -const PrimaryButton: React.FC = ({ label, icon, ...props }) => ( - ); diff --git a/frontend/src/components/general/fileUpload/FileUpload.tsx b/frontend/src/components/general/fileUpload/FileUpload.tsx index 3e7c8432..3ab9868a 100644 --- a/frontend/src/components/general/fileUpload/FileUpload.tsx +++ b/frontend/src/components/general/fileUpload/FileUpload.tsx @@ -1,203 +1,67 @@ -import React, { useRef, useState } from "react"; -import axios from "axios"; -import { Check, MoveLeft, MoveRight, Trash2 } from "lucide-react"; -import { useAuthHeader } from "@/hooks/useAuthHeader"; -import PrimaryButton from "../buttons/PrimaryButton"; -import ModalField from "../form/Modal"; - - +import React, { useEffect, useRef } from "react"; +import { Trash2 } from "lucide-react"; interface FileUploadProps { - to: string; - fileType: string + fileType: string; + name: string; + message?: string; + showDeleteButton: boolean; + clearFileInput: () => void; + handleFileChange: (event: React.ChangeEvent) => void; } -const FileUpload: React.FC = ({ to, fileType }) => { - const inputRef = useRef(null); - const authHeader = useAuthHeader(); - const [selectedFile, setSelectedFile] = useState(null); - const [progress, setProgress] = useState(0); - const [errorMessage, setErrorMessage] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - - - type Status = "select" | "uploading" | "done"; - const status = { select: "select", uploading: "uploading", done: "done" } as const; - const [uploadStatus, setUploadStatus] = useState(status.select); - - - const handleFileChange = (event: React.ChangeEvent) => { - if (event.target.files && event.target.files.length > 0) { - setSelectedFile(event.target.files[0]); - } - }; - - const onChooseFile = () => { - inputRef.current?.click(); - }; - - const clearFileInput = () => { - if (inputRef.current) { - inputRef.current.value = ""; - } - setSelectedFile(null); - setProgress(0); - setUploadStatus(status.select); - setErrorMessage(null); - }; - - const handleUpload = () => { - if (uploadStatus != status.done) { - setIsModalOpen(true) - } else { - clearFileInput(); - } - } - const handleCancel = () => { - setIsModalOpen(false); - clearFileInput(); +const FileUpload: React.FC = ({ + fileType, + name = "file", + message = "", + handleFileChange, + clearFileInput, + showDeleteButton, +}) => { + const inputFileRef = useRef(null); + + const handleClearInput = () => { + if (inputFileRef.current) { + inputFileRef.current.value = ""; } + clearFileInput(); + }; - - const handleConfirm = async () => { - console.log("handle upload"); - setIsModalOpen(false); - try { - setUploadStatus(status.uploading); - setErrorMessage(null); - - - const formData = new FormData(); - if (selectedFile) { - formData.append("file", selectedFile); - } - - const pr = await axios.post(to, formData, { - headers: { - 'Authorization': authHeader, - 'Content-Type': 'multipart/form-data', - }, onUploadProgress: (progressEvent) => { - if (progressEvent.total) { - const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); - setProgress(percentCompleted); - } - } - }); - - console.log(pr) - - setUploadStatus(status.done); - } catch (error) { - handleAxiosError(error); - setUploadStatus(status.select); - } - - }; - const handleAxiosError = (error: any) => { - - if (error.response && error.response.data && error.response.data.error) { - var errorMessage = error.response.data.error; - const errorMessage_ = error.response.data.err; - if (error.response.data.err) { - errorMessage = errorMessage + ": " + errorMessage_; - } - console.error('Error Message:', errorMessage); - setErrorMessage(errorMessage); - } else { - - if (error.message) { - console.error('Error Message:', error.message); - setErrorMessage(error.message); - } - } + useEffect(() => { + if (message && inputFileRef.current) { + inputFileRef.current.value = ""; } - - return ( -
- - - {/* Button to trigger the file input dialog */} - {!selectedFile && ( - - )} - - {selectedFile && ( - <> -
- 📄 - -
-

{selectedFile?.name}

- -
-
-
-
- - {uploadStatus === status.select ? ( - - ) : ( -
- {uploadStatus === "uploading" ? ( - `${progress}%` - ) : uploadStatus === status.done ? ( - - ) : null} -
- )} -
- - ) : ( - - )} className="w-72 p-4 mt-10"> - - )} - {isModalOpen && ( - - )} - {errorMessage && ( -
- {errorMessage} -
- )} + }, [message]); + + return ( + <> +
+ + + +
+ + {message && ( +
+ {message}
- ); + )} + + ); }; export default FileUpload; diff --git a/frontend/src/components/general/filter/Dialog.tsx b/frontend/src/components/general/filter/Dialog.tsx index 127ffb18..a96c9bf4 100644 --- a/frontend/src/components/general/filter/Dialog.tsx +++ b/frontend/src/components/general/filter/Dialog.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import FilterButton from '../buttons/FilterButton'; import PrimaryButton from '../buttons/PrimaryButton'; import SecondaryButton from '../buttons/SecondaryButton'; -import { MoveRight, X } from 'lucide-react'; +import { X } from 'lucide-react'; import useOutsideClick from '@/hooks/useOutsideClick'; import Option from './Option'; import { useNavigate } from '@tanstack/react-router'; @@ -124,9 +124,7 @@ const Dialog: React.FC = ({ initStatusTags, initRegionTags, headlin
- - } type="button" onClick={applyFilters} /> +
diff --git a/frontend/src/components/general/form/Modal.tsx b/frontend/src/components/general/form/Modal.tsx index b7e93a03..11da9bb2 100644 --- a/frontend/src/components/general/form/Modal.tsx +++ b/frontend/src/components/general/form/Modal.tsx @@ -1,52 +1,44 @@ -import { MoveRight } from "lucide-react"; import React from "react"; import PrimaryButton from "../buttons/PrimaryButton"; +import SecondaryButton from "../buttons/SecondaryButton"; interface ModalProps { - title: string; - description: string; - confirmText: string; - onConfirm: () => void; - onCancel: () => void; - isOpen: boolean; + title: string; + description: string; + confirmText: string; + onConfirm: () => void; + onCancel: () => void; + isOpen: boolean; } -const ModalField: React.FC = ({ - title, - description, - confirmText, - onConfirm, - onCancel, - isOpen, +const Modal: React.FC = ({ + title, + description, + confirmText, + onConfirm, + onCancel, + isOpen, }) => { - if (!isOpen) return null; - return ( - <> -
-
-
-

{title}

-

- {description} -

-
- } - label={confirmText} - /> - -
-
-
- - ); + return ( + <> +
+
+

{title}

+

{description}

+
+ + +
+
+ + ); }; -export default ModalField; +export default Modal; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 00000000..5af26b2c --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/frontend/src/routes/_protected/settings/import.tsx b/frontend/src/routes/_protected/settings/import.tsx index 178ef019..ed3945f6 100644 --- a/frontend/src/routes/_protected/settings/import.tsx +++ b/frontend/src/routes/_protected/settings/import.tsx @@ -1,6 +1,9 @@ import { createFileRoute } from '@tanstack/react-router' import FileUpload from '@/components/general/fileUpload/FileUpload'; import GeneralStatusCard from '@/components/general/cards/GeneralStatusCard'; +import Modal from '@/components/general/form/Modal'; +import PrimaryButton from '@/components/general/buttons/PrimaryButton'; +import { useEffect, useState } from 'react'; import { useTrees } from '@/hooks/useTrees'; export const Route = createFileRoute('/_protected/settings/import')({ @@ -8,6 +11,9 @@ export const Route = createFileRoute('/_protected/settings/import')({ }) function ImportFile() { + const [isModalOpen, setIsModalOpen] = useState(false); + const [file, setFile] = useState(null); + const [message, setMessage] = useState(""); const trees = useTrees(); const getReadonlyTreesLength = () => { @@ -16,8 +22,41 @@ function ImportFile() { const readonlyTrees = trees.filter(tree => tree.readonly); return readonlyTrees.length; } - - const url = '/api-local/v1/import/csv' + + const handleFileChange = (event: React.ChangeEvent) => { + if (!event.target.files) return; + if (event.target.files.length === 0) setFile(null); + + const file = event.target.files[0]; + if (file.type !== "text/csv") { + setMessage("Es sind nur CSV-Dateien erlaubt."); + return; + } + + setFile(event.target.files[0]); + }; + + const handleConfirm = async () => { + setIsModalOpen(false); + + try { + if (!file) return; + + setMessage(""); + const formData = new FormData(); + formData.append("file", file); + + // TODO: Send form to provided endpoint of backend client + + setFile(null); + setMessage("Es wurden erfolgreich neue Daten importiert."); + } catch (error: any) { + console.error(error); + setMessage(error) + } + }; + + // TODO: use real date of import const cards = [ { headline: 'Anzahl der importierten Bäume', @@ -37,7 +76,7 @@ function ImportFile() { 'Am 20.05.2024 wurde das letzte Mal die Bäume anhand einer CSV Datei importiert.', }, ]; - + return (
@@ -52,7 +91,7 @@ function ImportFile() { Mollit laborum officia commodo mollit dolor deserunt qui occaecat anim.

-
+
    {cards.map((card, key) => (
  • @@ -61,15 +100,35 @@ function ImportFile() { ))}
- {/* File input section */} -
+
+

Import neu anstoßen:

+

+ CSV-Datei mit aktuellen Bäumen: +

+
+ setFile(null)} /> + -

Import neu anstoßen:

+ setIsModalOpen(true)} + disabled={!file} + className="mt-10" + label="Daten importieren" /> - - + setIsModalOpen(false)} + isOpen={isModalOpen} + />
); diff --git a/yarn.lock b/yarn.lock index 10b10b42..a132b7a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1503,15 +1503,6 @@ axios@1.6.8: form-data "^4.0.0" proxy-from-env "^1.1.0" -axios@^1.7.7: - version "1.7.7" - resolved "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz" - integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - babel-dead-code-elimination@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.6.tgz"