Compare commits

..

12 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
RobinB27
5fa2b1f618 Task 3: File Selector 2025-11-16 18:22:03 +01:00
RobinB27
f65cf176f9 Task 3: Backend 2025-11-16 14:45:18 +01:00
20 changed files with 7213 additions and 224 deletions

View File

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

View File

@@ -1,43 +1,144 @@
import "./App.css";
import './css/App.css';
import '@fortawesome/fontawesome-free/css/all.css';
import { readCSV } from './csv';
import { CSV_Data, fileInfo } from './types';
import React, { useState } from "react";
import Layout from "./Layout";
import CSVCard from "./components/CSVCard";
import InfoCard from "./components/InfoCard";
import DataTable from "./components/DataTable";
import './sse';
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 );
// 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 );
useEffect( () => {
if ( !active ) {
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', () => {
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 );
// This is triggered in CSVCard
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
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");
if ( !file ) throw new Error( 'No file received' );
const data = await readCSV( e );
const newFileInfo: fileInfo = {
filename: file.name,
filetype: file.type,
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 );
setLoading( false );
if ( formRef.current ) {
// Upload to server
const formData = new FormData( formRef.current );
await fetch( '/upload?fname=' + file.name, {
'method': 'POST',
'body': formData
} );
}
};
const handleFileChange = async ( fileName: string ) => {
setLoading( true );
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 );
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>
<CSVCard handleChange={handleFileChange}></CSVCard>
<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>
<DataTable data={data}></DataTable>
</Layout>

View File

@@ -1,29 +0,0 @@
body h1,
body h2 {
margin-bottom: 0;
}
nav {
border-bottom: 1px solid var(--border-color);
}
body main {
max-height: calc(100vh - 100px);
overflow-y: auto;
padding: var(--spacing) !important;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: start;
gap: var(--spacing);
& article {
margin: 0;
padding: var(--spacing);
& > header {
margin: calc(var(--spacing) * -1) calc(var(--spacing) * -1) var(--spacing);
padding: var(--spacing);
}
}
}

View File

@@ -1,21 +0,0 @@
import React from "react";
import "./Layout.css";
const Layout = (props: { children: React.ReactNode }) => {
return (
<>
<nav className="container-fluid">
<ul>
<li>
<h1>Open data explorer</h1>
</li>
</ul>
</nav>
<main className="container-fluid">
{props.children}
</main>
</>
);
};
export default Layout;

View File

@@ -1,23 +1,29 @@
import React from "react";
import "../Layout.css";
import '../css/Layout.css';
import React from 'react';
const CSVCard = ( props: {
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
'handleChange': ( e: React.ChangeEvent<HTMLInputElement> ) => void,
'formRef': React.RefObject<HTMLFormElement>
} ) => {
return (
<article>
<header>
<h2>Select CSV data</h2>
<h2>Upload CSV data</h2>
</header>
<form>
<form ref={props.formRef} action="/upload" method="post" encType="multipart/form-data" >
<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" 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;

View File

@@ -1,47 +1,75 @@
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;
} );
} else {
props.data.sort();
}
return (
<article className="table-container">
<header>
<h2>Data table</h2>
</header>
<div className="table-scroll-wrapper">
<table id="table-content">
<thead>
<tr>
{
header.map( (col) => (
<ColHeader col={col} sortingHandle={sortingHandler} isSelected={col == sortCol} sortType={sortType}></ColHeader>
))
header.map( ( col, i ) => <ColHeader
col={col}
key={i}
sortingHandle={sortingHandler}
isSelected={col == sortCol}
sortType={sortType}></ColHeader> )
}
</tr>
</thead>
@@ -50,35 +78,47 @@ const DataTable = (props: {data: CSV_Data}) => {
props.data.map( ( row, i ) => (
<tr key={i}>
{
header.map( (col) => (
<Row 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>
) )
}
</tbody>
</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}) => {
const Row = ( props: {
'col': string,
'content': string,
'key': Key
} ) => {
return <td key={props.col as Key}>{props.content}</td>;
}
};
export default DataTable;

View File

@@ -0,0 +1,84 @@
import {
responseObject
} from '../types';
const FileCard = ( props: {
'fileList': responseObject,
'fileChangeHandle': ( fileName: string ) => Promise<void>
} ) => {
const convert = ( res: responseObject ) => {
const 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>
<div className="table-scroll-wrapper">
<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}/> ) : <tr></tr>
}
</tbody>
</table>
</div>
</article>
);
};
const FileRow = ( props: {
'filename': string,
'uploadTime': string,
'fileChangeHandle': ( fileName: string ) => Promise<void>
} ) => {
const rmFile = 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={() => {
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;

View File

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

View File

@@ -0,0 +1,23 @@
import '../css/Layout.css';
import React from 'react';
const Layout = ( props: {
'children': React.ReactNode
} ) => {
return (
<>
<nav className="container-fluid">
<ul>
<li>
<h1>Open data explorer</h1>
</li>
</ul>
</nav>
<main className="container-fluid">
{props.children}
</main>
</>
);
};
export default Layout;

View File

@@ -1,6 +1,6 @@
/*some style for app component*/
:root {
--spacing: 0.25rem;
--spacing: 0.5rem;
--border-color: #a0a0a0;
}
@@ -36,6 +36,59 @@ article {
width: 400px;
}
article.wide {
width: 800px;
.table-scroll-wrapper {
max-height: 40vh;
}
}
.action-icons {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
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 {
max-height: calc(100vh - 100px);
overflow-y: auto;

View File

@@ -0,0 +1,30 @@
body h1,
body h2 {
margin-bottom: 0;
}
nav {
border-bottom: 1px solid var(--border-color);
}
body main {
max-height: calc(100vh - 100px);
overflow-y: auto;
padding: var(--spacing) !important;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: start;
gap: var(--spacing);
& article {
margin: 0;
padding: var(--spacing);
&>header {
margin: calc(var(--spacing) * -1) calc(var(--spacing) * -1) var(--spacing);
padding: var(--spacing);
}
}
}

View File

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

View File

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

View File

@@ -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>, );

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

@@ -9,3 +9,9 @@ export type fileInfo = {
filesize: string;
rowcount: number;
}
// FileCard receives this via props
export type responseObject = {
names: string[],
uploadTimes: string[]
}

View File

@@ -1,17 +1,199 @@
import express from "express";
import ViteExpress from "vite-express";
import * as fs from 'node:fs/promises';
import {
EventEmitter
} from 'node:stream';
import ViteExpress from 'vite-express';
import express from 'express';
import multer from 'multer';
import path from 'path';
import {
responseObject
} from './types';
// creates the expres app do not change
const app = express();
// add your routes here
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
) => {
// Suggested in Multer's readme
const uniqueSuffix = Date.now() + '-' + Math.round( Math.random() * 1E3 );
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
const upload = multer( {
'storage': storage
} );
class FileEvent extends EventEmitter {}
const fileEvent = new FileEvent();
app.post(
'/upload',
upload.single( 'dataFile' ),
( _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 = {
'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( formatDate( stats.birthtime ) );
}
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.rm( filePath ); // deletes the file
res.status( 200 ).send( 'File deleted successfully' );
fileEvent.emit( 'deleted', filePath );
} 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) {
res.status(200).json({ message: "Hello World!" });
app.get( '/hello', async function ( _req, res ) {
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
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' ),
);

4
task_3_react/src/server/types.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export type responseObject = {
names: string[],
uploadTimes: string[]
}

File diff suppressed because it is too large Load Diff

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