diff --git a/task_3_react/src/client/App.tsx b/task_3_react/src/client/App.tsx index cb03d47..6407afb 100644 --- a/task_3_react/src/client/App.tsx +++ b/task_3_react/src/client/App.tsx @@ -1,13 +1,14 @@ import "./css/App.css"; import '@fortawesome/fontawesome-free/css/all.css'; -import { readCSV } from './csv'; -import { CSV_Data, fileInfo } from './types'; +import { CSV_Data, fileInfo, responseObject } from './types'; +import { readCSV, convertCSVtoJSON } from './csv'; -import React, { useState, useRef } from "react"; -import Layout from "./Layout"; +import React, { useState, useRef, useEffect } from "react"; +import Layout from "./components/Layout"; import CSVCard from "./components/CSVCard"; import InfoCard from "./components/InfoCard"; import DataTable from "./components/DataTable"; +import FileCard from "./components/FileCard"; function App() { const [data, setData] = useState([] as CSV_Data); @@ -18,10 +19,19 @@ function App() { rowcount: 0 }); + const [fileList, setFileList] = useState(null as responseObject | null); + // Effect has to be in top level of the component + useEffect(() => { + fetch("/status", { method: "GET" }) + .then((response) => response.json()) + .then((response) => setFileList(response)) + .catch((error) => console.log(error)); + }); + const formRef = useRef(null); // This is triggered in CSVCard - const handleFileChange = async (e: React.ChangeEvent): Promise => { + const handleFileUpload = async (e: React.ChangeEvent): Promise => { const file = e.target.files?.[0]; if (!file) throw new Error("No file received"); @@ -29,7 +39,7 @@ function App() { const newFileInfo: fileInfo = { filename: file.name, - filetype: file.type, + filetype: ".csv", // file.type delivers weird name filesize: String(file.size) + "B", rowcount: data.length } @@ -39,18 +49,31 @@ function App() { if (formRef.current) { // Upload to server const formData = new FormData(formRef.current); - const res = await fetch("/upload", { + await fetch("/upload", { method: "POST", body: formData }); - const result = await res.json(); - console.log(result); } } + const handleFileChange = async (fileName: string) => { + const response = await fetch(`/download/${fileName}`); + const blob = await response.blob(); + const text = await blob.text(); + + if (!response) throw new Error("No file received"); + + const data = await convertCSVtoJSON(text); + + // Updating fileInfo requires more effort since blob doesn't have the metadata + + setData(data); + } + return ( - + + diff --git a/task_3_react/src/client/components/CSVCard.tsx b/task_3_react/src/client/components/CSVCard.tsx index abf3726..7a94018 100644 --- a/task_3_react/src/client/components/CSVCard.tsx +++ b/task_3_react/src/client/components/CSVCard.tsx @@ -8,7 +8,7 @@ const CSVCard = (props: { return (
-

Select CSV data

+

Upload CSV data

); } diff --git a/task_3_react/src/client/Layout.tsx b/task_3_react/src/client/components/Layout.tsx similarity index 93% rename from task_3_react/src/client/Layout.tsx rename to task_3_react/src/client/components/Layout.tsx index 06c8876..9c758c2 100644 --- a/task_3_react/src/client/Layout.tsx +++ b/task_3_react/src/client/components/Layout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import "./css/Layout.css"; +import "../css/Layout.css"; const Layout = (props: { children: React.ReactNode }) => { return ( diff --git a/task_3_react/src/client/css/App.css b/task_3_react/src/client/css/App.css index 09d81a6..e1f5fe6 100644 --- a/task_3_react/src/client/css/App.css +++ b/task_3_react/src/client/css/App.css @@ -36,6 +36,19 @@ article { width: 400px; } +article.wide { + width: 800px +} + +.action-icons { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + font-size: 18px; + cursor: pointer; +} + body>main { max-height: calc(100vh - 100px); overflow-y: auto; diff --git a/task_3_react/src/client/csv.ts b/task_3_react/src/client/csv.ts index bc29697..f302e03 100644 --- a/task_3_react/src/client/csv.ts +++ b/task_3_react/src/client/csv.ts @@ -1,7 +1,7 @@ import { CSV_Data } from './types'; import { csv2json } from 'json-2-csv'; -const convertCSVtoJSON = async ( csvText: string ) => { +export const convertCSVtoJSON = async ( csvText: string ) => { // Type cast OK, as the typing of the external library is not perfect -> Actually it is. // NOTE: On transpilation to JS, it will be (more or less) disregarded anyway. // If you claim it isn't good typing, it's the same as expecting it to guess the typing, diff --git a/task_3_react/src/client/types.d.ts b/task_3_react/src/client/types.d.ts index e949b5d..ec3f0c3 100644 --- a/task_3_react/src/client/types.d.ts +++ b/task_3_react/src/client/types.d.ts @@ -9,3 +9,9 @@ export type fileInfo = { filesize: string; rowcount: number; } + +// FileCard receives this via props +export type responseObject = { + names: string[], + uploadTimes: string[] +} diff --git a/task_3_react/src/server/main.ts b/task_3_react/src/server/main.ts index 18ad63c..8b003dd 100644 --- a/task_3_react/src/server/main.ts +++ b/task_3_react/src/server/main.ts @@ -1,19 +1,24 @@ import express from "express"; import ViteExpress from "vite-express"; import multer from "multer"; +import * as fs from "node:fs/promises"; +import { responseObject } from "./types"; +import path from "path"; const app = express(); +// Set up file storage const storage = multer.diskStorage({ destination: "./src/server/uploads", filename: (_req, file, cb) => { // Suggested in Multer's readme - const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1E9); - cb(null, file.fieldname + "-" + uniqueSuffix) + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1E3); + cb(null, file.fieldname + "-" + uniqueSuffix); } }); -const upload = multer({ storage: storage }); +// CSV file upload endpoint +const upload = multer({ storage: storage }); app.post( "/upload", upload.single("dataFile"), @@ -22,6 +27,48 @@ app.post( } ); +// Endpoint to send back file names/upload times +app.get("/status", async (_req, res) => { + const resObject: responseObject = { + names: [], + uploadTimes: [] + }; + + const dir = await fs.opendir("./src/server/uploads/"); + for await (const file of dir) { + resObject.names.push(file.name); + const stats = await fs.stat(`./src/server/uploads/${file.name}`); + resObject.uploadTimes.push(stats.birthtime.toString()); + } + + res.status(200).json(resObject); +}) + +// Endpoint to send back whole files +app.get("/download/:fileName", (req, res) => { + const fileName = req.params.fileName; + const filePath = path.join(__dirname, "uploads", fileName); // Filepaths must be absolute + res.sendFile(filePath, err => { + if (err) { + console.error("Error sending file:", err); + res.status(500).send("Error downloading file"); + } + }); +}) + +// Endpoint to remove files from server +app.delete("/delete/:fileName", async (req, res) => { + const fileName = req.params.fileName; + const filePath = path.join(__dirname, "uploads", fileName); + try { + await fs.unlink(filePath); // deletes the file + res.status(200).send("File deleted successfully"); + } catch (error) { + console.error("Error deleting file:", error); + res.status(500).send("Error deleting file"); + } +}); + // example route which returns a message app.get("/hello", async function (_req, res) { diff --git a/task_3_react/src/server/types.d.ts b/task_3_react/src/server/types.d.ts new file mode 100644 index 0000000..28acf30 --- /dev/null +++ b/task_3_react/src/server/types.d.ts @@ -0,0 +1,4 @@ +export type responseObject = { + names: string[], + uploadTimes: string[] +} \ No newline at end of file diff --git a/task_3_react/src/server/uploads/dataFile-1763300594057-221106251 b/task_3_react/src/server/uploads/dataFile-1763313536334-942 similarity index 100% rename from task_3_react/src/server/uploads/dataFile-1763300594057-221106251 rename to task_3_react/src/server/uploads/dataFile-1763313536334-942