Skip to content

Commit

Permalink
Fix/make file-imput more accessible and use semantic html (#97)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
doriengr and Dorien Grönwald authored Oct 6, 2024
1 parent bf816d5 commit afc3036
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 269 deletions.
3 changes: 1 addition & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 7 additions & 8 deletions frontend/src/components/general/buttons/PrimaryButton.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { MoveRight } from 'lucide-react';
import React from 'react';

interface PrimaryButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
label: string;
icon?: React.ReactNode; // Optional icon prop
isDanger?: boolean;
}

const PrimaryButton: React.FC<PrimaryButtonProps> = ({ label, icon, ...props }) => (
<button
const PrimaryButton: React.FC<PrimaryButtonProps> = ({ label, isDanger = false, ...props }) => (
<button
{...props}
className={`bg-green-dark text-white px-5 py-2 group flex gap-x-3 rounded-xl items-center transition-all ease-in-out duration-300 hover:bg-green-light ${props.className}`}
className={`${isDanger ? 'bg-red' : 'bg-green-dark hover:bg-green-light'} text-white w-fit px-5 py-2 group flex gap-x-3 rounded-xl items-center transition-all ease-in-out duration-300 disabled:bg-dark-400 ${props.className}`}
>
<span className="font-medium">{label}</span>

{/* Render the icon if it's provided */}
{icon && <span className="transition-all ease-in-out duration-300 group-hover:translate-x-2">{icon}</span>}
<span className="font-medium text-base">{label}</span>
<MoveRight className="transition-all ease-in-out duration-300 group-hover:translate-x-2" />
</button>
);

Expand Down
248 changes: 56 additions & 192 deletions frontend/src/components/general/fileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => void;
}

const FileUpload: React.FC<FileUploadProps> = ({ to, fileType }) => {
const inputRef = useRef<HTMLInputElement | null>(null);
const authHeader = useAuthHeader();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [progress, setProgress] = useState<number>(0);
const [errorMessage, setErrorMessage] = useState<string | null>(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>(status.select);


const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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<FileUploadProps> = ({
fileType,
name = "file",
message = "",
handleFileChange,
clearFileInput,
showDeleteButton,
}) => {
const inputFileRef = useRef<HTMLInputElement | null>(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 (
<div className="w-full flex flex-col justify-center">
<input
ref={inputRef}
accept={fileType}
type="file"
onChange={handleFileChange}
className="hidden"
/>

{/* Button to trigger the file input dialog */}
{!selectedFile && (
<button
className="w-80 h-36 text-lg font-medium flex flex-col items-center justify-center gap-4 text-indigo-600 bg-black border-2 border-dashed border-indigo-600 rounded-xl cursor-pointer transition duration-300 hover:bg-gray-100"
onClick={onChooseFile}
>
<span className="w-12 h-12 text-4xl text-indigo-600 flex items-center justify-center rounded-full bg-indigo-50">
⬆️
</span>
Datei hochladen
</button>
)}

{selectedFile && (
<>
<div className="w-72 p-4 bg-black rounded-lg flex items-center gap-4 border border-indigo-300">
<span className="text-indigo-600 text-2xl">📄</span>

<div className="flex-1">
<p className="text-sm font-medium">{selectedFile?.name}</p>

<div className="w-full h-1.5 bg-gray-200 rounded mt-2">
<div
className="h-1.5 bg-indigo-600 rounded"
style={{ width: `${progress}%` }}
/>
</div>
</div>

{uploadStatus === status.select ? (
<button
className="w-9 h-9 text-indigo-600 bg-indigo-50 rounded-full flex items-center justify-center"
onClick={clearFileInput}
>
<Trash2 />
</button>
) : (
<div className="w-9 h-9 bg-indigo-50 rounded-full flex items-center justify-center text-indigo-600">
{uploadStatus === "uploading" ? (
`${progress}%`
) : uploadStatus === status.done ? (
<button
className="w-9 h-9 text-indigo-600 bg-indigo-50 rounded-full flex items-center justify-center"
onClick={clearFileInput}
>
<Check />
</button>
) : null}
</div>
)}
</div>
<PrimaryButton onClick={handleUpload} label={uploadStatus === status.done ? "erneut selektieren" : "Daten importieren"}
icon={uploadStatus === status.done ? (
<MoveLeft className="transition-all ease-in-out duration-300 group-hover:translate-x-2" />
) : (
<MoveRight className="transition-all ease-in-out duration-300 group-hover:translate-x-2" />
)} className="w-72 p-4 mt-10"></PrimaryButton>
</>
)}
{isModalOpen && (
<ModalField
title="Soll der Import wirklich neu angestoßen werden?"
description="Der Import kann etwas länger dauern, sodass die Website für einen Moment in den Wartungsmodus schaltet und nicht erreichbar ist."
confirmText="Import fortfahren"
onConfirm={handleConfirm}
onCancel={handleCancel}
isOpen={isModalOpen}
/>
)}
{errorMessage && (
<div className="mt-4 p-3 bg-red-100 text-red-700 rounded-lg">
{errorMessage}
</div>
)}
}, [message]);

return (
<>
<div className="flex items-center gap-x-4">
<input
ref={inputFileRef}
accept={fileType}
id={name}
type="file"
onChange={handleFileChange}
className="w-full items-center gap-4 text-dark-800 border border-green-light rounded-lg bg-white px-4 py-3 focus:outline-green-dark lg:w-1/2"
/>

<button
type="button"
className={`${showDeleteButton ? 'block' : 'hidden'} `}
onClick={handleClearInput}
>
<Trash2 className="text-dark-600" />
<span className="sr-only">Datei unselektieren</span>
</button>
</div>

{message && (
<div className="mt-4 text-red">
{message}
</div>
);
)}
</>
);
};

export default FileUpload;
6 changes: 2 additions & 4 deletions frontend/src/components/general/filter/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -124,9 +124,7 @@ const Dialog: React.FC<DialogProps> = ({ initStatusTags, initRegionTags, headlin
</fieldset>

<div className="flex flex-wrap gap-5 mt-6">
<PrimaryButton label="Anwenden" icon={
<MoveRight className="transition-all ease-in-out duration-300 group-hover:translate-x-2" />
} type="button" onClick={applyFilters} />
<PrimaryButton label="Anwenden" type="button" onClick={applyFilters} />
<SecondaryButton label="Zurücksetzen" onClick={resetFilters} />
</div>
</section>
Expand Down
Loading

0 comments on commit afc3036

Please sign in to comment.