mirror of
https://github.com/janishutz/fundamentals-of-webengineering.git
synced 2025-11-25 05:44:24 +00:00
Task 3: File Selector
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
import "./css/App.css";
|
import "./css/App.css";
|
||||||
import '@fortawesome/fontawesome-free/css/all.css';
|
import '@fortawesome/fontawesome-free/css/all.css';
|
||||||
import { readCSV } from './csv';
|
import { CSV_Data, fileInfo, responseObject } from './types';
|
||||||
import { CSV_Data, fileInfo } from './types';
|
import { readCSV, convertCSVtoJSON } from './csv';
|
||||||
|
|
||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import Layout from "./Layout";
|
import Layout from "./components/Layout";
|
||||||
import CSVCard from "./components/CSVCard";
|
import CSVCard from "./components/CSVCard";
|
||||||
import InfoCard from "./components/InfoCard";
|
import InfoCard from "./components/InfoCard";
|
||||||
import DataTable from "./components/DataTable";
|
import DataTable from "./components/DataTable";
|
||||||
|
import FileCard from "./components/FileCard";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [data, setData] = useState([] as CSV_Data);
|
const [data, setData] = useState([] as CSV_Data);
|
||||||
@@ -18,10 +19,19 @@ function App() {
|
|||||||
rowcount: 0
|
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);
|
const formRef = useRef(null);
|
||||||
|
|
||||||
// This is triggered in CSVCard
|
// This is triggered in CSVCard
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) throw new Error("No file received");
|
if (!file) throw new Error("No file received");
|
||||||
|
|
||||||
@@ -29,7 +39,7 @@ function App() {
|
|||||||
|
|
||||||
const newFileInfo: fileInfo = {
|
const newFileInfo: fileInfo = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
filetype: file.type,
|
filetype: ".csv", // file.type delivers weird name
|
||||||
filesize: String(file.size) + "B",
|
filesize: String(file.size) + "B",
|
||||||
rowcount: data.length
|
rowcount: data.length
|
||||||
}
|
}
|
||||||
@@ -39,18 +49,31 @@ function App() {
|
|||||||
if (formRef.current) {
|
if (formRef.current) {
|
||||||
// Upload to server
|
// Upload to server
|
||||||
const formData = new FormData(formRef.current);
|
const formData = new FormData(formRef.current);
|
||||||
const res = await fetch("/upload", {
|
await fetch("/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData
|
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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<CSVCard handleChange={handleFileChange} formRef={formRef}></CSVCard>
|
<CSVCard handleChange={handleFileUpload} formRef={formRef}></CSVCard>
|
||||||
|
<FileCard fileList={fileList as responseObject} fileChangeHandle={handleFileChange}></FileCard>
|
||||||
<InfoCard info={info}></InfoCard>
|
<InfoCard info={info}></InfoCard>
|
||||||
<DataTable data={data}></DataTable>
|
<DataTable data={data}></DataTable>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const CSVCard = (props: {
|
|||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h2>Select CSV data</h2>
|
<h2>Upload CSV data</h2>
|
||||||
</header>
|
</header>
|
||||||
<form ref={props.formRef} action="/upload" method="post" encType="multipart/form-data" >
|
<form ref={props.formRef} action="/upload" method="post" encType="multipart/form-data" >
|
||||||
<label htmlFor="file-input" className="custom-file-upload">
|
<label htmlFor="file-input" className="custom-file-upload">
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const DataTable = (props: {data: CSV_Data}) => {
|
|||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
{
|
{
|
||||||
header.map( (col) => (
|
header.map( (col) => (
|
||||||
<Row col={col} content={row[col] as String}></Row>
|
<Row key={i} col={col} content={row[col] as String}></Row>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -84,7 +84,7 @@ const ColHeader = (props: {col: String, sortingHandle: (s: String) => void, isSe
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row = (props: {col: String, content: String}) => {
|
const Row = (props: {col: String, content: String, key: Key}) => {
|
||||||
return <td key={props.col as Key}>{props.content}</td>;
|
return <td key={props.col as Key}>{props.content}</td>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
task_3_react/src/client/components/FileCard.tsx
Normal file
71
task_3_react/src/client/components/FileCard.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { responseObject } from "../types";
|
||||||
|
|
||||||
|
const FileCard = (props: {
|
||||||
|
fileList: responseObject,
|
||||||
|
fileChangeHandle: (fileName: string) => Promise<void>
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const convert = (res: responseObject) => {
|
||||||
|
let list = [];
|
||||||
|
for (let i = 0; i < res.names.length; i++) {
|
||||||
|
const elem = {
|
||||||
|
filename: res.names[i],
|
||||||
|
uploadTime: res.uploadTimes[i]
|
||||||
|
}
|
||||||
|
list.push(elem);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = props.fileList != null ? convert(props.fileList) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="wide">
|
||||||
|
<header>
|
||||||
|
<h2>Select a File</h2>
|
||||||
|
</header>
|
||||||
|
<table id="table-content">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Filename</th>
|
||||||
|
<th>Upload Time</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
list ? list.map( (file, i) => (
|
||||||
|
<FileRow key={i} filename={file.filename!} uploadTime={file.uploadTime!} fileChangeHandle={props.fileChangeHandle}></FileRow>
|
||||||
|
)) : <tr></tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileRow = (props: {
|
||||||
|
filename: string,
|
||||||
|
uploadTime: string,
|
||||||
|
fileChangeHandle: (fileName: string) => Promise<void>
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const remFile = async () => {
|
||||||
|
await fetch(`/delete/${props.filename}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{props.filename}</td>
|
||||||
|
<td>{props.uploadTime}</td>
|
||||||
|
<td>
|
||||||
|
<div className="action-icons">
|
||||||
|
<i onClick={() => { remFile()} } className="fa-solid fa-trash-can"></i>
|
||||||
|
<i onClick={() => {props.fileChangeHandle(props.filename)}} className="fa-solid fa-file-arrow-down"></i>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileCard;
|
||||||
@@ -14,6 +14,7 @@ const InfoCard = (props: {
|
|||||||
<h2>Data infos</h2>
|
<h2>Data infos</h2>
|
||||||
</header>
|
</header>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
|
{noFileMessage}
|
||||||
<h4>Filename</h4>
|
<h4>Filename</h4>
|
||||||
<p>{props.info.filename}</p>
|
<p>{props.info.filename}</p>
|
||||||
|
|
||||||
@@ -26,7 +27,6 @@ const InfoCard = (props: {
|
|||||||
<h4>Number of rows</h4>
|
<h4>Number of rows</h4>
|
||||||
<p>{props.info.rowcount}</p>
|
<p>{props.info.rowcount}</p>
|
||||||
</div>
|
</div>
|
||||||
{noFileMessage}
|
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import "./css/Layout.css";
|
import "../css/Layout.css";
|
||||||
|
|
||||||
const Layout = (props: { children: React.ReactNode }) => {
|
const Layout = (props: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +36,19 @@ article {
|
|||||||
width: 400px;
|
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 {
|
body>main {
|
||||||
max-height: calc(100vh - 100px);
|
max-height: calc(100vh - 100px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CSV_Data } from './types';
|
import { CSV_Data } from './types';
|
||||||
import { csv2json } from 'json-2-csv';
|
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.
|
// 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.
|
// 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,
|
// If you claim it isn't good typing, it's the same as expecting it to guess the typing,
|
||||||
|
|||||||
6
task_3_react/src/client/types.d.ts
vendored
6
task_3_react/src/client/types.d.ts
vendored
@@ -9,3 +9,9 @@ export type fileInfo = {
|
|||||||
filesize: string;
|
filesize: string;
|
||||||
rowcount: number;
|
rowcount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileCard receives this via props
|
||||||
|
export type responseObject = {
|
||||||
|
names: string[],
|
||||||
|
uploadTimes: string[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import ViteExpress from "vite-express";
|
import ViteExpress from "vite-express";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import { responseObject } from "./types";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
// Set up file storage
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: "./src/server/uploads",
|
destination: "./src/server/uploads",
|
||||||
filename: (_req, file, cb) => {
|
filename: (_req, file, cb) => {
|
||||||
// Suggested in Multer's readme
|
// Suggested in Multer's readme
|
||||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1E9);
|
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1E3);
|
||||||
cb(null, file.fieldname + "-" + uniqueSuffix)
|
cb(null, file.fieldname + "-" + uniqueSuffix);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const upload = multer({ storage: storage });
|
|
||||||
|
|
||||||
|
// CSV file upload endpoint
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
app.post(
|
app.post(
|
||||||
"/upload",
|
"/upload",
|
||||||
upload.single("dataFile"),
|
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
|
// example route which returns a message
|
||||||
app.get("/hello", async function (_req, res) {
|
app.get("/hello", async function (_req, res) {
|
||||||
|
|||||||
4
task_3_react/src/server/types.d.ts
vendored
Normal file
4
task_3_react/src/server/types.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type responseObject = {
|
||||||
|
names: string[],
|
||||||
|
uploadTimes: string[]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user