mirror of
https://github.com/janishutz/fundamentals-of-webengineering.git
synced 2025-11-25 13:54:25 +00:00
Compare commits
5 Commits
7e336dfef0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 88127c6107 | |||
| fd8ec668b4 | |||
| 157603d3d7 | |||
| 5916cce77f | |||
|
|
5525c4b4ad |
@@ -1,5 +1,6 @@
|
||||
import './css/App.css';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
import './sse';
|
||||
import {
|
||||
CSV_Data, fileInfo, responseObject
|
||||
} from './types';
|
||||
@@ -15,7 +16,6 @@ import DataTable from './components/DataTable';
|
||||
import FileCard from './components/FileCard';
|
||||
import InfoCard from './components/InfoCard';
|
||||
import Layout from './components/Layout';
|
||||
import "./sse"
|
||||
|
||||
function App () {
|
||||
const [
|
||||
@@ -35,22 +35,29 @@ function App () {
|
||||
fileList,
|
||||
setFileList
|
||||
] = useState( null as responseObject | null );
|
||||
|
||||
// For the loading spinner in DataTable
|
||||
const [
|
||||
loading,
|
||||
setLoading
|
||||
] = useState( false );
|
||||
// Add evenbt listener for server-sent events on first render
|
||||
const [active, setActive] = useState(false);
|
||||
const [
|
||||
active,
|
||||
setActive
|
||||
] = useState( false );
|
||||
|
||||
useEffect( () => {
|
||||
if ( !active ) {
|
||||
document.addEventListener("sse:uploaded", async _ev => {
|
||||
await fetch( '/status', {
|
||||
document.addEventListener( 'sse:uploaded', () => {
|
||||
fetch( '/status', {
|
||||
'method': 'GET'
|
||||
} )
|
||||
.then( response => response.json() )
|
||||
.then( response => setFileList( response ) )
|
||||
.catch( error => console.log( error ) );
|
||||
} );
|
||||
document.addEventListener("sse:deleted", async _ev => {
|
||||
await fetch( '/status', {
|
||||
document.addEventListener( 'sse:deleted', () => {
|
||||
fetch( '/status', {
|
||||
'method': 'GET'
|
||||
} )
|
||||
.then( response => response.json() )
|
||||
@@ -66,12 +73,13 @@ function App () {
|
||||
.then( response => setFileList( response ) )
|
||||
.catch( error => console.log( error ) );
|
||||
}
|
||||
})
|
||||
} );
|
||||
|
||||
const formRef = useRef( null );
|
||||
|
||||
// This is triggered in CSVCard
|
||||
const handleFileUpload = async ( e: React.ChangeEvent<HTMLInputElement> ): Promise<void> => {
|
||||
setLoading( true );
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if ( !file ) throw new Error( 'No file received' );
|
||||
@@ -86,12 +94,13 @@ function App () {
|
||||
|
||||
setInfo( newFileInfo );
|
||||
setData( data );
|
||||
setLoading( false );
|
||||
|
||||
if ( formRef.current ) {
|
||||
// Upload to server
|
||||
const formData = new FormData( formRef.current );
|
||||
|
||||
await fetch( '/upload', {
|
||||
await fetch( '/upload?fname=' + file.name, {
|
||||
'method': 'POST',
|
||||
'body': formData
|
||||
} );
|
||||
@@ -99,6 +108,8 @@ function App () {
|
||||
};
|
||||
|
||||
const handleFileChange = async ( fileName: string ) => {
|
||||
setLoading( true );
|
||||
|
||||
const response = await fetch( `/download/${ fileName }` );
|
||||
const blob = await response.blob();
|
||||
const text = await blob.text();
|
||||
@@ -107,13 +118,25 @@ function App () {
|
||||
|
||||
const data = await convertCSVtoJSON( text );
|
||||
|
||||
// Updating fileInfo requires more effort since blob doesn't have the metadata
|
||||
setInfo( {
|
||||
'filesize': blob.size + 'B',
|
||||
'filetype': response.headers.get( 'Content-Type' ) ?? 'text/csv',
|
||||
'filename': fileName,
|
||||
'rowcount': data.length
|
||||
} );
|
||||
|
||||
// Updating fileInfo requires more effort since blob doesn't have the metadata
|
||||
setData( data );
|
||||
setLoading( false );
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className={'loading-spinner' + ( loading ? ' active' : '' )}>
|
||||
<div aria-busy="true" >
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
<CSVCard handleChange={handleFileUpload} formRef={formRef}></CSVCard>
|
||||
<FileCard fileList={fileList as responseObject} fileChangeHandle={handleFileChange}></FileCard>
|
||||
<InfoCard info={info}></InfoCard>
|
||||
|
||||
@@ -5,8 +5,9 @@ import {
|
||||
CSV_Data
|
||||
} from '../types';
|
||||
|
||||
|
||||
const DataTable = ( props: {
|
||||
'data': CSV_Data
|
||||
'data': CSV_Data,
|
||||
} ) => {
|
||||
if ( props.data.length == 0 ) return <></>;
|
||||
|
||||
@@ -32,6 +33,7 @@ const DataTable = ( props: {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if ( sortCol !== 'None' && sortType === 'asc' ) {
|
||||
props.data.sort( ( a, b ) => {
|
||||
if ( a[sortCol]! < b[sortCol]! ) return -1;
|
||||
@@ -48,6 +50,8 @@ const DataTable = ( props: {
|
||||
|
||||
return 0;
|
||||
} );
|
||||
} else {
|
||||
props.data.sort();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -60,8 +64,9 @@ const DataTable = ( props: {
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
header.map( col => <ColHeader
|
||||
header.map( ( col, i ) => <ColHeader
|
||||
col={col}
|
||||
key={i}
|
||||
sortingHandle={sortingHandler}
|
||||
isSelected={col == sortCol}
|
||||
sortType={sortType}></ColHeader> )
|
||||
@@ -73,7 +78,8 @@ const DataTable = ( props: {
|
||||
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, j ) => <Row key={`${ i }:${ j }`}
|
||||
col={col} content={row[col] as string}></Row> )
|
||||
}
|
||||
</tr>
|
||||
) )
|
||||
@@ -116,4 +122,3 @@ const Row = ( props: {
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ const FileCard = ( props: {
|
||||
<header>
|
||||
<h2>Select a File</h2>
|
||||
</header>
|
||||
<div className="table-scroll-wrapper">
|
||||
<table id="table-content">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -46,6 +47,7 @@ const FileCard = ( props: {
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -80,4 +82,3 @@ const FileRow = ( props: {
|
||||
};
|
||||
|
||||
export default FileCard;
|
||||
|
||||
|
||||
@@ -5,18 +5,21 @@ import {
|
||||
const InfoCard = ( props: {
|
||||
'info': fileInfo
|
||||
} ) => {
|
||||
let noFileMessage = <div></div>;
|
||||
|
||||
if ( props.info.filename === 'None' )
|
||||
noFileMessage = <div id="data-info-placeholder">No file selected</div>;
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header>
|
||||
<h2>Data infos</h2>
|
||||
<InfoRenderer info={props.info}></InfoRenderer>
|
||||
</header>
|
||||
<div className="info">
|
||||
{noFileMessage}
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
const InfoRenderer = ( props: {
|
||||
'info': fileInfo
|
||||
} ) => {
|
||||
if ( props.info.filename !== 'None' ) {
|
||||
return <div className="info">
|
||||
<h4>Filename</h4>
|
||||
<p>{props.info.filename}</p>
|
||||
|
||||
@@ -28,10 +31,13 @@ const InfoCard = ( props: {
|
||||
|
||||
<h4>Number of rows</h4>
|
||||
<p>{props.info.rowcount}</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
</div>;
|
||||
} else {
|
||||
return <div className="info">
|
||||
<p>No file selected</p>
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
|
||||
export default InfoCard;
|
||||
|
||||
export default InfoCard;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*some style for app component*/
|
||||
:root {
|
||||
--spacing: 0.25rem;
|
||||
--spacing: 0.5rem;
|
||||
--border-color: #a0a0a0;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ article {
|
||||
}
|
||||
|
||||
article.wide {
|
||||
width: 800px
|
||||
width: 800px;
|
||||
|
||||
.table-scroll-wrapper {
|
||||
max-height: 40vh;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icons {
|
||||
@@ -45,8 +49,44 @@ article.wide {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
font-size: 18px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
|
||||
i {
|
||||
transition: color 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
color: var(--pico-primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
background-color: rgb(40, 40, 40, 0.3);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
color: white;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
transform: scale(0);
|
||||
transition: transform 0.1s linear;
|
||||
|
||||
&.active {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: rgb(40, 40, 40, 0.6);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
body>main {
|
||||
|
||||
@@ -21,6 +21,7 @@ body main {
|
||||
& article {
|
||||
margin: 0;
|
||||
padding: var(--spacing);
|
||||
|
||||
&>header {
|
||||
margin: calc(var(--spacing) * -1) calc(var(--spacing) * -1) var(--spacing);
|
||||
padding: var(--spacing);
|
||||
|
||||
@@ -8,13 +8,16 @@ dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "@fortawesome/fontawesome-free/css/all.css";
|
||||
import App from "./App";
|
||||
import "@picocss/pico/css/pico.min.css";
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
import './index.css';
|
||||
import '@picocss/pico/css/pico.min.css';
|
||||
import App from './App';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
ReactDOM.createRoot( document.getElementById( 'root' )! ).render( <React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
</React.StrictMode>, );
|
||||
|
||||
@@ -11,18 +11,39 @@ import {
|
||||
} from './types';
|
||||
|
||||
const app = express();
|
||||
|
||||
const sanitizeFilePath = ( path: string ) => {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return path.replace( /[\/\\].*/, '' ).replace( '..', '' );
|
||||
};
|
||||
|
||||
// Set up file storage
|
||||
const storage = multer.diskStorage( {
|
||||
'destination': './src/server/uploads',
|
||||
'filename': (
|
||||
_req, file, cb
|
||||
req, file, cb
|
||||
) => {
|
||||
// Suggested in Multer's readme
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round( Math.random() * 1E3 );
|
||||
|
||||
fileEvent.emit( 'uploaded', file.fieldname + '-' + uniqueSuffix );
|
||||
let fname = req.query['fname']
|
||||
? sanitizeFilePath( String( req.query['fname'] ) )
|
||||
: file.fieldname;
|
||||
|
||||
cb( null, file.fieldname + '-' + uniqueSuffix );
|
||||
const index = fname.lastIndexOf( '.' );
|
||||
|
||||
let fext = '';
|
||||
|
||||
if ( index > -1 ) {
|
||||
fname = fname.slice( 0, index );
|
||||
fext = fname.substring( index );
|
||||
}
|
||||
|
||||
fname += '-' + uniqueSuffix + fext;
|
||||
|
||||
fileEvent.emit( 'uploaded', fname );
|
||||
|
||||
cb( null, fname );
|
||||
}
|
||||
} );
|
||||
// CSV file upload endpoint
|
||||
@@ -37,15 +58,29 @@ const fileEvent = new FileEvent();
|
||||
app.post(
|
||||
'/upload',
|
||||
upload.single( 'dataFile' ),
|
||||
(
|
||||
req, res, next
|
||||
) => {
|
||||
console.log(
|
||||
req, res, next
|
||||
);
|
||||
( _req, res ) => {
|
||||
// NOTE: We do only need the next function in the handler when we want a middleware,
|
||||
// otherwise we can simply omit it
|
||||
console.log( 'Uploaded file' );
|
||||
res.send( 'Ok' );
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const zeroExtend = ( num: number ) => {
|
||||
if ( num < 10 ) {
|
||||
return '0' + num;
|
||||
} else {
|
||||
return '' + num;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = ( date: Date ) => {
|
||||
return `${ date.getFullYear() }-${ zeroExtend( date.getMonth() ) }-${ zeroExtend( date.getDay() ) }`
|
||||
+ ' at '
|
||||
+ `${ zeroExtend( date.getHours() ) }:${ zeroExtend( date.getMinutes() ) }:${ zeroExtend( date.getSeconds() ) }`;
|
||||
};
|
||||
|
||||
// Endpoint to send back file names/upload times
|
||||
app.get( '/status', async ( _req, res ) => {
|
||||
const resObject: responseObject = {
|
||||
@@ -58,7 +93,7 @@ app.get( '/status', async ( _req, res ) => {
|
||||
resObject.names.push( file.name );
|
||||
const stats = await fs.stat( `./src/server/uploads/${ file.name }` );
|
||||
|
||||
resObject.uploadTimes.push( stats.birthtime.toString() );
|
||||
resObject.uploadTimes.push( formatDate( stats.birthtime ) );
|
||||
}
|
||||
|
||||
res.status( 200 ).json( resObject );
|
||||
@@ -140,10 +175,14 @@ const sendSSEData = ( event: string, data: string ) => {
|
||||
const subs = Object.values( subscribers );
|
||||
|
||||
for ( let i = 0; i < subs.length; i++ ) {
|
||||
try {
|
||||
subs[i]!.response.write( `data: ${ JSON.stringify( {
|
||||
'event': event,
|
||||
'data': data
|
||||
} ) }\n\n` );
|
||||
} catch ( e ) {
|
||||
console.debug( e );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
6421
task_3_react/src/server/uploads/KANTON_ZUERICH_418-1763371124710-469
Normal file
6421
task_3_react/src/server/uploads/KANTON_ZUERICH_418-1763371124710-469
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
title,artist,dancingStyle
|
||||
1 Emperor's Dream,Ballroom Dance Orchestra & Marc Reift,Slow Walz
|
||||
2 Kaiserwalzer,Ballroom Dance Orchestra & Marc Reift,Wiener Walzer
|
||||
3 Macarena (Wish),Los del Mar,Flashmob
|
||||
4 Wannabe (Wish),Spice Girls,Discofox
|
||||
5 Dancing Queen (Wish),ABBA,Discofox
|
||||
6 Splish Splash,Bobby Darin,Jive/ Rocknroll
|
||||
"7 See You Later, Alligator",Bill Haley,Jive/ Rocknroll
|
||||
8 Rico Vacilon,Pedro Garcia & His Del Prado Orchestra,Cha Cha Cha
|
||||
9 El Diablo Anda Suelto,Rey Ruiz,Cha ch cha
|
||||
10 Hips Don't Lie (Wish,Shakira,Salsa/ Discofox
|
||||
11 Como Volver a Ser Feliz,Luis Enrique,Salsa
|
||||
12 Moon River,,Slow Walz
|
||||
13 Morning has broken,Cat Stevens,Wiener Walzer
|
||||
14 Jeepers creepers,Benny Goodmann,Discofox/Foxtrott
|
||||
15 ..Baby One More Time (Wish),Britney Spears,Discofox
|
||||
16 Can't Stop the Feeling! (Wish),Justin Timberlake,Discofox
|
||||
17 Tea for Two Cha Cha,Tommy Dorsey and His Orchestra,Cha ch cha
|
||||
18 Muevelo,Rey Ruiz,Cha ch cha
|
||||
19 I feel good,Ray Charles,Jive/Rocknroll
|
||||
21 Jailhouse Rock,Elvis Presley,Jive/Rocknroll
|
||||
22 Are you lonesome,Elvis Presley,Slow Walz
|
||||
23 Louenesee,Span,Walzer
|
||||
24 Something Stupid,Michael Bubl<62>,Rumba/cha cha cha
|
||||
25 Stayin' Alive (wish),Bee Gees,Discofox
|
||||
26 Uptown Funk (Wish),Bruno Mars,Discofox
|
||||
27 suelta la cintura,Ruben leon,Cha ch cha
|
||||
28 Come Dance With Me,Michael Bubl<62>,cha cha cha
|
||||
29 Kiss me,Sixpence None the Richer,Discofox/Foxtrott
|
||||
30 ily (i love you baby) [feat. Emilee],Surf Mesa,Discofox
|
||||
31 Crazy Little Thing Called Love,Queen,Jive/ Rocknroll
|
||||
32 Great Balls of Fire,Jerry Lee Lewis,Jive/ Rocknroll
|
||||
33 La Bamba,Ritchie Valens,Jive/ Rocknroll
|
||||
34 Macho,Charles Fox,Salsa
|
||||
35 The Last Waltz,Engelbert Humperdinck,Walzer
|
||||
36 An der sch<63>nen blauen Donau,New 101 Strings Orchestra,Wiener Walzer
|
||||
37 Dance The Night,Dua Lipa,Discofox
|
||||
38 Iko Iko,Justin Wellington,Discofox
|
||||
39 Beat It (Wish),Michael Jackson,Jive
|
||||
40 Super Freaky Girl (Wish),Nicki Minaj,Jive/ Rocknroll
|
||||
41 California Girls (Wish),Katy Perry,Discofox
|
||||
3 Macarena (Wish),Los del Mar,Flashmob
|
||||
43 Ay Mujer,Rey Ruiz,Cha ch cha
|
||||
44 Lets get loud,Jennifer Lopez,Cha ch cha
|
||||
45 Friday,Riton & Nightcrawlers,Party
|
||||
46 Take On Me,a-ha,Party
|
||||
47 Gimme! Gimme! Gimme! (Wish),ABBA,Party
|
||||
48 I Gotta Feeling (Wish),Black Eyed Peas,Party
|
||||
49 Angels,Robbie Williams,Slow
|
||||
50 Hangover (Wish),Taio Cruz,Party
|
||||
51 The Real Slim Shady (Wish),Eminem,Party
|
||||
52 I don't care,,Party
|
||||
53 YMCA,Village People,Party
|
||||
54 Cotton Eye Joe (Wish),Rednex,Sex & Violins
|
||||
55 Nothing compares to you,Sinhead O' Connor,Slow
|
||||
56 Komet,Udo Lindenberg & Apache 207,Partyende
|
||||
Reference in New Issue
Block a user