Compare commits

..

10 Commits

Author SHA1 Message Date
88127c6107 [Task 3] Finish up 2025-11-17 11:06:54 +01:00
fd8ec668b4 [Task 3] Fixes, formatting, layout updates 2025-11-17 10:38:31 +01:00
157603d3d7 [Task 3] More fixes 2025-11-17 09:07:47 +01:00
5916cce77f [Task 3] Fix sse crash 2025-11-17 08:14:54 +01:00
RobinB27
5525c4b4ad [Task 3] Loading Spinner 2025-11-17 08:04:46 +01:00
RobinB27
7e336dfef0 [Task 3] remove data 2025-11-16 21:18:26 +01:00
RobinB27
6fd4db5520 [Task 3] SSE complete 2025-11-16 21:17:12 +01:00
2d8ac9a33e [Task 3] Fix SSE 2025-11-16 20:07:47 +01:00
0429e3057f [Task 3] SSE 2025-11-16 20:00:33 +01:00
13cc143888 [Task 3] Format 2025-11-16 19:25:55 +01:00
16 changed files with 679 additions and 312 deletions

View File

@@ -30,6 +30,6 @@
"@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-react": "^4.3.2",
"nodemon": "^3.1.7", "nodemon": "^3.1.7",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"vite": "^5.4.9" "vite": "^7.2.2"
} }
} }

View File

@@ -1,77 +1,142 @@
import "./css/App.css"; import './css/App.css';
import '@fortawesome/fontawesome-free/css/all.css'; import '@fortawesome/fontawesome-free/css/all.css';
import { CSV_Data, fileInfo, responseObject } from './types'; import './sse';
import { readCSV, convertCSVtoJSON } from './csv'; import {
CSV_Data, fileInfo, responseObject
import React, { useState, useRef, useEffect } from "react"; } from './types';
import Layout from "./components/Layout"; import React, {
import CSVCard from "./components/CSVCard"; useEffect,
import InfoCard from "./components/InfoCard"; useRef, useState
import DataTable from "./components/DataTable"; } from 'react';
import FileCard from "./components/FileCard"; 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 () { function App () {
const [data, setData] = useState([] as CSV_Data); const [
const [info, setInfo] = useState({ data,
filename: "None", setData
filetype: "None", ] = useState( [] as CSV_Data );
filesize: "None", const [
rowcount: 0 info,
setInfo
] = useState( {
'filename': 'None',
'filetype': 'None',
'filesize': 'None',
'rowcount': 0
} ); } );
const [
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 [fileList, setFileList] = useState(null as responseObject | null);
// Effect has to be in top level of the component
useEffect( () => { useEffect( () => {
fetch("/status", { method: "GET" }) if ( !active ) {
.then((response) => response.json()) document.addEventListener( 'sse:uploaded', () => {
.then((response) => setFileList(response)) fetch( '/status', {
.catch((error) => console.log(error)); 'method': 'GET'
} )
.then( response => response.json() )
.then( response => setFileList( response ) )
.catch( error => console.log( error ) );
} );
document.addEventListener( 'sse:deleted', () => {
fetch( '/status', {
'method': 'GET'
} )
.then( response => response.json() )
.then( response => setFileList( response ) )
.catch( error => console.log( error ) );
} );
setActive( true );
// Initial fetch of file list at first component render
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 handleFileUpload = async ( e: React.ChangeEvent<HTMLInputElement> ): Promise<void> => { const handleFileUpload = async ( e: React.ChangeEvent<HTMLInputElement> ): Promise<void> => {
setLoading( true );
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' );
const data = await readCSV( e ); const data = await readCSV( e );
const newFileInfo: fileInfo = { const newFileInfo: fileInfo = {
filename: file.name, 'filename': file.name,
filetype: ".csv", // file.type delivers weird name 'filetype': '.csv', // file.type delivers weird name
filesize: String(file.size) + "B", 'filesize': String( file.size ) + 'B',
rowcount: data.length 'rowcount': data.length
} };
setInfo( newFileInfo ); setInfo( newFileInfo );
setData( data ); setData( data );
setLoading( false );
if ( formRef.current ) { if ( formRef.current ) {
// Upload to server // Upload to server
const formData = new FormData( formRef.current ); const formData = new FormData( formRef.current );
await fetch("/upload", {
method: "POST", await fetch( '/upload?fname=' + file.name, {
body: formData 'method': 'POST',
'body': formData
} ); } );
} }
} };
const handleFileChange = async ( fileName: string ) => { const handleFileChange = async ( fileName: string ) => {
setLoading( true );
const response = await fetch( `/download/${ fileName }` ); const response = await fetch( `/download/${ fileName }` );
const blob = await response.blob(); const blob = await response.blob();
const text = await blob.text(); 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 ); 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 ); setData( data );
} setLoading( false );
};
return ( return (
<Layout> <Layout>
<div className={'loading-spinner' + ( loading ? ' active' : '' )}>
<div aria-busy="true" >
Loading...
</div>
</div>
<CSVCard handleChange={handleFileUpload} formRef={formRef}></CSVCard> <CSVCard handleChange={handleFileUpload} formRef={formRef}></CSVCard>
<FileCard fileList={fileList as responseObject} fileChangeHandle={handleFileChange}></FileCard> <FileCard fileList={fileList as responseObject} fileChangeHandle={handleFileChange}></FileCard>
<InfoCard info={info}></InfoCard> <InfoCard info={info}></InfoCard>

View File

@@ -1,9 +1,9 @@
import React from "react"; import '../css/Layout.css';
import "../css/Layout.css"; import React from 'react';
const CSVCard = ( props: { const CSVCard = ( props: {
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void, 'handleChange': ( e: React.ChangeEvent<HTMLInputElement> ) => void,
formRef: React.RefObject<HTMLFormElement> 'formRef': React.RefObject<HTMLFormElement>
} ) => { } ) => {
return ( return (
<article> <article>
@@ -14,11 +14,16 @@ const CSVCard = (props: {
<label htmlFor="file-input" className="custom-file-upload"> <label htmlFor="file-input" className="custom-file-upload">
<i className="fa fa-file-csv"></i> Select CSV file to explore <i className="fa fa-file-csv"></i> Select CSV file to explore
</label> </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> <small>Please upload a CSV file, where the first row is the header.</small>
</form> </form>
</article> </article>
); );
} };
export default CSVCard; export default CSVCard;

View File

@@ -1,37 +1,57 @@
import { Key, SetStateAction, useState } from "react"; import {
import { CSV_Data } from "../types"; 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 <></>; if ( props.data.length == 0 ) return <></>;
const header = Object.keys( props.data[0]! ); const header = Object.keys( props.data[0]! );
const [sortCol, setSortCol] = useState("None"); const [
const [sortType, setSortType] = useState("asc"); sortCol,
setSortCol
] = useState( 'None' );
const [
sortType,
setSortType
] = useState( 'asc' );
const sortingHandler = (col: String) => { const sortingHandler = ( col: string ) => {
if ( sortCol !== col ) { if ( sortCol !== col ) {
setSortCol( col as SetStateAction<string> ); setSortCol( col as SetStateAction<string> );
setSortType("asc"); setSortType( 'asc' );
} else if (sortType === "asc") { } else if ( sortType === 'asc' ) {
setSortType("desc"); setSortType( 'desc' );
} else { } else {
setSortCol("None"); setSortCol( 'None' );
setSortType("None"); setSortType( 'None' );
}
} }
};
if (sortCol !== "None" && sortType === "asc") {
if ( sortCol !== 'None' && sortType === 'asc' ) {
props.data.sort( ( a, b ) => { props.data.sort( ( a, b ) => {
if ( a[sortCol]! < b[sortCol]! ) return -1; if ( a[sortCol]! < b[sortCol]! ) return -1;
if ( a[sortCol]! > b[sortCol]! ) return 1; if ( a[sortCol]! > b[sortCol]! ) return 1;
return 0; return 0;
} ); } );
} else if (sortCol !== "None" && sortType === "desc") { } else if ( sortCol !== 'None' && sortType === 'desc' ) {
props.data.sort( ( a, b ) => { props.data.sort( ( a, b ) => {
if ( a[sortCol]! > b[sortCol]! ) return -1; if ( a[sortCol]! > b[sortCol]! ) return -1;
if ( a[sortCol]! < b[sortCol]! ) return 1; if ( a[sortCol]! < b[sortCol]! ) return 1;
return 0; return 0;
} ); } );
} else {
props.data.sort();
} }
return ( return (
@@ -44,9 +64,12 @@ const DataTable = (props: {data: CSV_Data}) => {
<thead> <thead>
<tr> <tr>
{ {
header.map( (col) => ( header.map( ( col, i ) => <ColHeader
<ColHeader col={col} sortingHandle={sortingHandler} isSelected={col == sortCol} sortType={sortType}></ColHeader> col={col}
)) key={i}
sortingHandle={sortingHandler}
isSelected={col == sortCol}
sortType={sortType}></ColHeader> )
} }
</tr> </tr>
</thead> </thead>
@@ -55,9 +78,8 @@ const DataTable = (props: {data: CSV_Data}) => {
props.data.map( ( row, i ) => ( props.data.map( ( row, i ) => (
<tr key={i}> <tr key={i}>
{ {
header.map( (col) => ( header.map( ( col, j ) => <Row key={`${ i }:${ j }`}
<Row key={i} col={col} content={row[col] as String}></Row> col={col} content={row[col] as string}></Row> )
))
} }
</tr> </tr>
) ) ) )
@@ -66,26 +88,37 @@ const DataTable = (props: {data: CSV_Data}) => {
</table> </table>
</div> </div>
</article> </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 ( return (
<th <th
className={ className={
props.isSelected props.isSelected
? (props.sortType === "asc" ? "active sorting asc" : "active sorting desc") ? ( props.sortType === 'asc' ? 'active sorting asc' : 'active sorting desc' )
: "sortable" : 'sortable'
} }
onClick={() => {props.sortingHandle!(props.col)}} onClick={() => {
props.sortingHandle!( props.col );
}}
key={props.col as Key}> key={props.col as Key}>
{props.col} {props.col}
</th> </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>; return <td key={props.col as Key}>{props.content}</td>;
} };
export default DataTable; export default DataTable;

View File

@@ -1,21 +1,25 @@
import { responseObject } from "../types"; import {
responseObject
} from '../types';
const FileCard = ( props: { const FileCard = ( props: {
fileList: responseObject, 'fileList': responseObject,
fileChangeHandle: (fileName: string) => Promise<void> 'fileChangeHandle': ( fileName: string ) => Promise<void>
} ) => { } ) => {
const convert = ( res: responseObject ) => { const convert = ( res: responseObject ) => {
let list = []; const list = [];
for ( let i = 0; i < res.names.length; i++ ) { for ( let i = 0; i < res.names.length; i++ ) {
const elem = { const elem = {
filename: res.names[i], 'filename': res.names[i],
uploadTime: res.uploadTimes[i] 'uploadTime': res.uploadTimes[i]
} };
list.push( elem ); list.push( elem );
} }
return list; return list;
} };
const list = props.fileList != null ? convert( props.fileList ) : null; const list = props.fileList != null ? convert( props.fileList ) : null;
@@ -24,6 +28,7 @@ const FileCard = (props: {
<header> <header>
<h2>Select a File</h2> <h2>Select a File</h2>
</header> </header>
<div className="table-scroll-wrapper">
<table id="table-content"> <table id="table-content">
<thead> <thead>
<tr> <tr>
@@ -34,25 +39,29 @@ const FileCard = (props: {
</thead> </thead>
<tbody> <tbody>
{ {
list ? list.map( (file, i) => ( list ? list.map( ( file, i ) => <FileRow
<FileRow key={i} filename={file.filename!} uploadTime={file.uploadTime!} fileChangeHandle={props.fileChangeHandle}></FileRow> key={i}
)) : <tr></tr> filename={file.filename!}
uploadTime={file.uploadTime!}
fileChangeHandle={props.fileChangeHandle}/> ) : <tr></tr>
} }
</tbody> </tbody>
</table> </table>
</div>
</article> </article>
); );
} };
const FileRow = ( props: { const FileRow = ( props: {
filename: string, 'filename': string,
uploadTime: string, 'uploadTime': string,
fileChangeHandle: (fileName: string) => Promise<void> 'fileChangeHandle': ( fileName: string ) => Promise<void>
} ) => { } ) => {
const rmFile = async () => {
const remFile = async () => { await fetch( `/delete/${ props.filename }`, {
await fetch(`/delete/${props.filename}`, { method: "DELETE" }); 'method': 'DELETE'
} } );
};
return ( return (
<tr> <tr>
@@ -60,12 +69,16 @@ const FileRow = (props: {
<td>{props.uploadTime}</td> <td>{props.uploadTime}</td>
<td> <td>
<div className="action-icons"> <div className="action-icons">
<i onClick={() => { remFile()} } className="fa-solid fa-trash-can"></i> <i onClick={() => {
<i onClick={() => {props.fileChangeHandle(props.filename)}} className="fa-solid fa-file-arrow-down"></i> rmFile();
} } className="fa-solid fa-trash-can"></i>
<i onClick={() => {
props.fileChangeHandle( props.filename );
}} className="fa-solid fa-file-arrow-down"></i>
</div> </div>
</td> </td>
</tr> </tr>
) );
} };
export default FileCard; export default FileCard;

View File

@@ -1,20 +1,25 @@
import { fileInfo } from "../types"; import {
fileInfo
} from '../types';
const InfoCard = ( props: { const InfoCard = ( props: {
info: fileInfo 'info': fileInfo
} ) => { } ) => {
let noFileMessage = <div></div>
if (props.info.filename === "None")
noFileMessage = <div id="data-info-placeholder">No file selected</div>;
return ( return (
<article> <article>
<header> <header>
<h2>Data infos</h2> <h2>Data infos</h2>
<InfoRenderer info={props.info}></InfoRenderer>
</header> </header>
<div className="info"> </article>
{noFileMessage} );
};
const InfoRenderer = ( props: {
'info': fileInfo
} ) => {
if ( props.info.filename !== 'None' ) {
return <div className="info">
<h4>Filename</h4> <h4>Filename</h4>
<p>{props.info.filename}</p> <p>{props.info.filename}</p>
@@ -26,9 +31,13 @@ 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>;
</article> } else {
); return <div className="info">
<p>No file selected</p>
</div>;
} }
};
export default InfoCard; export default InfoCard;

View File

@@ -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 ( return (
<> <>
<nav className="container-fluid"> <nav className="container-fluid">

View File

@@ -1,6 +1,6 @@
/*some style for app component*/ /*some style for app component*/
:root { :root {
--spacing: 0.25rem; --spacing: 0.5rem;
--border-color: #a0a0a0; --border-color: #a0a0a0;
} }
@@ -37,7 +37,11 @@ article {
} }
article.wide { article.wide {
width: 800px width: 800px;
.table-scroll-wrapper {
max-height: 40vh;
}
} }
.action-icons { .action-icons {
@@ -45,8 +49,44 @@ article.wide {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
font-size: 18px; font-size: 1rem;
cursor: pointer; 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 { body>main {

View File

@@ -21,6 +21,7 @@ body main {
& article { & article {
margin: 0; margin: 0;
padding: var(--spacing); padding: var(--spacing);
&>header { &>header {
margin: calc(var(--spacing) * -1) calc(var(--spacing) * -1) var(--spacing); margin: calc(var(--spacing) * -1) calc(var(--spacing) * -1) var(--spacing);
padding: var(--spacing); padding: var(--spacing);

View File

@@ -1,5 +1,9 @@
import { CSV_Data } from './types'; import {
import { csv2json } from 'json-2-csv'; CSV_Data
} from './types';
import {
csv2json
} from 'json-2-csv';
export 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.

View File

@@ -8,13 +8,16 @@ dl {
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: 20px;
} }
dt, dt,
dd { dd {
line-height: 1.42857143; line-height: 1.42857143;
} }
dt { dt {
font-weight: 700; font-weight: 700;
} }
dd { dd {
margin-left: 0; margin-left: 0;
} }

View File

@@ -1,13 +1,10 @@
import React from "react"; import '@fortawesome/fontawesome-free/css/all.css';
import ReactDOM from "react-dom/client"; import './index.css';
import "@fortawesome/fontawesome-free/css/all.css"; import '@picocss/pico/css/pico.min.css';
import App from "./App"; import App from './App';
import "@picocss/pico/css/pico.min.css"; 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 /> <App />
</React.StrictMode>, </React.StrictMode>, );
);

View File

@@ -0,0 +1,21 @@
interface SSEMessage {
'event': string,
'data': string
}
const eventSource: EventSource = new EventSource( '/sse' );
eventSource.onopen = () => {
document.dispatchEvent( new CustomEvent( 'sse:connect', {
'detail': 'success',
'cancelable': false
} ) );
};
eventSource.onmessage = event => {
const data: SSEMessage = JSON.parse( event.data );
document.dispatchEvent( new CustomEvent( 'sse:' + data.event, {
'cancelable': false,
'detail': data.data
} ) );
};

View File

@@ -1,81 +1,199 @@
import express from "express"; import * as fs from 'node:fs/promises';
import ViteExpress from "vite-express"; import {
import multer from "multer"; EventEmitter
import * as fs from "node:fs/promises"; } from 'node:stream';
import { responseObject } from "./types"; import ViteExpress from 'vite-express';
import path from "path"; import express from 'express';
import multer from 'multer';
import path from 'path';
import {
responseObject
} from './types';
const app = express(); const app = express();
const sanitizeFilePath = ( path: string ) => {
// eslint-disable-next-line no-useless-escape
return path.replace( /[\/\\].*/, '' ).replace( '..', '' );
};
// Set up file storage // 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() * 1E3); const uniqueSuffix = Date.now() + '-' + Math.round( Math.random() * 1E3 );
cb(null, file.fieldname + "-" + uniqueSuffix);
let fname = req.query['fname']
? sanitizeFilePath( String( req.query['fname'] ) )
: file.fieldname;
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 // CSV file upload endpoint
const upload = multer({ storage: storage }); const upload = multer( {
'storage': storage
} );
class FileEvent extends EventEmitter {}
const fileEvent = new FileEvent();
app.post( app.post(
"/upload", '/upload',
upload.single("dataFile"), upload.single( 'dataFile' ),
(req, res, next) => { ( _req, res ) => {
console.log(req, res, next) // 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' );
} }
); );
// Endpoint to send back file names/upload times
app.get("/status", async (_req, res) => { const zeroExtend = ( num: number ) => {
const resObject: responseObject = { if ( num < 10 ) {
names: [], return '0' + num;
uploadTimes: [] } else {
return '' + num;
}
}; };
const dir = await fs.opendir("./src/server/uploads/"); 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 = {
'names': [],
'uploadTimes': []
};
const dir = await fs.opendir( './src/server/uploads/' );
for await ( const file of dir ) { for await ( const file of dir ) {
resObject.names.push( file.name ); resObject.names.push( file.name );
const stats = await fs.stat( `./src/server/uploads/${ 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 ); res.status( 200 ).json( resObject );
}) } );
// Endpoint to send back whole files // Endpoint to send back whole files
app.get("/download/:fileName", (req, res) => { app.get( '/download/:fileName', ( req, res ) => {
const fileName = req.params.fileName; 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 => { res.sendFile( filePath, err => {
if ( err ) { if ( err ) {
console.error("Error sending file:", err); console.error( 'Error sending file:', err );
res.status(500).send("Error downloading file"); res.status( 500 ).send( 'Error downloading file' );
} }
} ); } );
}) } );
// Endpoint to remove files from server // 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 fileName = req.params.fileName;
const filePath = path.join(__dirname, "uploads", fileName); const filePath = path.join(
__dirname, 'uploads', fileName
);
try { try {
await fs.unlink(filePath); // deletes the file await fs.rm( filePath ); // deletes the file
res.status(200).send("File deleted successfully"); res.status( 200 ).send( 'File deleted successfully' );
fileEvent.emit( 'deleted', filePath );
} catch ( error ) { } catch ( error ) {
console.error("Error deleting file:", error); console.error( 'Error deleting file:', error );
res.status(500).send("Error deleting file"); 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 ) {
res.status(200).json({ message: "Hello World!" }); res.status( 200 ).json( {
'message': 'Hello World!'
} );
} );
interface SSESubscriber {
'uuid': string;
'response': express.Response;
}
interface SSESubscribers {
[id: string]: SSESubscriber | undefined;
}
const subscribers: SSESubscribers = {};
app.get( '/sse', async ( request: express.Request, response: express.Response ) => {
response.writeHead( 200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
} );
response.status( 200 );
response.flushHeaders();
response.write( `data: ${ JSON.stringify( [] ) }\n\n` );
const uuid = crypto.randomUUID();
subscribers[uuid] = {
'uuid': uuid,
'response': response
};
request.on( 'close', () => {
subscribers[ uuid ] = undefined;
} );
} );
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 );
}
}
};
fileEvent.on( 'uploaded', file => {
sendSSEData( 'uploaded', file );
} );
fileEvent.on( 'deleted', file => {
sendSSEData( 'deleted', file );
} ); } );
// Do not change below this line // Do not change below this line
ViteExpress.listen(app, 5173, () => ViteExpress.listen(
console.log("Server is listening on http://localhost:5173"), app, 5173, () => console.log( 'Server is listening on http://localhost:5173' ),
); );

View File

@@ -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