mirror of
https://github.com/janishutz/fundamentals-of-webengineering.git
synced 2025-11-25 05:44:24 +00:00
[Task 3] Format
This commit is contained in:
@@ -30,6 +30,6 @@
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"nodemon": "^3.1.7",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.4.9"
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
import "./css/App.css";
|
||||
import './css/App.css';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
import { CSV_Data, fileInfo, responseObject } from './types';
|
||||
import { readCSV, convertCSVtoJSON } from './csv';
|
||||
|
||||
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";
|
||||
import {
|
||||
CSV_Data, fileInfo, responseObject
|
||||
} from './types';
|
||||
import React, {
|
||||
useEffect, useRef, useState
|
||||
} from 'react';
|
||||
import {
|
||||
convertCSVtoJSON, readCSV
|
||||
} from './csv';
|
||||
import CSVCard from './components/CSVCard';
|
||||
import DataTable from './components/DataTable';
|
||||
import FileCard from './components/FileCard';
|
||||
import InfoCard from './components/InfoCard';
|
||||
import Layout from './components/Layout';
|
||||
|
||||
function App () {
|
||||
const [data, setData] = useState([] as CSV_Data);
|
||||
const [info, setInfo] = useState({
|
||||
filename: "None",
|
||||
filetype: "None",
|
||||
filesize: "None",
|
||||
rowcount: 0
|
||||
const [
|
||||
data,
|
||||
setData
|
||||
] = useState( [] as CSV_Data );
|
||||
const [
|
||||
info,
|
||||
setInfo
|
||||
] = useState( {
|
||||
'filename': 'None',
|
||||
'filetype': 'None',
|
||||
'filesize': 'None',
|
||||
'rowcount': 0
|
||||
} );
|
||||
const [
|
||||
fileList,
|
||||
setFileList
|
||||
] = useState( null as responseObject | null );
|
||||
|
||||
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));
|
||||
fetch( '/status', {
|
||||
'method': 'GET'
|
||||
} )
|
||||
.then( response => response.json() )
|
||||
.then( response => setFileList( response ) )
|
||||
.catch( error => console.log( error ) );
|
||||
} );
|
||||
|
||||
const formRef = useRef( null );
|
||||
@@ -33,42 +49,44 @@ function App() {
|
||||
// This is triggered in CSVCard
|
||||
const handleFileUpload = async ( e: React.ChangeEvent<HTMLInputElement> ): Promise<void> => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) throw new Error("No file received");
|
||||
|
||||
if ( !file ) throw new Error( 'No file received' );
|
||||
|
||||
const data = await readCSV( e );
|
||||
|
||||
const newFileInfo: fileInfo = {
|
||||
filename: file.name,
|
||||
filetype: ".csv", // file.type delivers weird name
|
||||
filesize: String(file.size) + "B",
|
||||
rowcount: data.length
|
||||
}
|
||||
'filename': file.name,
|
||||
'filetype': '.csv', // file.type delivers weird name
|
||||
'filesize': String( file.size ) + 'B',
|
||||
'rowcount': data.length
|
||||
};
|
||||
|
||||
setInfo( newFileInfo );
|
||||
setData( data );
|
||||
|
||||
if ( formRef.current ) {
|
||||
// Upload to server
|
||||
const formData = new FormData( formRef.current );
|
||||
await fetch("/upload", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
|
||||
await fetch( '/upload', {
|
||||
'method': 'POST',
|
||||
'body': formData
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
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 (
|
||||
<Layout>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import "../css/Layout.css";
|
||||
import '../css/Layout.css';
|
||||
import React from 'react';
|
||||
|
||||
const CSVCard = ( props: {
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
formRef: React.RefObject<HTMLFormElement>
|
||||
'handleChange': ( e: React.ChangeEvent<HTMLInputElement> ) => void,
|
||||
'formRef': React.RefObject<HTMLFormElement>
|
||||
} ) => {
|
||||
return (
|
||||
<article>
|
||||
@@ -14,11 +14,16 @@ const CSVCard = (props: {
|
||||
<label htmlFor="file-input" className="custom-file-upload">
|
||||
<i className="fa fa-file-csv"></i> Select CSV file to explore
|
||||
</label>
|
||||
<input id="file-input" type="file" name="dataFile" aria-describedby="fileHelp" accept="text/csv" onChange={props.handleChange}/>
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
name="dataFile"
|
||||
aria-describedby="fileHelp" accept="text/csv" onChange={props.handleChange}/>
|
||||
<small>Please upload a CSV file, where the first row is the header.</small>
|
||||
</form>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default CSVCard;
|
||||
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
import { Key, SetStateAction, useState } from "react";
|
||||
import { CSV_Data } from "../types";
|
||||
import {
|
||||
Key, SetStateAction, useState
|
||||
} from 'react';
|
||||
import {
|
||||
CSV_Data
|
||||
} from '../types';
|
||||
|
||||
const DataTable = (props: {data: CSV_Data}) => {
|
||||
const DataTable = ( props: {
|
||||
'data': CSV_Data
|
||||
} ) => {
|
||||
if ( props.data.length == 0 ) return <></>;
|
||||
|
||||
const header = Object.keys( props.data[0]! );
|
||||
const [sortCol, setSortCol] = useState("None");
|
||||
const [sortType, setSortType] = useState("asc");
|
||||
const [
|
||||
sortCol,
|
||||
setSortCol
|
||||
] = useState( 'None' );
|
||||
const [
|
||||
sortType,
|
||||
setSortType
|
||||
] = useState( 'asc' );
|
||||
|
||||
const sortingHandler = (col: String) => {
|
||||
const sortingHandler = ( col: string ) => {
|
||||
if ( sortCol !== col ) {
|
||||
setSortCol( col as SetStateAction<string> );
|
||||
setSortType("asc");
|
||||
} else if (sortType === "asc") {
|
||||
setSortType("desc");
|
||||
setSortType( 'asc' );
|
||||
} else if ( sortType === 'asc' ) {
|
||||
setSortType( 'desc' );
|
||||
} else {
|
||||
setSortCol("None");
|
||||
setSortType("None");
|
||||
}
|
||||
setSortCol( 'None' );
|
||||
setSortType( 'None' );
|
||||
}
|
||||
};
|
||||
|
||||
if (sortCol !== "None" && sortType === "asc") {
|
||||
if ( sortCol !== 'None' && sortType === 'asc' ) {
|
||||
props.data.sort( ( a, b ) => {
|
||||
if ( a[sortCol]! < b[sortCol]! ) return -1;
|
||||
|
||||
if ( a[sortCol]! > b[sortCol]! ) return 1;
|
||||
|
||||
return 0;
|
||||
} );
|
||||
} else if (sortCol !== "None" && sortType === "desc") {
|
||||
} else if ( sortCol !== 'None' && sortType === 'desc' ) {
|
||||
props.data.sort( ( a, b ) => {
|
||||
if ( a[sortCol]! > b[sortCol]! ) return -1;
|
||||
|
||||
if ( a[sortCol]! < b[sortCol]! ) return 1;
|
||||
|
||||
return 0;
|
||||
} );
|
||||
}
|
||||
@@ -44,9 +60,11 @@ const DataTable = (props: {data: CSV_Data}) => {
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
header.map( (col) => (
|
||||
<ColHeader col={col} sortingHandle={sortingHandler} isSelected={col == sortCol} sortType={sortType}></ColHeader>
|
||||
))
|
||||
header.map( col => <ColHeader
|
||||
col={col}
|
||||
sortingHandle={sortingHandler}
|
||||
isSelected={col == sortCol}
|
||||
sortType={sortType}></ColHeader> )
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -55,9 +73,7 @@ const DataTable = (props: {data: CSV_Data}) => {
|
||||
props.data.map( ( row, i ) => (
|
||||
<tr key={i}>
|
||||
{
|
||||
header.map( (col) => (
|
||||
<Row key={i} col={col} content={row[col] as String}></Row>
|
||||
))
|
||||
header.map( col => <Row key={i} col={col} content={row[col] as string}></Row> )
|
||||
}
|
||||
</tr>
|
||||
) )
|
||||
@@ -66,26 +82,38 @@ const DataTable = (props: {data: CSV_Data}) => {
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const ColHeader = (props: {col: String, sortingHandle: (s: String) => void, isSelected: boolean, sortType: String}) => {
|
||||
const ColHeader = ( props: {
|
||||
'col': string,
|
||||
'sortingHandle': ( s: string ) => void,
|
||||
'isSelected': boolean,
|
||||
'sortType': string
|
||||
} ) => {
|
||||
return (
|
||||
<th
|
||||
className={
|
||||
props.isSelected
|
||||
? (props.sortType === "asc" ? "active sorting asc" : "active sorting desc")
|
||||
: "sortable"
|
||||
? ( props.sortType === 'asc' ? 'active sorting asc' : 'active sorting desc' )
|
||||
: 'sortable'
|
||||
}
|
||||
onClick={() => {props.sortingHandle!(props.col)}}
|
||||
onClick={() => {
|
||||
props.sortingHandle!( props.col );
|
||||
}}
|
||||
key={props.col as Key}>
|
||||
{props.col}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Row = (props: {col: String, content: String, key: Key}) => {
|
||||
const Row = ( props: {
|
||||
'col': string,
|
||||
'content': string,
|
||||
'key': Key
|
||||
} ) => {
|
||||
return <td key={props.col as Key}>{props.content}</td>;
|
||||
}
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { responseObject } from "../types";
|
||||
import {
|
||||
responseObject
|
||||
} from '../types';
|
||||
|
||||
const FileCard = ( props: {
|
||||
fileList: responseObject,
|
||||
fileChangeHandle: (fileName: string) => Promise<void>
|
||||
'fileList': responseObject,
|
||||
'fileChangeHandle': ( fileName: string ) => Promise<void>
|
||||
} ) => {
|
||||
|
||||
const convert = ( res: responseObject ) => {
|
||||
let list = [];
|
||||
const list = [];
|
||||
|
||||
for ( let i = 0; i < res.names.length; i++ ) {
|
||||
const elem = {
|
||||
filename: res.names[i],
|
||||
uploadTime: res.uploadTimes[i]
|
||||
}
|
||||
'filename': res.names[i],
|
||||
'uploadTime': res.uploadTimes[i]
|
||||
};
|
||||
|
||||
list.push( elem );
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
};
|
||||
|
||||
const list = props.fileList != null ? convert( props.fileList ) : null;
|
||||
|
||||
@@ -34,25 +38,28 @@ const FileCard = (props: {
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
list ? list.map( (file, i) => (
|
||||
<FileRow key={i} filename={file.filename!} uploadTime={file.uploadTime!} fileChangeHandle={props.fileChangeHandle}></FileRow>
|
||||
)) : <tr></tr>
|
||||
list ? list.map( ( file, i ) => <FileRow
|
||||
key={i}
|
||||
filename={file.filename!}
|
||||
uploadTime={file.uploadTime!}
|
||||
fileChangeHandle={props.fileChangeHandle}/> ) : <tr></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const FileRow = ( props: {
|
||||
filename: string,
|
||||
uploadTime: string,
|
||||
fileChangeHandle: (fileName: string) => Promise<void>
|
||||
'filename': string,
|
||||
'uploadTime': string,
|
||||
'fileChangeHandle': ( fileName: string ) => Promise<void>
|
||||
} ) => {
|
||||
|
||||
const remFile = async () => {
|
||||
await fetch(`/delete/${props.filename}`, { method: "DELETE" });
|
||||
}
|
||||
const rmFile = async () => {
|
||||
await fetch( `/delete/${ props.filename }`, {
|
||||
'method': 'DELETE'
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
@@ -60,12 +67,17 @@ const FileRow = (props: {
|
||||
<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>
|
||||
<i onClick={() => {
|
||||
rmFile();
|
||||
} } 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;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { fileInfo } from "../types";
|
||||
import {
|
||||
fileInfo
|
||||
} from '../types';
|
||||
|
||||
const InfoCard = ( props: {
|
||||
info: fileInfo
|
||||
'info': fileInfo
|
||||
} ) => {
|
||||
let noFileMessage = <div></div>;
|
||||
|
||||
let noFileMessage = <div></div>
|
||||
if (props.info.filename === "None")
|
||||
if ( props.info.filename === 'None' )
|
||||
noFileMessage = <div id="data-info-placeholder">No file selected</div>;
|
||||
|
||||
return (
|
||||
@@ -29,6 +31,7 @@ const InfoCard = (props: {
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default InfoCard;
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import "../css/Layout.css";
|
||||
import '../css/Layout.css';
|
||||
import React from 'react';
|
||||
|
||||
const Layout = (props: { children: React.ReactNode }) => {
|
||||
const Layout = ( props: {
|
||||
'children': React.ReactNode
|
||||
} ) => {
|
||||
return (
|
||||
<>
|
||||
<nav className="container-fluid">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { CSV_Data } from './types';
|
||||
import { csv2json } from 'json-2-csv';
|
||||
import {
|
||||
CSV_Data
|
||||
} from './types';
|
||||
import {
|
||||
csv2json
|
||||
} from 'json-2-csv';
|
||||
|
||||
export const convertCSVtoJSON = async ( csvText: string ) => {
|
||||
// Type cast OK, as the typing of the external library is not perfect -> Actually it is.
|
||||
|
||||
@@ -1,81 +1,99 @@
|
||||
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";
|
||||
import * as fs from 'node:fs/promises';
|
||||
import ViteExpress from 'vite-express';
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import {
|
||||
responseObject
|
||||
} from './types';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Set up file storage
|
||||
const storage = multer.diskStorage( {
|
||||
destination: "./src/server/uploads",
|
||||
filename: (_req, file, cb) => {
|
||||
'destination': './src/server/uploads',
|
||||
'filename': (
|
||||
_req, file, cb
|
||||
) => {
|
||||
// Suggested in Multer's readme
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1E3);
|
||||
cb(null, file.fieldname + "-" + uniqueSuffix);
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round( Math.random() * 1E3 );
|
||||
|
||||
cb( null, file.fieldname + '-' + uniqueSuffix );
|
||||
}
|
||||
} );
|
||||
|
||||
// CSV file upload endpoint
|
||||
const upload = multer({ storage: storage });
|
||||
const upload = multer( {
|
||||
'storage': storage
|
||||
} );
|
||||
|
||||
app.post(
|
||||
"/upload",
|
||||
upload.single("dataFile"),
|
||||
(req, res, next) => {
|
||||
console.log(req, res, next)
|
||||
'/upload',
|
||||
upload.single( 'dataFile' ),
|
||||
(
|
||||
req, res, next
|
||||
) => {
|
||||
console.log(
|
||||
req, res, next
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Endpoint to send back file names/upload times
|
||||
app.get("/status", async (_req, res) => {
|
||||
app.get( '/status', async ( _req, res ) => {
|
||||
const resObject: responseObject = {
|
||||
names: [],
|
||||
uploadTimes: []
|
||||
'names': [],
|
||||
'uploadTimes': []
|
||||
};
|
||||
const dir = await fs.opendir( './src/server/uploads/' );
|
||||
|
||||
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) => {
|
||||
app.get( '/download/:fileName', ( req, res ) => {
|
||||
const fileName = req.params.fileName;
|
||||
const filePath = path.join(__dirname, "uploads", fileName); // Filepaths must be absolute
|
||||
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");
|
||||
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) => {
|
||||
app.delete( '/delete/:fileName', async ( req, res ) => {
|
||||
const fileName = req.params.fileName;
|
||||
const filePath = path.join(__dirname, "uploads", fileName);
|
||||
const filePath = path.join(
|
||||
__dirname, 'uploads', fileName
|
||||
);
|
||||
|
||||
try {
|
||||
await fs.unlink( filePath ); // deletes the file
|
||||
res.status(200).send("File deleted successfully");
|
||||
res.status( 200 ).send( 'File deleted successfully' );
|
||||
} catch ( error ) {
|
||||
console.error("Error deleting file:", error);
|
||||
res.status(500).send("Error deleting file");
|
||||
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) {
|
||||
res.status(200).json({ message: "Hello World!" });
|
||||
app.get( '/hello', async function ( _req, res ) {
|
||||
res.status( 200 ).json( {
|
||||
'message': 'Hello World!'
|
||||
} );
|
||||
} );
|
||||
|
||||
// Do not change below this line
|
||||
ViteExpress.listen(app, 5173, () =>
|
||||
console.log("Server is listening on http://localhost:5173"),
|
||||
ViteExpress.listen(
|
||||
app, 5173, () => console.log( 'Server is listening on http://localhost:5173' ),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user