From 7ce9f2dd93f6d9cae241916b0063b2966afb5ca9 Mon Sep 17 00:00:00 2001 From: leokontente Date: Tue, 3 Jun 2025 12:12:48 +0200 Subject: [PATCH] feat: add javascript linter format --- .prettierrc | 3 + README.md | 2 +- bun.lockb | Bin 182696 -> 263004 bytes client/script.js | 949 ++++++++++++++++++----------------- config.js | 31 +- eslint.config.mjs | 49 ++ lib/data.js | 4 +- lib/data/mongodb.js | 338 ++++++------- lib/data/redis.js | 329 ++++++------- lib/rooms.js | 263 +++++----- package.json | 8 + server.js | 1140 +++++++++++++++++++++---------------------- views/home.jade | 2 +- 13 files changed, 1561 insertions(+), 1557 deletions(-) create mode 100644 .prettierrc create mode 100644 eslint.config.mjs diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..92cde39 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} \ No newline at end of file diff --git a/README.md b/README.md index 384c89c..ca9cd62 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Web-based memos that support real-time collaboration. Inspired by scrumblr. -test Léo +Run 'bun install' for installing deps and 'redis-server --daemonize yes && bun run start' to start the servers diff --git a/bun.lockb b/bun.lockb index f520f8a547a8a8834f7cde5eaacecf4f1d691c5d..797cae2abfbd23540afba62f3c1af8abaf691201 100755 GIT binary patch literal 263004 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
  • x}3A9b(!I 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 C@uKMf delta 31307 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 diff --git a/client/script.js b/client/script.js index 7b3df3b..0abec71 100644 --- a/client/script.js +++ b/client/script.js @@ -1,56 +1,54 @@ function toggleFullScreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen() - } else { - if (document.exitFullscreen) { - document.exitFullscreen() - } + } else if (document.exitFullscreen) { + document.exitFullscreen() } } -var cards = {} -var totalcolumns = 0 -var columns = [] -var currentTheme = "bigcards" -var boardInitialized = false -var keyTrap = null +let cards = {} +let totalcolumns = 0 +let columns = [] +let currentTheme = 'bigcards' +let boardInitialized = false +let keyTrap = null -var baseurl = location.pathname.substring(0, location.pathname.lastIndexOf("/")) -var socket = io.connect({ path: baseurl + "/socket.io" }) +const baseurl = location.pathname.substring(0, location.pathname.lastIndexOf('/')) +const socket = io.connect({ path: `${baseurl}/socket.io` }) moment.locale(navigator.language || navigator.languages[0]) marked.setOptions({ sanitize: true }) -//an action has happened, send it to the -//server +// an action has happened, send it to the +// server function sendAction(a, d) { - //console.log('--> ' + a); + // console.log('--> ' + a); - var message = { + const message = { action: a, - data: d, + data: d } socket.json.send(message) } -socket.on("connect", function () { - //console.log('successful socket.io connect'); +socket.on('connect', () => { + // console.log('successful socket.io connect'); - //let the final part of the path be the room name - var room = location.pathname.substring(location.pathname.lastIndexOf("/")) + // let the final part of the path be the room name + const room = location.pathname.substring(location.pathname.lastIndexOf('/')) - //imediately join the room which will trigger the initializations - sendAction("joinRoom", room) + // imediately join the room which will trigger the initializations + sendAction('joinRoom', room) }) -socket.on("disconnect", function () { - blockUI("Serveur déconnecté. Veuillez rafraîchir la page pour essayer de vous reconnecter…") - //$('.blockOverlay').on('click', $.unblockUI); +socket.on('disconnect', () => { + blockUI('Serveur déconnecté. Veuillez rafraîchir la page pour essayer de vous reconnecter…') + // $('.blockOverlay').on('click', $.unblockUI); }) -socket.on("message", function (data) { +socket.on('message', (data) => { getMessage(data) }) @@ -59,173 +57,172 @@ function unblockUI() { } function blockUI(message) { - message = message || "En attente…" + message = message || 'En attente…' $.blockUI({ - message: message, + message, css: { - border: "none", - padding: "15px", - backgroundColor: "#000", - "-webkit-border-radius": "10px", - "-moz-border-radius": "10px", + border: 'none', + padding: '15px', + backgroundColor: '#000', + '-webkit-border-radius': '10px', + '-moz-border-radius': '10px', opacity: 0.5, - color: "#fff", - fontSize: "20px", + color: '#fff', + fontSize: '20px' }, fadeOut: 0, - fadeIn: 10, + fadeIn: 10 }) } -//respond to an action event +// respond to an action event function getMessage(m) { - var message = m //JSON.parse(m); - var action = message.action - var data = message.data + const message = m // JSON.parse(m); + const { action } = message + const { data } = message - //console.log('<-- ' + action); + // console.log('<-- ' + action); switch (action) { - case "roomAccept": - //okay we're accepted, then request initialization - //(this is a bit of unnessary back and forth but that's okay for now) - sendAction("initializeMe", null) + case 'roomAccept': + // okay we're accepted, then request initialization + // (this is a bit of unnessary back and forth but that's okay for now) + sendAction('initializeMe', null) break - case "roomDeny": - //this doesn't happen yet + case 'roomDeny': + // this doesn't happen yet break - case "moveCard": - moveCard($("#" + data.id), data.position) + case 'moveCard': + moveCard($(`#${data.id}`), data.position) break - case "initCards": + case 'initCards': initCards(data) break - case "createCard": - //console.log(data); + case 'createCard': + // console.log(data); drawNewCard(data.id, data.text, data.x, data.y, data.rot, data.colour, null) break - case "deleteCard": - $("#" + data.id).fadeOut(500, function () { + case 'deleteCard': + $(`#${data.id}`).fadeOut(500, function() { $(this).remove() }) break - case "editCard": - $("#" + data.id) - .children(".content:first") - .attr("data-text", data.value) - $("#" + data.id) - .children(".content:first") + case 'editCard': + $(`#${data.id}`) + .children('.content:first') + .attr('data-text', data.value) + $(`#${data.id}`) + .children('.content:first') .html(marked(data.value)) break - case "initColumns": + case 'initColumns': initColumns(data) break - case "updateColumns": + case 'updateColumns': initColumns(data) break - case "changeTheme": + case 'changeTheme': changeThemeTo(data) break - case "join-announce": + case 'join-announce': displayUserJoined(data.sid, data.user_name) break - case "leave-announce": + case 'leave-announce': displayUserLeft(data.sid) break - case "initialUsers": + case 'initialUsers': displayInitialUsers(data) break - case "nameChangeAnnounce": + case 'nameChangeAnnounce': updateName(message.data.sid, message.data.user_name) break - case "addSticker": + case 'addSticker': addSticker(message.data.cardId, message.data.stickerId) break - case "setBoardSize": + case 'setBoardSize': resizeBoard(message.data) break - case "export": + case 'export': download(message.data.filename, message.data.text) break - case "addRevision": + case 'addRevision': addRevision(message.data) break - case "deleteRevision": - $("#revision-" + message.data).remove() + case 'deleteRevision': + $(`#revision-${message.data}`).remove() break - case "initRevisions": - $("#revisions-list").empty() - for (var i = 0; i < message.data.length; i++) { + case 'initRevisions': + $('#revisions-list').empty() + for (let i = 0; i < message.data.length; i++) { addRevision(message.data[i]) } break default: - //unknown message - alert("action inconnue : " + JSON.stringify(message)) + // unknown message + alert(`action inconnue : ${JSON.stringify(message)}`) break } } -$(document).on("keyup", function (event) { +$(document).on('keyup', (event) => { keyTrap = event.which }) -function drawNewCard(id, text, x, y, rot, colour, sticker, animationspeed, mx = 0 , my = 0) { - //cards[id] = {id: id, text: text, x: x, y: y, rot: rot, colour: colour}; +function drawNewCard(id, text, x, y, rot, colour, sticker, animationspeed, mx = 0, my = 0) { + // cards[id] = {id: id, text: text, x: x, y: y, rot: rot, colour: colour}; - var h = - '
    \ \ -
    ' + - marked(text) + - '
    \ -
    ' +
    ${ + marked(text) +}
    \ + ` - var card = $(h) - card.appendTo("#board") - $("#" + id) - .children(".content:first") - .attr("data-text", text) + const card = $(h) + card.appendTo('#board') + $(`#${id}`) + .children('.content:first') + .attr('data-text', text) - //@TODO - //Draggable has a bug which prevents blur event - //http://bugs.jqueryui.com/ticket/4261 - //So we have to blur all the cards and editable areas when - //we click on a card - //The following doesn't work so we will do the bug - //fix recommended in the above bug report + // @TODO + // Draggable has a bug which prevents blur event + // http://bugs.jqueryui.com/ticket/4261 + // So we have to blur all the cards and editable areas when + // we click on a card + // The following doesn't work so we will do the bug + // fix recommended in the above bug report // card.on('click', function() { // $(this).focus(); // } ); @@ -234,182 +231,176 @@ function drawNewCard(id, text, x, y, rot, colour, sticker, animationspeed, mx = snap: false, snapTolerance: 5, containment: [0, 0, 2000, 2000], - stack: ".card", - start: function (event, ui) { + stack: '.card', + start(event, ui) { keyTrap = null }, - drag: function (event, ui) { + drag(event, ui) { if (keyTrap == 27) { ui.helper.css(ui.originalPosition) return false } }, - handle: "div.content", + handle: 'div.content' }) - //After a drag: - card.on("dragstop", function (event, ui) { + // After a drag: + card.on('dragstop', function(event, ui) { if (keyTrap == 27) { keyTrap = null return } - var data = { + const data = { id: this.id, position: ui.position, - oldposition: ui.originalPosition, + oldposition: ui.originalPosition } - sendAction("moveCard", data) + sendAction('moveCard', data) }) - card.children(".droppable").droppable({ - accept: ".sticker", - drop: function (event, ui) { - var stickerId = ui.draggable.attr("id") - var cardId = $(this).parent().attr("id") + card.children('.droppable').droppable({ + accept: '.sticker', + drop(event, ui) { + const stickerId = ui.draggable.attr('id') + const cardId = $(this).parent().attr('id') addSticker(cardId, stickerId) - var data = { - cardId: cardId, - stickerId: stickerId, + const data = { + cardId, + stickerId } - sendAction("addSticker", data) + sendAction('addSticker', data) - //remove hover state to everything on the board to prevent - //a jquery bug where it gets left around - $(".card-hover-draggable").removeClass("card-hover-draggable") + // remove hover state to everything on the board to prevent + // a jquery bug where it gets left around + $('.card-hover-draggable').removeClass('card-hover-draggable') }, - hoverClass: "card-hover-draggable", + hoverClass: 'card-hover-draggable' }) - var speed = Math.floor(Math.random() * 1000) - if (typeof animationspeed != "undefined") speed = animationspeed + let speed = Math.floor(Math.random() * 1000) + if (typeof animationspeed != 'undefined') speed = animationspeed if (mx == 0 && my == 0) { - var startPosition = $("#create-card").position() - mx = startPosition.left; - my = startPosition.top; + const startPosition = $('#create-card').position() + mx = startPosition.left + my = startPosition.top } - card.css("top", my) - card.css("left", mx) + card.css('top', my) + card.css('left', mx) card.animate( { - left: x + "px", - top: y + "px", + left: `${x}px`, + top: `${y}px` }, speed ) - - card.children(".delete-card-icon").on("click", function () { - $("#" + id).remove() - //notify server of delete - sendAction("deleteCard", { - id: id, - }) + card.children('.delete-card-icon').on('click', () => { + $(`#${id}`).remove() + // notify server of delete + sendAction('deleteCard', { id }) }) - card.children(".content").editable( - function (value, settings) { - $("#" + id) - .children(".content:first") - .attr("data-text", value) + card.children('.content').editable( + (value, settings) => { + $(`#${id}`) + .children('.content:first') + .attr('data-text', value) onCardChange(id, value) return marked(value) }, { - type: "textarea", - data: function () { - return $("#" + id) - .children(".content:first") - .attr("data-text") + type: 'textarea', + data() { + return $(`#${id}`) + .children('.content:first') + .attr('data-text') }, - submit: "OK", - style: "inherit", - cssclass: "card-edit-form", - placeholder: "Double cliquez pour m’éditer", - onblur: "submit", - event: "dblclick", //event: 'mouseover' + submit: 'OK', + style: 'inherit', + cssclass: 'card-edit-form', + placeholder: 'Double cliquez pour m’éditer', + onblur: 'submit', + event: 'dblclick' // event: 'mouseover' } ) - //add applicable sticker + // add applicable sticker if (sticker !== null) addSticker(id, sticker) } function onCardChange(id, text) { - sendAction("editCard", { - id: id, - value: text, + sendAction('editCard', { + id, + value: text }) } function moveCard(card, position) { card.animate( { - left: position.left + "px", - top: position.top + "px", + left: `${position.left}px`, + top: `${position.top}px` }, 500 ) } function addSticker(cardId, stickerId) { - stickerContainer = $("#" + cardId + " .filler") + stickerContainer = $(`#${cardId} .filler`) - if (stickerId === "nosticker") { - stickerContainer.html("") + if (stickerId === 'nosticker') { + stickerContainer.html('') return } if (Array.isArray(stickerId)) { - for (var i in stickerId) { - stickerContainer.prepend('') + for (const i in stickerId) { + stickerContainer.prepend(``) } - } else { - if (stickerContainer.html().indexOf(stickerId) < 0) - stickerContainer.prepend('') - } + } else if (stickerContainer.html().indexOf(stickerId) < 0) { stickerContainer.prepend(``) } } //---------------------------------- // cards //---------------------------------- -function createCard(id, text, x, y, rot, colour, mx = 0,my = 0) { +function createCard(id, text, x, y, rot, colour, mx = 0, my = 0) { drawNewCard(id, text, x, y, rot, colour, null, null, mx, my) - var action = "createCard" + const action = 'createCard' - var data = { - id: id, - text: text, - x: x, - y: y, - rot: rot, - colour: colour, + const data = { + id, + text, + x, + y, + rot, + colour } sendAction(action, data) } function randomCardColour() { - var colours = ["yellow", "green", "blue", "white"] + const colours = ['yellow', 'green', 'blue', 'white'] - var i = Math.floor(Math.random() * colours.length) + const i = Math.floor(Math.random() * colours.length) return colours[i] } function initCards(cardArray) { - //first delete any cards that exist - $(".card").remove() + // first delete any cards that exist + $('.card').remove() cards = cardArray - for (var i in cardArray) { + for (const i in cardArray) { card = cardArray[i] drawNewCard(card.id, card.text, card.x, card.y, card.rot, card.colour, card.sticker, 0) @@ -424,53 +415,53 @@ function initCards(cardArray) { //---------------------------------- function drawNewColumn(columnName) { - var cls = "col" + let cls = 'col' if (totalcolumns === 0) { - cls = "col first" + cls = 'col first' } - $("#icon-col").before( - '

    ' + - columnName + - "

    " + $('#icon-col').before( + `

    ${ + columnName + }

    ` ) - $(".editable").editable( - function (value, settings) { + $('.editable').editable( + function(value, settings) { onColumnChange(this.id, value) return value }, { - style: "inherit", - cssclass: "card-edit-form", - type: "textarea", - placeholder: "Nouveau", - onblur: "submit", - width: "", - height: "", + style: 'inherit', + cssclass: 'card-edit-form', + type: 'textarea', + placeholder: 'Nouveau', + onblur: 'submit', + width: '', + height: '', xindicator: '', - event: "dblclick", //event: 'mouseover' + event: 'dblclick' // event: 'mouseover' } ) - $(".col:last").fadeIn(500) + $('.col:last').fadeIn(500) totalcolumns++ } function onColumnChange(id, text) { - var names = Array() + const names = [] - //console.log(id + " " + text ); + // console.log(id + " " + text ); - //Get the names of all the columns right from the DOM - $(".col").each(function () { - //get ID of current column we are traversing over - var thisID = $(this).children("h2").attr("id") + // Get the names of all the columns right from the DOM + $('.col').each(function() { + // get ID of current column we are traversing over + const thisID = $(this).children('h2').attr('id') if (id == thisID) { names.push(text) @@ -485,7 +476,7 @@ function onColumnChange(id, text) { function displayRemoveColumn() { if (totalcolumns <= 0) return false - $(".col:last").fadeOut(150, function () { + $('.col:last').fadeOut(150, function() { $(this).remove() }) @@ -498,9 +489,9 @@ function createColumn(name) { drawNewColumn(name) columns.push(name) - var action = "updateColumns" + const action = 'updateColumns' - var data = columns + const data = columns sendAction(action, data) } @@ -511,9 +502,9 @@ function deleteColumn() { displayRemoveColumn() columns.pop() - var action = "updateColumns" + const action = 'updateColumns' - var data = columns + const data = columns sendAction(action, data) } @@ -521,25 +512,25 @@ function deleteColumn() { function updateColumns(c) { columns = c - var action = "updateColumns" + const action = 'updateColumns' - var data = columns + const data = columns sendAction(action, data) } function deleteColumns(next) { - //delete all existing columns: - $(".col").fadeOut("slow", next()) + // delete all existing columns: + $('.col').fadeOut('slow', next()) } function initColumns(columnArray) { totalcolumns = 0 columns = columnArray - $(".col").remove() + $('.col').remove() - for (var i in columnArray) { + for (const i in columnArray) { column = columnArray[i] drawNewColumn(column) @@ -549,33 +540,33 @@ function initColumns(columnArray) { function changeThemeTo(theme) { currentTheme = theme if (theme == 'bigcards') { - $("#board").removeClass('smallcards') + $('#board').removeClass('smallcards') } else { - $("#board").removeClass('bigcards') + $('#board').removeClass('bigcards') } - $("#board").addClass(theme) + $('#board').addClass(theme) } -////////////////////////////////////////////////////////// -////////// NAMES STUFF /////////////////////////////////// -////////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// +/// /////// NAMES STUFF /////////////////////////////////// +/// /////////////////////////////////////////////////////// function setCookie(c_name, value, exdays) { - var exdate = new Date() + const exdate = new Date() exdate.setDate(exdate.getDate() + exdays) - var c_value = escape(value) + (exdays === null ? "" : "; expires=" + exdate.toUTCString()) + ';SameSite=Strict' - document.cookie = c_name + "=" + c_value + const c_value = `${escape(value) + (exdays === null ? '' : `; expires=${exdate.toUTCString()}`)};SameSite=Strict` + document.cookie = `${c_name}=${c_value}` } function getCookie(c_name) { - var i, - x, - y, - ARRcookies = document.cookie.split(";") + let i + let x + let y + const ARRcookies = document.cookie.split(';') for (i = 0; i < ARRcookies.length; i++) { - x = ARRcookies[i].substr(0, ARRcookies[i].indexOf("=")) - y = ARRcookies[i].substr(ARRcookies[i].indexOf("=") + 1) - x = x.replace(/^\s+|\s+$/g, "") + x = ARRcookies[i].substr(0, ARRcookies[i].indexOf('=')) + y = ARRcookies[i].substr(ARRcookies[i].indexOf('=') + 1) + x = x.replace(/^\s+|\s+$/g, '') if (x == c_name) { return unescape(y) } @@ -583,74 +574,74 @@ function getCookie(c_name) { } function setName(name) { - sendAction("setUserName", name) + sendAction('setUserName', name) - setCookie("scrumscrum-username", name, 365) + setCookie('scrumscrum-username', name, 365) } function displayInitialUsers(users) { - for (var i in users) { - //console.log(users); + for (const i in users) { + // console.log(users); displayUserJoined(users[i].sid, users[i].user_name) } } function displayUserJoined(sid, user_name) { - name = "" + name = '' if (user_name) name = user_name else name = sid.substring(0, 5) - $("#names-ul").append('
  • ' + name + "
  • ") + $('#names-ul').append(`
  • ${name}
  • `) } function displayUserLeft(sid) { - name = "" + name = '' if (name) name = user_name else name = sid - var id = "#user-" + sid.toString() + const id = `#user-${sid.toString()}` - $("#names-ul") + $('#names-ul') .children(id) - .fadeOut(1000, function () { + .fadeOut(1000, function() { $(this).remove() }) } function updateName(sid, name) { - var id = "#user-" + sid.toString() + const id = `#user-${sid.toString()}` - $("#names-ul").children(id).text(name) + $('#names-ul').children(id).text(name) } -////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// function boardResizeHappened(event, ui) { - var newsize = ui.size + const newsize = ui.size - sendAction("setBoardSize", newsize) + sendAction('setBoardSize', newsize) } function resizeBoard(size) { - $(".board-outline").animate({ + $('.board-outline').animate({ height: size.height, - width: size.width, + width: size.width }) } -////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// function calcCardOffset() { - var offsets = {} - $(".card").each(function () { - var card = $(this) - $(".col").each(function (i) { - var col = $(this) - if (col.offset().left + col.outerWidth() > card.offset().left + card.outerWidth() || i === $(".col").length - 1) { - offsets[card.attr("id")] = { - col: col, - x: (card.offset().left - col.offset().left) / col.outerWidth(), + const offsets = {} + $('.card').each(function() { + const card = $(this) + $('.col').each(function(i) { + const col = $(this) + if (col.offset().left + col.outerWidth() > card.offset().left + card.outerWidth() || i === $('.col').length - 1) { + offsets[card.attr('id')] = { + col, + x: (card.offset().left - col.offset().left) / col.outerWidth() } return false } @@ -659,70 +650,70 @@ function calcCardOffset() { return offsets } -//moves cards with a resize of the Board -//doSync is false if you don't want to synchronize -//with all the other users who are in this room +// moves cards with a resize of the Board +// doSync is false if you don't want to synchronize +// with all the other users who are in this room function adjustCard(offsets, doSync) { - $(".card").each(function () { - var card = $(this) - var offset = offsets[this.id] + $('.card').each(function() { + const card = $(this) + const offset = offsets[this.id] if (offset) { - var data = { + const data = { id: this.id, position: { left: offset.col.position().left + offset.x * offset.col.outerWidth(), - top: parseInt(card.css("top").slice(0, -2)), + top: parseInt(card.css('top').slice(0, -2)) }, oldposition: { - left: parseInt(card.css("left").slice(0, -2)), - top: parseInt(card.css("top").slice(0, -2)), - }, - } //use .css() instead of .position() because css' rotate - //console.log(data); + left: parseInt(card.css('left').slice(0, -2)), + top: parseInt(card.css('top').slice(0, -2)) + } + } // use .css() instead of .position() because css' rotate + // console.log(data); if (!doSync) { - card.css("left", data.position.left) - card.css("top", data.position.top) + card.css('left', data.position.left) + card.css('top', data.position.top) } else { - //note that in this case, data.oldposition isn't accurate since - //many moves have happened since the last sync - //but that's okay becuase oldPosition isn't used right now + // note that in this case, data.oldposition isn't accurate since + // many moves have happened since the last sync + // but that's okay becuase oldPosition isn't used right now moveCard(card, data.position) - sendAction("moveCard", data) + sendAction('moveCard', data) } } }) } -////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// function download(filename, text) { - var element = document.createElement("a") - var mime = "text/plain" + const element = document.createElement('a') + let mime = 'text/plain' if (filename.match(/.csv$/)) { - mime = "text/csv" + mime = 'text/csv' } - element.setAttribute("href", "data:" + mime + ";charset=utf-8," + encodeURIComponent(text)) - element.setAttribute("download", filename) + element.setAttribute('href', `data:${mime};charset=utf-8,${encodeURIComponent(text)}`) + element.setAttribute('download', filename) - element.style.display = "none" + element.style.display = 'none' document.body.appendChild(element) element.click() document.body.removeChild(element) } function addRevision(timestamp) { - var li = $('
  • ') - var s1 = $("") - var s2 = $('delete revision') - if (typeof timestamp === "string") { + const li = $(`
  • `) + const s1 = $('') + const s2 = $('delete revision') + if (typeof timestamp === 'string') { timestamp = parseInt(timestamp) } - s1.text(moment(timestamp).format("LLLL")) + s1.text(moment(timestamp).format('LLLL')) li.append(s1) li.append(s2) - $("#revisions-list").append(li) + $('#revisions-list').append(li) // $('body').on("click", s1, function () { // socket.json.send({ @@ -738,207 +729,207 @@ function addRevision(timestamp) { // }) } -////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// +/// /////////////////////////////////////////////////////// -$(function () { - //disable image dragging - //window.ondragstart = function() { return false; }; +$(() => { + // disable image dragging + // window.ondragstart = function() { return false; }; if (boardInitialized === false) blockUI('') - //setTimeout($.unblockUI, 2000); + // setTimeout($.unblockUI, 2000); - $(".add-post-it").on("click", function (e) { - var rotation = Math.random() * 10 - 5 //add a bit of random rotation (+/- 10deg) - var cardLeft = 150 + Math.random() * 400 - var cardTop = 20 + Math.random() * 50 - var uniqueID = Math.round(Math.random() * 99999999) //is this big enough to assure uniqueness? - console.log(e.clientX, e.clientY); - createCard("card" + uniqueID, "", cardLeft, cardTop, rotation, $(this).data("color"), e.clientX, e.clientY) + $('.add-post-it').on('click', function(e) { + const rotation = Math.random() * 10 - 5 // add a bit of random rotation (+/- 10deg) + const cardLeft = 150 + Math.random() * 400 + const cardTop = 20 + Math.random() * 50 + const uniqueID = Math.round(Math.random() * 99999999) // is this big enough to assure uniqueness? + console.log(e.clientX, e.clientY) + createCard(`card${uniqueID}`, '', cardLeft, cardTop, rotation, $(this).data('color'), e.clientX, e.clientY) }) // Style changer - $("#smallify").on("click", function () { - if (currentTheme == "bigcards") { - changeThemeTo("smallcards") - } else if (currentTheme == "smallcards") { - changeThemeTo("bigcards") + $('#smallify').on('click', () => { + if (currentTheme == 'bigcards') { + changeThemeTo('smallcards') + } else if (currentTheme == 'smallcards') { + changeThemeTo('bigcards') } - sendAction("changeTheme", currentTheme) + sendAction('changeTheme', currentTheme) return false }) - $("#icon-col").on( - "hover", - function () { - $(".col-icon").fadeIn(10) + $('#icon-col').on( + 'hover', + () => { + $('.col-icon').fadeIn(10) }, - function () { - $(".col-icon").fadeOut(150) + () => { + $('.col-icon').fadeOut(150) } ) - $("#add-col").on("click", function () { - createColumn("Nouvelle colonne") + $('#add-col').on('click', () => { + createColumn('Nouvelle colonne') return false }) - $("#delete-col").on("click", function () { + $('#delete-col').on('click', () => { deleteColumn() return false }) - var user_name = getCookie("scrumscrum-username") + const user_name = getCookie('scrumscrum-username') - $("#yourname-input").on("focus", function () { - if ($(this).val() == "anonyme") { - $(this).val("") + $('#yourname-input').on('focus', function() { + if ($(this).val() == 'anonyme') { + $(this).val('') } - $(this).addClass("focused") + $(this).addClass('focused') }) - $("#yourname-input").on("blur", function () { - if ($(this).val() === "") { - $(this).val("anonyme") + $('#yourname-input').on('blur', function() { + if ($(this).val() === '') { + $(this).val('anonyme') } - $(this).removeClass("focused") + $(this).removeClass('focused') setName($(this).val()) }) - $("#yourname-input").val(user_name) - $("#yourname-input").trigger("blur") + $('#yourname-input').val(user_name) + $('#yourname-input').trigger('blur') - $("#yourname-li").hide() + $('#yourname-li').hide() - $("#yourname-input").on("keypress", function (e) { + $('#yourname-input').on('keypress', function(e) { code = e.keyCode ? e.keyCode : e.which if (code == 10 || code == 13) { - $(this).trigger("blur") + $(this).trigger('blur') } }) - $(".sticker").draggable({ + $('.sticker').draggable({ revert: true, - zIndex: 1000, + zIndex: 1000 }) - $(".board-outline").resizable({ + $('.board-outline').resizable({ ghost: false, minWidth: 640, minHeight: 480, maxWidth: 1140, - maxHeight: 855, + maxHeight: 855 }) - //A new scope for precalculating - ;(function () { - var offsets + // A new scope for precalculating + ;(function() { + let offsets - $(".board-outline").on("resizestart", function () { + $('.board-outline').on('resizestart', () => { offsets = calcCardOffset() }) - $(".board-outline").on("resize", function (event, ui) { + $('.board-outline').on('resize', (event, ui) => { adjustCard(offsets, false) }) - $(".board-outline").on("resizestop", function (event, ui) { + $('.board-outline').on('resizestop', (event, ui) => { boardResizeHappened(event, ui) adjustCard(offsets, true) }) - })() + }()) - $("#marker").draggable({ - axis: "x", - containment: "parent", + $('#marker').draggable({ + axis: 'x', + containment: 'parent' }) - $("#eraser").draggable({ - axis: "x", - containment: "parent", + $('#eraser').draggable({ + axis: 'x', + containment: 'parent' }) - $("#export-txt").on("click", function () { + $('#export-txt').on('click', () => { socket.json.send({ - action: "exportTxt", - data: $(".col").length !== 0 ? $(".col").css("width").replace("px", "") : null, + action: 'exportTxt', + data: $('.col').length !== 0 ? $('.col').css('width').replace('px', '') : null }) }) - $("#export-csv").on("click", function () { + $('#export-csv').on('click', () => { socket.json.send({ - action: "exportCsv", - data: $(".col").length !== 0 ? $(".col").css("width").replace("px", "") : null, + action: 'exportCsv', + data: $('.col').length !== 0 ? $('.col').css('width').replace('px', '') : null }) }) - $("#export-json").on("click", function () { + $('#export-json').on('click', () => { socket.json.send({ - action: "exportJson", + action: 'exportJson', data: { - width: $(".board-outline").css("width").replace("px", ""), - height: $(".board-outline").css("height").replace("px", ""), - }, + width: $('.board-outline').css('width').replace('px', ''), + height: $('.board-outline').css('height').replace('px', '') + } }) }) - $("#import-file").on("click", function (evt) { + $('#import-file').on('click', (evt) => { evt.stopPropagation() evt.preventDefault() - var f = $("#import-input").get(0).files[0] - var fr = new FileReader() - fr.onloadend = function () { - var text = fr.result + const f = $('#import-input').get(0).files[0] + const fr = new FileReader() + fr.onloadend = function() { + const text = fr.result socket.json.send({ - action: "importJson", - data: JSON.parse(text), + action: 'importJson', + data: JSON.parse(text) }) } fr.readAsText(f) }) - $("#create-revision").on("click", function () { + $('#create-revision').on('click', () => { socket.json.send({ - action: "createRevision", + action: 'createRevision', data: { - width: $(".board-outline").css("width").replace("px", ""), - height: $(".board-outline").css("height").replace("px", ""), - }, + width: $('.board-outline').css('width').replace('px', ''), + height: $('.board-outline').css('height').replace('px', '') + } }) }) }) -/** Doubleclick on mobile + Layout Framemo with tabs **/ -$(document).ready(function () { - if (window.location.href != window.location.protocol + "//" + window.location.host + "/") { +/** Doubleclick on mobile + Layout Framemo with tabs * */ +$(document).ready(() => { + if (window.location.href != `${window.location.protocol}//${window.location.host}/`) { // Not on homepage - /** Double click on mobile interface **/ + /** Double click on mobile interface * */ - var clickTimer = null - var clickTarget = null - var editTarget = null + let clickTimer = null + let clickTarget = null + let editTarget = null function doubletapCards(selector) { - $(selector + " .stickertarget").addClass("doubletap") // Escape multi bound + $(`${selector} .stickertarget`).addClass('doubletap') // Escape multi bound - $(selector + " .doubletap").on("click", function () { - clickTarget = selector.replace("#", "") + $(`${selector} .doubletap`).on('click', () => { + clickTarget = selector.replace('#', '') if (clickTimer == null) { - clickTimer = setTimeout(function () { + clickTimer = setTimeout(() => { clickTimer = null }, 1000) } else { - //console.log('doubleclick : '+clickTimer+':'+editTarget); + // console.log('doubleclick : '+clickTimer+':'+editTarget); clearTimeout(clickTimer) clickTimer = null if (editTarget == clickTarget && clickTarget !== undefined && clickTarget !== null) { - $("#" + clickTarget.replace("content:", "") + " .doubletap").trigger("dblclick") + $(`#${clickTarget.replace('content:', '')} .doubletap`).trigger('dblclick') } } @@ -947,22 +938,22 @@ $(document).ready(function () { } function doubletapTitle(selector) { - $(selector).addClass("doubletap") // Escape multi bound + $(selector).addClass('doubletap') // Escape multi bound - $(selector + ".doubletap").on("click", function () { - clickTarget = selector.replace("#", "") + $(`${selector}.doubletap`).on('click', () => { + clickTarget = selector.replace('#', '') if (clickTimer == null) { - clickTimer = setTimeout(function () { + clickTimer = setTimeout(() => { clickTimer = null }, 1000) } else { - //console.log('doubleclick : '+clickTimer+':'+editTarget); + // console.log('doubleclick : '+clickTimer+':'+editTarget); clearTimeout(clickTimer) clickTimer = null if (editTarget == clickTarget && clickTarget !== undefined && clickTarget !== null) { - $("#" + clickTarget + ".doubletap").trigger("dblclick") + $(`#${clickTarget}.doubletap`).trigger('dblclick') } } @@ -970,36 +961,36 @@ $(document).ready(function () { }) } - setInterval(function () { + setInterval(() => { // Add periodically the doubletap event on new cards - $(".stickertarget:not(.doubletap)").each(function () { - doubletapCards("#" + $(this).attr("id").replace("content:", "")) + $('.stickertarget:not(.doubletap)').each(function() { + doubletapCards(`#${$(this).attr('id').replace('content:', '')}`) }) - $("#board-table .col h2:not(.doubletap)").each(function () { - doubletapTitle("#" + $(this).attr("id")) + $('#board-table .col h2:not(.doubletap)').each(function() { + doubletapTitle(`#${$(this).attr('id')}`) }) }, 500) - /** Layout Framemo - Tabs **/ + /** Layout Framemo - Tabs * */ // Defaut board real size (not 'auto' or 'inherit') saved in database // in order to be able to center it - var boardReady = setInterval(function () { + var boardReady = setInterval(() => { if (boardInitialized) { // when board is ready - if ($(".board-outline").attr("style") === undefined) { + if ($('.board-outline').attr('style') === undefined) { // check if size is imported from db - $(".board-outline").css({ - width: $(".board-outline.ui-resizable").width() + 16 + "px", - height: "466px", + $('.board-outline').css({ + width: `${$('.board-outline.ui-resizable').width() + 16}px`, + height: '466px' }) - var data = {} + const data = {} data.size = { height: 466, - width: $(".board-outline.ui-resizable").width() + 16, + width: $('.board-outline.ui-resizable').width() + 16 } - boardResizeHappened("resizestop", data) // using scrumblr function that keep size in db after a resize + boardResizeHappened('resizestop', data) // using scrumblr function that keep size in db after a resize } clearInterval(boardReady) } @@ -1020,45 +1011,45 @@ $(document).ready(function () { // $("#about").append($("#tuto-faq, #le-logiciel, #jardin")) // Style - $("#smallify").on("click", function () { - if (currentTheme == "bigcards") { - $(this).children("i").removeClass("fa-search-plus").addClass("fa-search-minus") + $('#smallify').on('click', function() { + if (currentTheme == 'bigcards') { + $(this).children('i').removeClass('fa-search-plus').addClass('fa-search-minus') } else { - $(this).children("i").removeClass("fa-search-minus").addClass("fa-search-plus") + $(this).children('i').removeClass('fa-search-minus').addClass('fa-search-plus') } }) - $("#full-page").on("click", function () { - if ($(this).children("i").hasClass("fa-expand")) { - $(this).children("i").removeClass("fa-expand").addClass("fa-compress") - $("#header-bar").hide() + $('#full-page').on('click', function() { + if ($(this).children('i').hasClass('fa-expand')) { + $(this).children('i').removeClass('fa-expand').addClass('fa-compress') + $('#header-bar').hide() } else { - $(this).children("i").removeClass("fa-compress").addClass("fa-expand") - $("#header-bar").show() + $(this).children('i').removeClass('fa-compress').addClass('fa-expand') + $('#header-bar').show() } toggleFullScreen() }) - /** Mode iframe **/ + /** Mode iframe * */ if (top.location != self.document.location) { - $("#header-bar").hide() + $('#header-bar').hide() } // put URL in share input - var mainurl = location.toString().split('#')[0] - $(".replace-url").val(mainurl) - $(".share-iframe").text($(".share-iframe").text().replace('{{replace-url}}', mainurl)) + const mainurl = location.toString().split('#')[0] + $('.replace-url').val(mainurl) + $('.share-iframe').text($('.share-iframe').text().replace('{{replace-url}}', mainurl)) // copy URL to clipboard - $("#copyurl").on("click", function (e) { + $('#copyurl').on('click', (e) => { e.preventDefault() - var node = document.getElementById("taburl") + const node = document.getElementById('taburl') node.disabled = null node.select() - var success = document.execCommand("copy") + const success = document.execCommand('copy') if (success) { getSelection().removeAllRanges() - node.disabled = "disabled" - alert("URL du tableau copiée dans votre presse-papier !") + node.disabled = 'disabled' + alert('URL du tableau copiée dans votre presse-papier !') } else { alert( "Impossible de copier l'URL du tableau dans votre presse-papier. Veuillez copier son adresse manuellement (Ctrl+C)." @@ -1069,20 +1060,20 @@ $(document).ready(function () { }) function go() { - var value = document.forms[0].elements["name"].value - value = value.replace(/[\/\?&#]/g, "") + let { value } = document.forms[0].elements.name + value = value.replace(/[\/\?&#]/g, '') window.location.href = value return false } -$(function () { - var headerBarUrl = $("#header-bar").data("url") +$(() => { + const headerBarUrl = $('#header-bar').data('url') if (headerBarUrl) { - var getJSON = function (url, callback) { - var xhr = new XMLHttpRequest() - xhr.open("GET", url, true) - xhr.responseType = "json" - xhr.onload = function () { - var status = xhr.status + const getJSON = function(url, callback) { + const xhr = new XMLHttpRequest() + xhr.open('GET', url, true) + xhr.responseType = 'json' + xhr.onload = function() { + const { status } = xhr if (status === 200) { callback(null, xhr.response) } else { @@ -1092,72 +1083,72 @@ $(function () { xhr.send() } - getJSON(headerBarUrl, function (err, data) { + getJSON(headerBarUrl, (err, data) => { if (err !== null) { - console.log("Something went wrong: " + err) + console.log(`Something went wrong: ${err}`) } else { - document.getElementById("header-bar").innerHTML = data.markup - var styleElement = document.createElement("style") + document.getElementById('header-bar').innerHTML = data.markup + const styleElement = document.createElement('style') styleElement.innerHTML = data.style - document.getElementById("header-bar").appendChild(styleElement) + document.getElementById('header-bar').appendChild(styleElement) } }) } }) -$(function () { +$(() => { // check if hash used to show informations if (window.location.hash == '#settings' || window.location.hash == '#share') { toggleNav(window.location.hash) } // Toggle Nav on Click - $(".toggle-nav").on("click", function () { - var target = $(this).attr("href") - + $('.toggle-nav').on('click', function() { + let target = $(this).attr('href') + if (target === '#' || ($('#site-wrapper').hasClass('show-nav') && target == window.location.hash)) { target = false - history.replaceState('', '', '#'); + history.replaceState('', '', '#') } else { - history.replaceState('', '', target); + history.replaceState('', '', target) } toggleNav(target) return false }) // When nav opened, a click on the canvas hides the menu - $("body").on("click", ".show-nav #site-canvas main, .show-nav .main-header", function (e) { - history.replaceState('', '', '#'); + $('body').on('click', '.show-nav #site-canvas main, .show-nav .main-header', (e) => { + history.replaceState('', '', '#') toggleNav(false) return false }) - $(".backgrounds .bg").on("click", function () { - if ($(this).hasClass("selected")) { - $("body").css("background-image", "none") - $(this).removeClass("selected") + $('.backgrounds .bg').on('click', function() { + if ($(this).hasClass('selected')) { + $('body').css('background-image', 'none') + $(this).removeClass('selected') } else { - $(".selected").removeClass("selected") + $('.selected').removeClass('selected') $('.bgurl').val('') - $("body").css("background-image", 'url("/' + $(this).attr("src") + '")') - $(this).addClass("selected") + $('body').css('background-image', `url("/${$(this).attr('src')}")`) + $(this).addClass('selected') } }) $('.bgurl').on('change', function() { - var url = $(this).val() + const url = $(this).val() if (url) { - $(".selected").removeClass("selected") - $("body").css("background-image", 'url("' + url + '")') + $('.selected').removeClass('selected') + $('body').css('background-image', `url("${url}")`) } }) }) function toggleNav(target) { - if ($("#site-wrapper").hasClass("show-nav") && target === false) { - $("#site-wrapper").removeClass("show-nav") + if ($('#site-wrapper').hasClass('show-nav') && target === false) { + $('#site-wrapper').removeClass('show-nav') } else { - $("#share, #settings").hide() + $('#share, #settings').hide() if (target !== false) { $(target).show() } - $("#site-wrapper").addClass("show-nav") + $('#site-wrapper').addClass('show-nav') } return false } diff --git a/config.js b/config.js index 5f48f99..0837e3a 100644 --- a/config.js +++ b/config.js @@ -1,4 +1,4 @@ -/*exports.database = { +/* exports.database = { type: 'mongodb', hostname: 'localhost', port: 27017, @@ -6,7 +6,7 @@ }; */ -var argv = require("yargs").usage( +const { argv } = require('yargs').usage( 'Usage: $0 [--port INTEGER [8080]] \ [--baseurl STRING ["/"]] \ [--redis STRING:INT [127.0.0.1:6379]] \ @@ -16,28 +16,27 @@ var argv = require("yargs").usage( [--logoUrl STRING] \ [--faviconUrl STRING] \ ' - -).argv +) exports.server = { port: argv.port || 8080, - baseurl: argv.baseurl || "/", + baseurl: argv.baseurl || '/' } exports.googleanalytics = { - enabled: argv["gaEnabled"] || false, - account: argv["gaAccount"] || "UA-2069672-4", + enabled: argv.gaEnabled || false, + account: argv.gaAccount || 'UA-2069672-4' } -var redis_conf = argv.redis || "127.0.0.1:6379" +const redis_conf = argv.redis || '127.0.0.1:6379' exports.database = { - sock: argv["sock"] || false, - type: "redis", - prefix: "#scrumblr#", - host: redis_conf.split(":")[0] || "127.0.0.1", - port: redis_conf.split(":")[1] || 6379, + sock: argv.sock || false, + type: 'redis', + prefix: '#scrumblr#', + host: redis_conf.split(':')[0] || '127.0.0.1', + port: redis_conf.split(':')[1] || 6379 } -exports.headerBarUrl = argv['headerBarUrl'] || null /* example url with appropriate json markup : 'https://colibris-lemouvement.org/archipel-markup?domain=colibris-outilslibres.org' */ -exports.logoUrl = argv['logoUrl'] || null /* example logo url : 'https://postit.colibris-outilslibres.org/images/logo-Post-it.svg' */ -exports.faviconUrl = argv['faviconUrl'] || null /* example favicon url : 'https://postit.colibris-outilslibres.org/images/favicon.png' */ \ No newline at end of file +exports.headerBarUrl = argv.headerBarUrl || null /* example url with appropriate json markup : 'https://colibris-lemouvement.org/archipel-markup?domain=colibris-outilslibres.org' */ +exports.logoUrl = argv.logoUrl || null /* example logo url : 'https://postit.colibris-outilslibres.org/images/logo-Post-it.svg' */ +exports.faviconUrl = argv.faviconUrl || null /* example favicon url : 'https://postit.colibris-outilslibres.org/images/favicon.png' */ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..10806fd --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,49 @@ +import { defineConfig, globalIgnores } from 'eslint/config' +import globals from 'globals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) + +export default defineConfig([globalIgnores([ + 'node_modules', + 'client/lib' +]), { + extends: compat.extends('airbnb-base'), + + languageOptions: { ecmaVersion: 13 }, + + rules: { + semi: ['error', 'never'], + + 'max-len': ['error', { code: 104 }], + + 'vars-on-top': 'off', + 'class-methods-use-this': 'off', + 'import/no-unresolved': 'off', + 'import/extensions': ['error', 'always'], + 'import/prefer-default-export': ['off'], + 'no-use-before-define': ['off'], + eqeqeq: ['error', 'smart'], + 'comma-dangle': ['error', 'never'], + + 'object-curly-newline': ['error', { multiline: true }], + + 'func-names': ['error', 'never'], + 'space-before-function-paren': ['error', 'never'], + + 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], + + 'no-new': 'off', + 'no-restricted-syntax': 'off', + 'guard-for-in': 'off' + } +}]) diff --git a/lib/data.js b/lib/data.js index d1cc2a6..b816336 100644 --- a/lib/data.js +++ b/lib/data.js @@ -1,6 +1,6 @@ -var conf = require('../config.js').database; +const conf = require('../config.js').database -exports.db = require('./data/'+conf.type+'.js').db; +exports.db = require(`./data/${conf.type}.js`).db /* var db = function(callback) { } diff --git a/lib/data/mongodb.js b/lib/data/mongodb.js index f0503b2..10d12ed 100644 --- a/lib/data/mongodb.js +++ b/lib/data/mongodb.js @@ -1,195 +1,181 @@ -var Db = require('mongodb').Db; - Server = require('mongodb').Server, - BSON = require('mongodb').BSONNative, - conf = require('../../config.js').database; +const { Db } = require('mongodb') +Server = require('mongodb').Server, +BSON = require('mongodb').BSONNative, +conf = require('../../config.js').database -var db = function(callback) -{ - this.rooms = false; - var t = this; +const db = function(callback) { + this.rooms = false + const t = this - var db = new Db(conf.database, new Server(conf.hostname, conf.port), {native_parser:true}); - db.open(function(err, db) { - db.collection('rooms', function(err, collection) { - // make sure we have an index on name - collection.ensureIndex([['name',1]],false,function() {}); - t.rooms = collection; - }); - callback(); - }); + const db = new Db(conf.database, new Server(conf.hostname, conf.port), { native_parser: true }) + db.open((err, db) => { + db.collection('rooms', (err, collection) => { + // make sure we have an index on name + collection.ensureIndex([['name', 1]], false, () => {}) + t.rooms = collection + }) + callback() + }) } db.prototype = { - clearRoom: function(room, callback) - { - this.rooms.remove({name:room},callback); - }, + clearRoom(room, callback) { + this.rooms.remove({ name: room }, callback) + }, - // theme commands - setTheme: function(room, theme) - { - this.rooms.update( - {name:room}, - {$set:{theme:theme}} - ); - }, + // theme commands + setTheme(room, theme) { + this.rooms.update( + { name: room }, + { $set: { theme } } + ) + }, - getTheme: function(room, callback) - { - this.rooms.findOne( - {name:room}, - {theme:true}, - function(err, room) { - if(room) { - callback(room.theme); - } else { - callback(); - } - } - ); - }, + getTheme(room, callback) { + this.rooms.findOne( + { name: room }, + { theme: true }, + (err, room) => { + if (room) { + callback(room.theme) + } else { + callback() + } + } + ) + }, - // revision commands - setRevisions: function(room, revisions) { - this.rooms.update( - {name:room}, - {$set:{revisions:revisions}} - ); - }, + // revision commands + setRevisions(room, revisions) { + this.rooms.update( + { name: room }, + { $set: { revisions } } + ) + }, - getRevisions: function(room, callback) { - this.rooms.findOne( - {name:room}, - {revisions:true}, - function(err, room) { - if(room) { - callback(room.revisions); - } else { - callback(); - } - } - ); - }, + getRevisions(room, callback) { + this.rooms.findOne( + { name: room }, + { revisions: true }, + (err, room) => { + if (room) { + callback(room.revisions) + } else { + callback() + } + } + ) + }, - // Column commands - createColumn: function(room, name, callback) - { - this.rooms.update( - {name:room}, - {$push:{columns:name}}, - {upsert:true} - ,callback - ); - }, + // Column commands + createColumn(room, name, callback) { + this.rooms.update( + { name: room }, + { $push: { columns: name } }, + { upsert: true }, + callback + ) + }, - getAllColumns: function(room, callback) - { - this.rooms.findOne({name:room},{columns:true},function(err, room) { - if(room) { - callback(room.columns); - } else { - callback(); - } - }); - }, + getAllColumns(room, callback) { + this.rooms.findOne({ name: room }, { columns: true }, (err, room) => { + if (room) { + callback(room.columns) + } else { + callback() + } + }) + }, - deleteColumn: function(room) - { - this.rooms.update( - {name:room}, - {$pop:{columns:1}} - ); - }, + deleteColumn(room) { + this.rooms.update( + { name: room }, + { $pop: { columns: 1 } } + ) + }, - setColumns: function(room, columns) - { - this.rooms.update( - {name:room}, - {$set:{columns:columns}}, - {upsert:true} - ); - }, + setColumns(room, columns) { + this.rooms.update( + { name: room }, + { $set: { columns } }, + { upsert: true } + ) + }, - // Card commands - createCard: function(room, id, card) - { - var doc = {}; - doc['cards.'+id] = card; - this.rooms.update( - {name:room}, - {$set:doc}, - {upsert:true} - ); - }, + // Card commands + createCard(room, id, card) { + const doc = {} + doc[`cards.${id}`] = card + this.rooms.update( + { name: room }, + { $set: doc }, + { upsert: true } + ) + }, - getAllCards: function(room, callback) - { - this.rooms.findOne({name:room},{cards:true},function(err, room) { - if(room) { - callback(room.cards); - } else { - callback(); - } - }); - }, + getAllCards(room, callback) { + this.rooms.findOne({ name: room }, { cards: true }, (err, room) => { + if (room) { + callback(room.cards) + } else { + callback() + } + }) + }, - cardEdit: function(room, id, text) - { - var doc = {}; - doc['cards.'+id+'.text'] = text; - this.rooms.update( - {name:room}, - {$set:doc} - ); - }, + cardEdit(room, id, text) { + const doc = {} + doc[`cards.${id}.text`] = text + this.rooms.update( + { name: room }, + { $set: doc } + ) + }, - cardSetXY: function(room, id, x, y) - { - var doc = {}; - doc['cards.'+id+'.x'] = x; - doc['cards.'+id+'.y'] = y; - this.rooms.update( - {name:room}, - {$set:doc} - ); - }, + cardSetXY(room, id, x, y) { + const doc = {} + doc[`cards.${id}.x`] = x + doc[`cards.${id}.y`] = y + this.rooms.update( + { name: room }, + { $set: doc } + ) + }, - deleteCard: function(room, id) - { - var doc = {}; - doc['cards.'+id] = true; - this.rooms.update( - {name:room}, - {$unset:doc} - ); - }, + deleteCard(room, id) { + const doc = {} + doc[`cards.${id}`] = true + this.rooms.update( + { name: room }, + { $unset: doc } + ) + }, - addSticker: function(room, cardId, stickerId) - { - var doc = {}; - doc['cards.'+cardId+'.sticker'] = stickerId; - this.rooms.update( - {name:room}, - {$set:doc} - ); - }, - getBoardSize: function(room, callback) { - this.rooms.findOne( - {name:room}, - function(err, room) { - if(room) { - callback(room.size); - } else { - callback(); - } - } - ); - }, - setBoardSize: function(room, size) { - this.rooms.update( - {name:room}, - {$set:{'size':size}} - ); - } -}; -exports.db = db; + addSticker(room, cardId, stickerId) { + const doc = {} + doc[`cards.${cardId}.sticker`] = stickerId + this.rooms.update( + { name: room }, + { $set: doc } + ) + }, + getBoardSize(room, callback) { + this.rooms.findOne( + { name: room }, + (err, room) => { + if (room) { + callback(room.size) + } else { + callback() + } + } + ) + }, + setBoardSize(room, size) { + this.rooms.update( + { name: room }, + { $set: { size } } + ) + } +} +exports.db = db diff --git a/lib/data/redis.js b/lib/data/redis.js index e672da2..5fd6027 100644 --- a/lib/data/redis.js +++ b/lib/data/redis.js @@ -1,202 +1,197 @@ -var conf = require('../../config.js').database; +const conf = require('../../config.js').database -var redis = require("redis"), - redisClient = null; //redis.createClient(); +const redis = require('redis') -var async = require("async"); -var sets = require('simplesets'); +let redisClient = null // redis.createClient(); + +const async = require('async') +const sets = require('simplesets') // If you want Memory Store instead... // var MemoryStore = require('connect/middleware/session/memory'); // var session_store = new MemoryStore(); -var REDIS_PREFIX = '#scrumblr#'; +const REDIS_PREFIX = '#scrumblr#' -//For Redis Debugging +// For Redis Debugging +const db = function(callback) { + if (conf.sock) { + console.log(`Opening redis connection to socket ${conf.host}`) + redisClient = redis.createClient(conf.host) + } else { + console.log(`Opening redis connection to ${conf.host}:${conf.port}`) + redisClient = redis.createClient(conf.port, conf.host, {}) + } + redisClient.on('connect', (err) => { + callback() + }) -var db = function(callback) { - if (conf.sock) { - console.log('Opening redis connection to socket ' + conf.host); - redisClient = redis.createClient(conf.host); - } else { - console.log('Opening redis connection to ' + conf.host + ':' + conf.port); - redisClient = redis.createClient(conf.port, conf.host, {}); - } - redisClient.on("connect", function (err) { - callback(); - }); - - redisClient.on("error", function (err) { - console.log("Redis error: " + err); - }); - -}; + redisClient.on('error', (err) => { + console.log(`Redis error: ${err}`) + }) +} db.prototype = { - clearRoom: function(room, callback) { - redisClient.del(REDIS_PREFIX + '-room:/demo-cards', function (err, res) { - redisClient.del(REDIS_PREFIX + '-room:/demo-columns', function (err, res) { - callback(); - }); - }); - }, + clearRoom(room, callback) { + redisClient.del(`${REDIS_PREFIX}-room:/demo-cards`, (err, res) => { + redisClient.del(`${REDIS_PREFIX}-room:/demo-columns`, (err, res) => { + callback() + }) + }) + }, - // theme commands - setTheme: function(room, theme) { - redisClient.set(REDIS_PREFIX + '-room:' + room + '-theme', theme); - }, + // theme commands + setTheme(room, theme) { + redisClient.set(`${REDIS_PREFIX}-room:${room}-theme`, theme) + }, - getTheme: function(room, callback) { - redisClient.get(REDIS_PREFIX + '-room:' + room + '-theme', function (err, res) { - callback(res); - }); - }, + getTheme(room, callback) { + redisClient.get(`${REDIS_PREFIX}-room:${room}-theme`, (err, res) => { + callback(res) + }) + }, - // revision commands - setRevisions: function(room, revisions) { - if (Object.keys(revisions).length === 0) { - redisClient.del(REDIS_PREFIX + '-room:' + room + '-revisions'); - } else { - redisClient.set(REDIS_PREFIX + '-room:' + room + '-revisions', JSON.stringify(revisions)); - } - }, + // revision commands + setRevisions(room, revisions) { + if (Object.keys(revisions).length === 0) { + redisClient.del(`${REDIS_PREFIX}-room:${room}-revisions`) + } else { + redisClient.set(`${REDIS_PREFIX}-room:${room}-revisions`, JSON.stringify(revisions)) + } + }, - getRevisions: function(room, callback) { - redisClient.get(REDIS_PREFIX + '-room:' + room + '-revisions', function (err, res) { - callback(JSON.parse(res)); - }); - }, + getRevisions(room, callback) { + redisClient.get(`${REDIS_PREFIX}-room:${room}-revisions`, (err, res) => { + callback(JSON.parse(res)) + }) + }, - // Column commands - createColumn: function(room, name, callback) { - redisClient.rpush(REDIS_PREFIX + '-room:' + room + '-columns', name, - function (err, res) { - if (typeof callback != "undefined" && callback !== null) callback(); - } - ); - }, + // Column commands + createColumn(room, name, callback) { + redisClient.rpush( + `${REDIS_PREFIX}-room:${room}-columns`, + name, + (err, res) => { + if (typeof callback != 'undefined' && callback !== null) callback() + } + ) + }, - getAllColumns: function(room, callback) { - redisClient.lrange(REDIS_PREFIX + '-room:' + room + '-columns', 0, -1, function(err, res) { - callback(res); - }); - }, + getAllColumns(room, callback) { + redisClient.lrange(`${REDIS_PREFIX}-room:${room}-columns`, 0, -1, (err, res) => { + callback(res) + }) + }, - deleteColumn: function(room) { - redisClient.rpop(REDIS_PREFIX + '-room:' + room + '-columns'); - }, + deleteColumn(room) { + redisClient.rpop(`${REDIS_PREFIX}-room:${room}-columns`) + }, - setColumns: function(room, columns) { - //1. first delete all columns - redisClient.del(REDIS_PREFIX + '-room:' + room + '-columns', function () { - //2. now add columns for each thingy - async.forEachSeries( - columns, - function( item, callback ) { - //console.log('rpush: ' + REDIS_PREFIX + '-room:' + room + '-columns' + ' -- ' + item); - redisClient.rpush(REDIS_PREFIX + '-room:' + room + '-columns', item, - function (err, res) { - callback(); - } - ); - }, - function() { - //this happens when the series is complete - } - ); - }); - }, + setColumns(room, columns) { + // 1. first delete all columns + redisClient.del(`${REDIS_PREFIX}-room:${room}-columns`, () => { + // 2. now add columns for each thingy + async.forEachSeries( + columns, + (item, callback) => { + // console.log('rpush: ' + REDIS_PREFIX + '-room:' + room + '-columns' + ' -- ' + item); + redisClient.rpush( + `${REDIS_PREFIX}-room:${room}-columns`, + item, + (err, res) => { + callback() + } + ) + }, + () => { + // this happens when the series is complete + } + ) + }) + }, - // Card commands - createCard: function(room, id, card) { - var cardString = JSON.stringify(card); - redisClient.hset( - REDIS_PREFIX + '-room:' + room + '-cards', - id, - cardString - ); - }, + // Card commands + createCard(room, id, card) { + const cardString = JSON.stringify(card) + redisClient.hset( + `${REDIS_PREFIX}-room:${room}-cards`, + id, + cardString + ) + }, - getAllCards: function(room, callback) { - redisClient.hgetall(REDIS_PREFIX + '-room:' + room + '-cards', function (err, res) { + getAllCards(room, callback) { + redisClient.hgetall(`${REDIS_PREFIX}-room:${room}-cards`, (err, res) => { + const cards = [] - var cards = []; + for (const i in res) { + cards.push(JSON.parse(res[i])) + } + // console.dir(cards); - for (var i in res) { - cards.push( JSON.parse(res[i]) ); - } - //console.dir(cards); + callback(cards) + }) + }, - callback(cards); - }); - }, + cardEdit(room, id, text) { + redisClient.hget(`${REDIS_PREFIX}-room:${room}-cards`, id, (err, res) => { + const card = JSON.parse(res) + if (card !== null) { + card.text = text + redisClient.hset(`${REDIS_PREFIX}-room:${room}-cards`, id, JSON.stringify(card)) + } + }) + }, - cardEdit: function(room, id, text) { - redisClient.hget(REDIS_PREFIX + '-room:' + room + '-cards', id, function(err, res) { - var card = JSON.parse(res); - if (card !== null) { - card.text = text; - redisClient.hset(REDIS_PREFIX + '-room:' + room + '-cards', id, JSON.stringify(card)); - } - }); - }, + cardSetXY(room, id, x, y) { + redisClient.hget(`${REDIS_PREFIX}-room:${room}-cards`, id, (err, res) => { + const card = JSON.parse(res) + if (card !== null) { + card.x = x + card.y = y + redisClient.hset(`${REDIS_PREFIX}-room:${room}-cards`, id, JSON.stringify(card)) + } + }) + }, - cardSetXY: function(room, id, x, y) { - redisClient.hget(REDIS_PREFIX + '-room:' + room + '-cards', id, function(err, res) { - var card = JSON.parse(res); - if (card !== null) { - card.x = x; - card.y = y; - redisClient.hset(REDIS_PREFIX + '-room:' + room + '-cards', id, JSON.stringify(card)); - } - }); - }, + deleteCard(room, id) { + redisClient.hdel( + `${REDIS_PREFIX}-room:${room}-cards`, + id + ) + }, - deleteCard: function(room, id) { - redisClient.hdel( - REDIS_PREFIX + '-room:' + room + '-cards', - id - ); - }, + addSticker(room, cardId, stickerId) { + redisClient.hget(`${REDIS_PREFIX}-room:${room}-cards`, cardId, (err, res) => { + const card = JSON.parse(res) + if (card !== null) { + if (stickerId === 'nosticker') { + card.sticker = null - addSticker: function(room, cardId, stickerId) { - redisClient.hget(REDIS_PREFIX + '-room:' + room + '-cards', cardId, function(err, res) { - var card = JSON.parse(res); - if (card !== null) { - if (stickerId === "nosticker") - { - card.sticker = null; + redisClient.hset(`${REDIS_PREFIX}-room:${room}-cards`, cardId, JSON.stringify(card)) + } else { + if (card.sticker !== null) { stickerSet = new sets.Set(card.sticker) } else { stickerSet = new sets.Set() } - redisClient.hset(REDIS_PREFIX + '-room:' + room + '-cards', cardId, JSON.stringify(card)); - } - else - { - if (card.sticker !== null) - stickerSet = new sets.Set( card.sticker ); - else - stickerSet = new sets.Set(); + stickerSet.add(stickerId) - stickerSet.add(stickerId); + card.sticker = stickerSet.array() - card.sticker = stickerSet.array(); + redisClient.hset(`${REDIS_PREFIX}-room:${room}-cards`, cardId, JSON.stringify(card)) + } + } + }) + }, - redisClient.hset(REDIS_PREFIX + '-room:' + room + '-cards', cardId, JSON.stringify(card)); - } + setBoardSize(room, size) { + redisClient.set(`${REDIS_PREFIX}-room:${room}-size`, JSON.stringify(size)) + }, - } - }); - }, + getBoardSize(room, callback) { + redisClient.get(`${REDIS_PREFIX}-room:${room}-size`, (err, res) => { + callback(JSON.parse(res)) + }) + } - setBoardSize: function(room, size) { - redisClient.set(REDIS_PREFIX + '-room:' + room + '-size', JSON.stringify(size)); - }, - - getBoardSize: function(room, callback) { - redisClient.get(REDIS_PREFIX + '-room:' + room + '-size', function (err, res) { - callback(JSON.parse(res)); - }); - } - -}; -exports.db = db; +} +exports.db = db diff --git a/lib/rooms.js b/lib/rooms.js index 0648212..7682077 100644 --- a/lib/rooms.js +++ b/lib/rooms.js @@ -3,153 +3,137 @@ // PubSubCore: Simple pub/sub library for Node.js and Socket.IO -var util = require('util'); -var sets = require('simplesets'); -var io = require('socket.io'); -var net = require('net'); +const util = require('util') +const sets = require('simplesets') +const io = require('socket.io') +const net = require('net') -////////////////////////////// +/// /////////////////////////// // Tracking who's in what room -////////////////////////////// +/// /////////////////////////// // Dict mapping room names with people to sets of client objects. -var rooms = {}; +const rooms = {} // Dict mapping room names with people to sets of usernames. -var room_users = {}; +const room_users = {} // Dict mapping sids to sets of rooms. -var sid_rooms = {}; - +const sid_rooms = {} // Add a client to a room and return the sid:client mapping. -exports.add_to_room = function (client, room, callback) { - //console.log('Client ' + client.username + ' (' + client.id + ') added to room ' + room); +exports.add_to_room = function(client, room, callback) { + // console.log('Client ' + client.username + ' (' + client.id + ') added to room ' + room); - if (!(sid_rooms.hasOwnProperty(client.id))) sid_rooms[client.id] = new sets.Set(); - sid_rooms[client.id].add(room); + if (!(sid_rooms.hasOwnProperty(client.id))) sid_rooms[client.id] = new sets.Set() + sid_rooms[client.id].add(room) - if (!(rooms.hasOwnProperty(room))) rooms[room] = new sets.Set(); - rooms[room].add(client); + if (!(rooms.hasOwnProperty(room))) rooms[room] = new sets.Set() + rooms[room].add(client) - if (!(room_users.hasOwnProperty(room))) room_users[room] = new sets.Set(); - room_users[room].add(client.username); + if (!(room_users.hasOwnProperty(room))) room_users[room] = new sets.Set() + room_users[room].add(client.username) - callback(rooms[room].array()); -}; + callback(rooms[room].array()) +} // Remove a client from all rooms and return the username:client // mapping for everybody in those rooms. -exports.remove_from_all_rooms = function (client, callback) { - var affected_clients = new sets.Set(); - if (sid_rooms.hasOwnProperty(client.id)) { - var client_rooms = sid_rooms[client.id].array(); - for (var i = 0; i < client_rooms.length; i++) { - var room = client_rooms[i]; +exports.remove_from_all_rooms = function(client, callback) { + const affected_clients = new sets.Set() + if (sid_rooms.hasOwnProperty(client.id)) { + const client_rooms = sid_rooms[client.id].array() + for (let i = 0; i < client_rooms.length; i++) { + const room = client_rooms[i] if (rooms.hasOwnProperty(room)) { - rooms[room].remove(client); - if (rooms[room].size() === 0) - delete rooms[room]; + rooms[room].remove(client) + if (rooms[room].size() === 0) { delete rooms[room] } } if (room_users.hasOwnProperty(room)) { - room_users[room].remove(client.username); - if (room_users[room].size() === 0) - delete room_users[room]; + room_users[room].remove(client.username) + if (room_users[room].size() === 0) { delete room_users[room] } } if (rooms.hasOwnProperty(room)) { - var this_room = rooms[room].array(); - for (var j = 0; j < this_room.length; j++) - affected_clients.add(this_room[j]); + const this_room = rooms[room].array() + for (let j = 0; j < this_room.length; j++) { affected_clients.add(this_room[j]) } } - } } - //console.log('Client ' + client.username + ' (' + client.id + ') disconnected.'); - delete sid_rooms[client.id]; - callback(affected_clients.array()); -}; + } + // console.log('Client ' + client.username + ' (' + client.id + ') disconnected.'); + delete sid_rooms[client.id] + callback(affected_clients.array()) +} // Remove a client from a room and return the username:client mapping // for everybody in that room. Returns [] if the room does not exist, // or if the client was not in the room to begin with. function remove_from_room(client, room, callback) { - if (!rooms.hasOwnProperty(room) || !rooms[room].has(client)) { - callback([]); - return; - } + if (!rooms.hasOwnProperty(room) || !rooms[room].has(client)) { + callback([]) + return + } - // Delete from the room - rooms[room].remove(client); - if (rooms[room].size() === 0) - delete rooms[room]; - if (room_users.hasOwnProperty(room)) { - room_users[room].remove(client.username); - if (room_users[room].size() === 0) - delete room_users[room]; - } + // Delete from the room + rooms[room].remove(client) + if (rooms[room].size() === 0) { delete rooms[room] } + if (room_users.hasOwnProperty(room)) { + room_users[room].remove(client.username) + if (room_users[room].size() === 0) { delete room_users[room] } + } - callback(exports.room_clients(room)); + callback(exports.room_clients(room)) } // Return list of clients in the given room. exports.room_clients = function(room) { - return rooms.hasOwnProperty(room) ? rooms[room].array() : []; -}; + return rooms.hasOwnProperty(room) ? rooms[room].array() : [] +} // Return true if room contains the given client, false otherwise. exports.client_in_room = function(room, client) { - return rooms.hasOwnProperty(room) && rooms[room].has(client); -}; + return rooms.hasOwnProperty(room) && rooms[room].has(client) +} // Return list of usernames in given room exports.users_in_room = function(room) { - return room_users.hasOwnProperty(room) ? room_users[room].array() : []; -}; + return room_users.hasOwnProperty(room) ? room_users[room].array() : [] +} // Return list of usernames in given room exports.room_clients_other_than_me = function(room, client) { - if (rooms.hasOwnProperty(room)) - { - var clients = rooms[room]; - //console.dir(clients.array()); + if (rooms.hasOwnProperty(room)) { + const clients = rooms[room] + // console.dir(clients.array()); - clients.remove(client); - //console.dir(clients.array()); - return clients.array(); - } - else - { - return []; - } -}; + clients.remove(client) + // console.dir(clients.array()); + return clients.array() + } -//gets the current room of the client (assumes one room -- will select first one if in multiple) -exports.get_room = function (client) { - var client_rooms = null; + return [] +} - if (sid_rooms.hasOwnProperty(client.id)) - { - client_rooms = sid_rooms[client.id].array(); - } +// gets the current room of the client (assumes one room -- will select first one if in multiple) +exports.get_room = function(client) { + let client_rooms = null - if ( client_rooms !== null ) - return client_rooms[0]; - else - return null; -}; + if (sid_rooms.hasOwnProperty(client.id)) { + client_rooms = sid_rooms[client.id].array() + } + if (client_rooms !== null) { return client_rooms[0] } + return null +} // Generic server code -exports.add_to_room_and_announce = function (client, room, msg) { - - // Add user info to the current dramatis personae - exports.add_to_room(client, room, function(clients) { +exports.add_to_room_and_announce = function(client, room, msg) { + // Add user info to the current dramatis personae + exports.add_to_room(client, room, (clients) => { // Broadcast new-user notification - for (var i = 0; i < clients.length; i++) - { - if (clients[i].id != client.id) - clients[i].json.send(msg); - } - }); -}; + for (let i = 0; i < clients.length; i++) { + if (clients[i].id != client.id) { clients[i].json.send(msg) } + } + }) +} /* exports.on_leave_room = function (client, room) { @@ -165,67 +149,58 @@ exports.on_leave_room = function (client, room) { }); }); -}*/ +} */ -//remember that this announces to all rooms that this client was a member of -exports.remove_from_all_rooms_and_announce = function (client, msg) { - exports.remove_from_all_rooms(client, function(clients) { - for (var i = 0; i < clients.length; i++) - { - if (clients[i].id != client.id) - clients[i].json.send(msg); - } - }); -}; +// remember that this announces to all rooms that this client was a member of +exports.remove_from_all_rooms_and_announce = function(client, msg) { + exports.remove_from_all_rooms(client, (clients) => { + for (let i = 0; i < clients.length; i++) { + if (clients[i].id != client.id) { clients[i].json.send(msg) } + } + }) +} -////////////////////////////// +/// /////////////////////////// // Broadcasting functions -////////////////////////////// +/// /////////////////////////// // Broadcast message to all clients exports.broadcast = function(msg) { - if (socket) socket.broadcast(msg); - net_server_streams.each(function(stream) { - stream.write(JSON.stringify(msg)+'\r\n'); - }); -}; + if (socket) socket.broadcast(msg) + net_server_streams.each((stream) => { + stream.write(`${JSON.stringify(msg)}\r\n`) + }) +} // Broadcast message to all clients in a given room. exports.broadcast_room = function(room, msg) { - var clients = exports.room_clients(room); - for (var i = 0; i < clients.length; i++) - clients[i].json.send(msg); -}; + const clients = exports.room_clients(room) + for (let i = 0; i < clients.length; i++) { clients[i].json.send(msg) } +} // Broadcast message to all the other clients that are in rooms with this client -exports.broadcast_to_roommates = function (client, msg) { - var roommates = new sets.Set(); +exports.broadcast_to_roommates = function(client, msg) { + let roommates = new sets.Set() - if (sid_rooms.hasOwnProperty(client.id)) - { - var client_rooms = sid_rooms[client.id].array(); - for (var i = 0; i < client_rooms.length; i++) - { - var room = client_rooms[i]; - if (rooms.hasOwnProperty(room)) - { - var this_room = rooms[room].array(); - for (var j = 0; j < this_room.length; j++) - roommates.add(this_room[j]); + if (sid_rooms.hasOwnProperty(client.id)) { + const client_rooms = sid_rooms[client.id].array() + for (let i = 0; i < client_rooms.length; i++) { + const room = client_rooms[i] + if (rooms.hasOwnProperty(room)) { + const this_room = rooms[room].array() + for (let j = 0; j < this_room.length; j++) { roommates.add(this_room[j]) } } - } - } + } + } - //remove self from the set - roommates.remove(client); - roommates = roommates.array(); + // remove self from the set + roommates.remove(client) + roommates = roommates.array() - //console.log('client: ' + client.id + " is broadcasting to: "); + // console.log('client: ' + client.id + " is broadcasting to: "); - - for (var k = 0; k < roommates.length; k++) - { - //console.log(' - ' + roommates[i].id); - roommates[k].json.send(msg); - } -}; + for (let k = 0; k < roommates.length; k++) { + // console.log(' - ' + roommates[i].id); + roommates[k].json.send(msg) + } +} diff --git a/package.json b/package.json index bc4652b..abe2fd4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "author": "Florian Schmitt", "scripts": { + "lint-js": "eslint --fix --ext .js,.ts .", "start": "nodemon server.js -e js,css,jade,json" }, "nodemonConfig": { @@ -34,6 +35,13 @@ "yargs": "~2.3.0" }, "devDependencies": { + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "^9.22.0", + "eslint": "^9.22.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-import": "^2.31", + "globals": "^16.0.0", + "prettier": "^3.5", "forever": "^0.15.3", "nodemon": "^2.0.16" } diff --git a/server.js b/server.js index 3ebeb54..299ed02 100644 --- a/server.js +++ b/server.js @@ -1,188 +1,190 @@ // vim:set noexpandtab: -/************** +/** ************ SYSTEM INCLUDES -**************/ -var http = require("http"); -var reload = require("reload"); -var sanitizer = require("sanitizer"); -var compression = require("compression"); -var express = require("express"); -var conf = require("./config.js").server; -var headerBarUrl = require("./config.js").headerBarUrl; -var logoUrl = require("./config.js").logoUrl; +************* */ +const http = require('http') +const reload = require('reload') +const sanitizer = require('sanitizer') +const compression = require('compression') +const express = require('express') +const conf = require('./config.js').server +const { headerBarUrl } = require('./config.js') +const { logoUrl } = require('./config.js') -/************** +/** ************ LOCAL INCLUDES -**************/ -var rooms = require("./lib/rooms.js"); -var data = require("./lib/data.js").db; +************* */ +const rooms = require('./lib/rooms.js') +let data = require('./lib/data.js').db -/************** +/** ************ GLOBALS -**************/ -//Map of sids to user_names -var sids_to_user_names = []; +************* */ +// Map of sids to user_names +const sids_to_user_names = [] -/************** +/** ************ SETUP EXPRESS -**************/ -var app = express(); -var router = express.Router(); +************* */ +const app = express() +const router = express.Router() -app.use(compression()); -app.use(conf.baseurl, router); +app.use(compression()) +app.use(conf.baseurl, router) + +router.use(express.static(`${__dirname}/node_modules`)) +router.use(express.static(`${__dirname}/client`)) + +const server = http.createServer(app) -router.use(express.static(__dirname + "/node_modules")); -router.use(express.static(__dirname + "/client")); -var server = http.createServer(app); // Reload code here reload(app) - .then(function (reloadReturned) { + .then((reloadReturned) => { // reloadReturned is documented in the returns API in the README // Reload started, start web server - server.listen(conf.port, function () { - console.log("Web server available on http://127.0.0.1:" + conf.port); - }); + server.listen(conf.port, () => { + console.log(`Web server available on http://127.0.0.1:${conf.port}`) + }) }) - .catch(function (err) { + .catch((err) => { console.error( - "Reload could not start, could not start server/sample app", + 'Reload could not start, could not start server/sample app', err - ); - }); + ) + }) -/************** +/** ************ SETUP Socket.IO -**************/ -var io = require("socket.io")(server, { - path: conf.baseurl == "/" ? "" : conf.baseurl + "/socket.io", +************* */ + +const io = require('socket.io')(server, { + path: conf.baseurl == '/' ? '' : `${conf.baseurl}/socket.io`, cookie: false -}); +}) -/************** +/** ************ ROUTES -**************/ -router.get("/", function (req, res) { - //console.log(req.header('host')); - url = req.header("host") + req.baseUrl; - var connected = io.sockets.connected; - clientsCount = Object.keys(connected).length; - res.render("home.jade", { - url: url, - headerBarUrl: headerBarUrl, - logoUrl: logoUrl, +************* */ +router.get('/', (req, res) => { + // console.log(req.header('host')); + url = req.header('host') + req.baseUrl + const { connected } = io.sockets + clientsCount = Object.keys(connected).length + res.render('home.jade', { + url, + headerBarUrl, + logoUrl, connected: clientsCount, - home: true, - }); -}); + home: true + }) +}) -router.get("/demo", function (req, res) { - url = req.header("host") + req.baseUrl; - res.render("index.jade", { - pageTitle: "Post-it - demo", - headerBarUrl: headerBarUrl, - logoUrl: logoUrl, - url: url, - demo: true, - }); -}); +router.get('/demo', (req, res) => { + url = req.header('host') + req.baseUrl + res.render('index.jade', { + pageTitle: 'Post-it - demo', + headerBarUrl, + logoUrl, + url, + demo: true + }) +}) -router.get("/:id", function (req, res) { - url = req.header("host") + req.baseUrl; - res.render("index.jade", { - pageTitle: "Post-it - " + req.params.id, - headerBarUrl: headerBarUrl, - logoUrl: logoUrl, - url: url, - }); -}); +router.get('/:id', (req, res) => { + url = req.header('host') + req.baseUrl + res.render('index.jade', { + pageTitle: `Post-it - ${req.params.id}`, + headerBarUrl, + logoUrl, + url + }) +}) -router.get("/stats", function (req, res) { - console.log("TODO: stats"); -}); +router.get('/stats', (req, res) => { + console.log('TODO: stats') +}) -/************** +/** ************ SOCKET.I0 -**************/ -//sanitizes text +************* */ +// sanitizes text function scrub(text) { - if (typeof text != "undefined" && text !== null) { - //clip the string if it is too long + if (typeof text != 'undefined' && text !== null) { + // clip the string if it is too long if (text.length > 65535) { - text = text.substr(0, 65535); + text = text.substr(0, 65535) } - return sanitizer.sanitize(text); - } else { - return null; + return sanitizer.sanitize(text) } + return null } -io.sockets.on("connection", function (client) { - client.on("message", function (message) { - //console.log(message.action + " -- " + sys.inspect(message.data) ); +io.sockets.on('connection', (client) => { + client.on('message', (message) => { + // console.log(message.action + " -- " + sys.inspect(message.data) ); - var clean_data = {}; - var clean_message = {}; - var message_out = {}; + let clean_data = {} + let clean_message = {} + let message_out = {} - if (!message.action) return; + if (!message.action) return switch (message.action) { - case "initializeMe": - initClient(client); - break; + case 'initializeMe': + initClient(client) + break - case "joinRoom": - joinRoom(client, message.data, function (clients) { - client.json.send({ action: "roomAccept", data: "" }); - }); + case 'joinRoom': + joinRoom(client, message.data, (clients) => { + client.json.send({ action: 'roomAccept', data: '' }) + }) - break; + break - case "moveCard": - //report to all other browsers + case 'moveCard': + // report to all other browsers message_out = { action: message.action, data: { id: scrub(message.data.id), position: { left: scrub(message.data.position.left), - top: scrub(message.data.position.top), - }, - }, - }; + top: scrub(message.data.position.top) + } + } + } - broadcastToRoom(client, message_out); + broadcastToRoom(client, message_out) // console.log("-----" + message.data.id); // console.log(JSON.stringify(message.data)); - getRoom(client, function (room) { + getRoom(client, (room) => { db.cardSetXY( room, message.data.id, message.data.position.left, message.data.position.top - ); - }); + ) + }) - break; + break - case "createCard": - data = message.data; - clean_data = {}; - clean_data.text = scrub(data.text); - clean_data.id = scrub(data.id); - clean_data.x = scrub(data.x); - clean_data.y = scrub(data.y); - clean_data.rot = scrub(data.rot); - clean_data.colour = scrub(data.colour); + case 'createCard': + data = message.data + clean_data = {} + clean_data.text = scrub(data.text) + clean_data.id = scrub(data.id) + clean_data.x = scrub(data.x) + clean_data.y = scrub(data.y) + clean_data.rot = scrub(data.rot) + clean_data.colour = scrub(data.colour) - getRoom(client, function (room) { + getRoom(client, (room) => { createCard( room, clean_data.id, @@ -191,681 +193,677 @@ io.sockets.on("connection", function (client) { clean_data.y, clean_data.rot, clean_data.colour - ); - }); + ) + }) message_out = { - action: "createCard", - data: clean_data, - }; - - //report to all other browsers - broadcastToRoom(client, message_out); - break; - - case "editCard": - clean_data = {}; - clean_data.value = scrub(message.data.value); - clean_data.id = scrub(message.data.id); - - //send update to database - getRoom(client, function (room) { - db.cardEdit(room, clean_data.id, clean_data.value); - }); - - message_out = { - action: "editCard", - data: clean_data, - }; - - broadcastToRoom(client, message_out); - - break; - - case "deleteCard": - clean_message = { - action: "deleteCard", - data: { id: scrub(message.data.id) }, - }; - - getRoom(client, function (room) { - db.deleteCard(room, clean_message.data.id); - }); - - //report to all other browsers - broadcastToRoom(client, clean_message); - - break; - - case "createColumn": - clean_message = { data: scrub(message.data) }; - - getRoom(client, function (room) { - db.createColumn(room, clean_message.data, function () {}); - }); - - broadcastToRoom(client, clean_message); - - break; - - case "deleteColumn": - getRoom(client, function (room) { - db.deleteColumn(room); - }); - broadcastToRoom(client, { action: "deleteColumn" }); - - break; - - case "updateColumns": - var columns = message.data; - - if (!(columns instanceof Array)) break; - - var clean_columns = []; - - for (var i in columns) { - clean_columns[i] = scrub(columns[i]); + action: 'createCard', + data: clean_data } - getRoom(client, function (room) { - db.setColumns(room, clean_columns); - }); + + // report to all other browsers + broadcastToRoom(client, message_out) + break + + case 'editCard': + clean_data = {} + clean_data.value = scrub(message.data.value) + clean_data.id = scrub(message.data.id) + + // send update to database + getRoom(client, (room) => { + db.cardEdit(room, clean_data.id, clean_data.value) + }) + + message_out = { + action: 'editCard', + data: clean_data + } + + broadcastToRoom(client, message_out) + + break + + case 'deleteCard': + clean_message = { + action: 'deleteCard', + data: { id: scrub(message.data.id) } + } + + getRoom(client, (room) => { + db.deleteCard(room, clean_message.data.id) + }) + + // report to all other browsers + broadcastToRoom(client, clean_message) + + break + + case 'createColumn': + clean_message = { data: scrub(message.data) } + + getRoom(client, (room) => { + db.createColumn(room, clean_message.data, () => {}) + }) + + broadcastToRoom(client, clean_message) + + break + + case 'deleteColumn': + getRoom(client, (room) => { + db.deleteColumn(room) + }) + broadcastToRoom(client, { action: 'deleteColumn' }) + + break + + case 'updateColumns': + var columns = message.data + + if (!(columns instanceof Array)) break + + var clean_columns = [] + + for (const i in columns) { + clean_columns[i] = scrub(columns[i]) + } + getRoom(client, (room) => { + db.setColumns(room, clean_columns) + }) broadcastToRoom(client, { - action: "updateColumns", - data: clean_columns, - }); + action: 'updateColumns', + data: clean_columns + }) - break; + break - case "changeTheme": - clean_message = {}; - clean_message.data = scrub(message.data); + case 'changeTheme': + clean_message = {} + clean_message.data = scrub(message.data) - getRoom(client, function (room) { - db.setTheme(room, clean_message.data); - }); + getRoom(client, (room) => { + db.setTheme(room, clean_message.data) + }) - clean_message.action = "changeTheme"; + clean_message.action = 'changeTheme' - broadcastToRoom(client, clean_message); - break; + broadcastToRoom(client, clean_message) + break - case "setUserName": - clean_message = {}; + case 'setUserName': + clean_message = {} - clean_message.data = scrub(message.data); + clean_message.data = scrub(message.data) - setUserName(client, clean_message.data); + setUserName(client, clean_message.data) - var msg = {}; - msg.action = "nameChangeAnnounce"; - msg.data = { sid: client.id, user_name: clean_message.data }; - broadcastToRoom(client, msg); - break; + var msg = {} + msg.action = 'nameChangeAnnounce' + msg.data = { sid: client.id, user_name: clean_message.data } + broadcastToRoom(client, msg) + break - case "addSticker": - var cardId = scrub(message.data.cardId); - var stickerId = scrub(message.data.stickerId); + case 'addSticker': + var cardId = scrub(message.data.cardId) + var stickerId = scrub(message.data.stickerId) - getRoom(client, function (room) { - db.addSticker(room, cardId, stickerId); - }); + getRoom(client, (room) => { + db.addSticker(room, cardId, stickerId) + }) broadcastToRoom(client, { - action: "addSticker", - data: { cardId: cardId, stickerId: stickerId }, - }); - break; + action: 'addSticker', + data: { cardId, stickerId } + }) + break - case "setBoardSize": - var size = {}; - size.width = scrub(message.data.width); - size.height = scrub(message.data.height); + case 'setBoardSize': + var size = {} + size.width = scrub(message.data.width) + size.height = scrub(message.data.height) - getRoom(client, function (room) { - db.setBoardSize(room, size); - }); + getRoom(client, (room) => { + db.setBoardSize(room, size) + }) - broadcastToRoom(client, { action: "setBoardSize", data: size }); - break; + broadcastToRoom(client, { action: 'setBoardSize', data: size }) + break - case "exportTxt": - exportBoard("txt", client, message.data); - break; + case 'exportTxt': + exportBoard('txt', client, message.data) + break - case "exportCsv": - exportBoard("csv", client, message.data); - break; + case 'exportCsv': + exportBoard('csv', client, message.data) + break - case "exportJson": - exportJson(client, message.data); - break; + case 'exportJson': + exportJson(client, message.data) + break - case "importJson": - importJson(client, message.data); - break; + case 'importJson': + importJson(client, message.data) + break - case "createRevision": - createRevision(client, message.data); - break; + case 'createRevision': + createRevision(client, message.data) + break - case "deleteRevision": - deleteRevision(client, message.data); - break; + case 'deleteRevision': + deleteRevision(client, message.data) + break - case "exportRevision": - exportRevision(client, message.data); - break; + case 'exportRevision': + exportRevision(client, message.data) + break default: - //console.log('unknown action'); - break; + // console.log('unknown action'); + break } - }); + }) - client.on("disconnect", function () { - leaveRoom(client); - }); + client.on('disconnect', () => { + leaveRoom(client) + }) - //tell all others that someone has connected - //client.broadcast('someone has connected'); -}); + // tell all others that someone has connected + // client.broadcast('someone has connected'); +}) -/************** +/** ************ FUNCTIONS -**************/ +************* */ function initClient(client) { - //console.log ('initClient Started'); - getRoom(client, function (room) { - db.getAllCards(room, function (cards) { + // console.log ('initClient Started'); + getRoom(client, (room) => { + db.getAllCards(room, (cards) => { client.json.send({ - action: "initCards", - data: cards, - }); - }); + action: 'initCards', + data: cards + }) + }) - db.getAllColumns(room, function (columns) { + db.getAllColumns(room, (columns) => { client.json.send({ - action: "initColumns", - data: columns, - }); - }); + action: 'initColumns', + data: columns + }) + }) - db.getRevisions(room, function (revisions) { + db.getRevisions(room, (revisions) => { client.json.send({ - action: "initRevisions", - data: revisions !== null ? Object.keys(revisions) : new Array(), - }); - }); + action: 'initRevisions', + data: revisions !== null ? Object.keys(revisions) : [] + }) + }) - db.getTheme(room, function (theme) { - if (theme === null) theme = "bigcards"; + db.getTheme(room, (theme) => { + if (theme === null) theme = 'bigcards' client.json.send({ - action: "changeTheme", - data: theme, - }); - }); + action: 'changeTheme', + data: theme + }) + }) - db.getBoardSize(room, function (size) { + db.getBoardSize(room, (size) => { if (size !== null) { client.json.send({ - action: "setBoardSize", - data: size, - }); + action: 'setBoardSize', + data: size + }) } - }); + }) - roommates_clients = rooms.room_clients(room); - roommates = []; + roommates_clients = rooms.room_clients(room) + roommates = [] - var j = 0; - for (var i in roommates_clients) { + let j = 0 + for (const i in roommates_clients) { if (roommates_clients[i].id != client.id) { roommates[j] = { sid: roommates_clients[i].id, - user_name: sids_to_user_names[roommates_clients[i].id], - }; - j++; + user_name: sids_to_user_names[roommates_clients[i].id] + } + j++ } } - //console.log('initialusers: ' + roommates); + // console.log('initialusers: ' + roommates); client.json.send({ - action: "initialUsers", - data: roommates, - }); - }); + action: 'initialUsers', + data: roommates + }) + }) } function joinRoom(client, room, successFunction) { - var msg = {}; - msg.action = "join-announce"; - msg.data = { sid: client.id, user_name: client.user_name }; + const msg = {} + msg.action = 'join-announce' + msg.data = { sid: client.id, user_name: client.user_name } - rooms.add_to_room_and_announce(client, room, msg); - successFunction(); + rooms.add_to_room_and_announce(client, room, msg) + successFunction() } function leaveRoom(client) { - //console.log (client.id + ' just left'); - var msg = {}; - msg.action = "leave-announce"; - msg.data = { sid: client.id }; - rooms.remove_from_all_rooms_and_announce(client, msg); + // console.log (client.id + ' just left'); + const msg = {} + msg.action = 'leave-announce' + msg.data = { sid: client.id } + rooms.remove_from_all_rooms_and_announce(client, msg) - delete sids_to_user_names[client.id]; + delete sids_to_user_names[client.id] } function broadcastToRoom(client, message) { - rooms.broadcast_to_roommates(client, message); + rooms.broadcast_to_roommates(client, message) } -//----------------CARD FUNCTIONS +// ----------------CARD FUNCTIONS function createCard(room, id, text, x, y, rot, colour) { - var card = { - id: id, - colour: colour, - rot: rot, - x: x, - y: y, - text: text, - sticker: null, - }; + const card = { + id, + colour, + rot, + x, + y, + text, + sticker: null + } - db.createCard(room, id, card); + db.createCard(room, id, card) } function roundRand(max) { - return Math.floor(Math.random() * max); + return Math.floor(Math.random() * max) } -//------------ROOM STUFF +// ------------ROOM STUFF // Get Room name for the given Session ID function getRoom(client, callback) { - room = rooms.get_room(client); - //console.log( 'client: ' + client.id + " is in " + room); - callback(room); + room = rooms.get_room(client) + // console.log( 'client: ' + client.id + " is in " + room); + callback(room) } function setUserName(client, name) { - client.user_name = name; - sids_to_user_names[client.id] = name; - //console.log('sids to user names: '); - console.dir(sids_to_user_names); + client.user_name = name + sids_to_user_names[client.id] = name + // console.log('sids to user names: '); + console.dir(sids_to_user_names) } function cleanAndInitializeDemoRoom() { // DUMMY DATA - db.clearRoom("/demo", function () { - db.createColumn("/demo", "Pas commencé"); - db.createColumn("/demo", "Commencé"); - db.createColumn("/demo", "En test"); - db.createColumn("/demo", "Validation"); - db.createColumn("/demo", "Terminé"); + db.clearRoom('/demo', () => { + db.createColumn('/demo', 'Pas commencé') + db.createColumn('/demo', 'Commencé') + db.createColumn('/demo', 'En test') + db.createColumn('/demo', 'Validation') + db.createColumn('/demo', 'Terminé') createCard( - "/demo", - "card1", + '/demo', + 'card1', "Salut, c'est fun", roundRand(600), roundRand(300), Math.random() * 10 - 5, - "yellow" - ); + 'yellow' + ) createCard( - "/demo", - "card2", + '/demo', + 'card2', "Salut, c'est une nouvelle histoire.", roundRand(600), roundRand(300), Math.random() * 10 - 5, - "white" - ); + 'white' + ) createCard( - "/demo", - "card3", - ".", + '/demo', + 'card3', + '.', roundRand(600), roundRand(300), Math.random() * 10 - 5, - "blue" - ); + 'blue' + ) createCard( - "/demo", - "card4", - ".", + '/demo', + 'card4', + '.', roundRand(600), roundRand(300), Math.random() * 10 - 5, - "green" - ); + 'green' + ) createCard( - "/demo", - "card5", + '/demo', + 'card5', "Salut, c'est fun", roundRand(600), roundRand(300), Math.random() * 10 - 5, - "yellow" - ); + 'yellow' + ) createCard( - "/demo", - "card6", + '/demo', + 'card6', "Salut, c'est un nouveau mémo.", roundRand(600), roundRand(300), Math.random() * 10 - 5, - "yellow" - ); + 'yellow' + ) createCard( - "/demo", - "card7", - ".", + '/demo', + 'card7', + '.', roundRand(600), roundRand(300), Math.random() * 10 - 5, - "blue" - ); + 'blue' + ) createCard( - "/demo", - "card8", - ".", + '/demo', + 'card8', + '.', roundRand(600), roundRand(300), Math.random() * 10 - 5, - "green" - ); - }); + 'green' + ) + }) } // Export board in txt or csv function exportBoard(format, client, data) { - var result = new Array(); - getRoom(client, function (room) { - db.getAllCards(room, function (cards) { - db.getAllColumns(room, function (columns) { - var text = new Array(); - var cols = {}; + const result = [] + getRoom(client, (room) => { + db.getAllCards(room, (cards) => { + db.getAllColumns(room, (columns) => { + const text = [] + const cols = {} if (columns.length > 0) { for (var i = 0; i < columns.length; i++) { - cols[columns[i]] = new Array(); + cols[columns[i]] = [] for (var j = 0; j < cards.length; j++) { if (i === 0) { - if (cards[j]["x"] < (i + 1) * data) { - cols[columns[i]].push(cards[j]); + if (cards[j].x < (i + 1) * data) { + cols[columns[i]].push(cards[j]) } } else if (i + 1 === columns.length) { - if (cards[j]["x"] >= i * data) { - cols[columns[i]].push(cards[j]); + if (cards[j].x >= i * data) { + cols[columns[i]].push(cards[j]) } } else if ( - cards[j]["x"] >= i * data && - cards[j]["x"] < (i + 1) * data + cards[j].x >= i * data + && cards[j].x < (i + 1) * data ) { - cols[columns[i]].push(cards[j]); + cols[columns[i]].push(cards[j]) } } - cols[columns[i]].sort(function (a, b) { - if (a["y"] === b["y"]) { - return a["x"] - b["x"]; - } else { - return a["y"] - b["y"]; + cols[columns[i]].sort((a, b) => { + if (a.y === b.y) { + return a.x - b.x } - }); + return a.y - b.y + }) } - if (format === "txt") { + if (format === 'txt') { for (var i = 0; i < columns.length; i++) { if (i === 0) { - text.push("# " + columns[i]); + text.push(`# ${columns[i]}`) } else { - text.push("\n# " + columns[i]); + text.push(`\n# ${columns[i]}`) } for (var j = 0; j < cols[columns[i]].length; j++) { - text.push("- " + cols[columns[i]][j]["text"]); + text.push(`- ${cols[columns[i]][j].text}`) } } - } else if (format === "csv") { - var max = 0; - var line = new Array(); - var patt_vuln = new RegExp("^[=+-@]"); + } else if (format === 'csv') { + let max = 0 + let line = [] + const patt_vuln = new RegExp('^[=+-@]') for (var i = 0; i < columns.length; i++) { if (cols[columns[i]].length > max) { - max = cols[columns[i]].length; + max = cols[columns[i]].length } - var val = columns[i].replace(/"/g, '""'); + var val = columns[i].replace(/"/g, '""') if (patt_vuln.test(val)) { // prevent CSV Formula Injection - var val = "'" + val; + var val = `'${val}` } - line.push('"' + val + '"'); + line.push(`"${val}"`) } - text.push(line.join(",")); + text.push(line.join(',')) for (var j = 0; j < max; j++) { - line = new Array(); + line = [] for (var i = 0; i < columns.length; i++) { - var val = - cols[columns[i]][j] !== undefined - ? cols[columns[i]][j]["text"].replace(/"/g, '""') - : ""; + var val = cols[columns[i]][j] !== undefined + ? cols[columns[i]][j].text.replace(/"/g, '""') + : '' if (patt_vuln.test(val)) { // prevent CSV Formula Injection - var val = "'" + val; + var val = `'${val}` } - line.push('"' + val + '"'); + line.push(`"${val}"`) } - text.push(line.join(",")); + text.push(line.join(',')) } } } else { for (var j = 0; j < cards.length; j++) { - if (format === "txt") { - text.push("- " + cards[j]["text"]); - } else if (format === "csv") { - text.push('"' + cards[j]["text"].replace(/"/g, '""') + '"\n'); + if (format === 'txt') { + text.push(`- ${cards[j].text}`) + } else if (format === 'csv') { + text.push(`"${cards[j].text.replace(/"/g, '""')}"\n`) } } } - var result; - if (format === "txt" || format === "csv") { - result = text.join("\n"); - } else if (format === "json") { - result = JSON.stringify(cols); + let result + if (format === 'txt' || format === 'csv') { + result = text.join('\n') + } else if (format === 'json') { + result = JSON.stringify(cols) } client.json.send({ - action: "export", + action: 'export', data: { - filename: room.replace("/", "") + "." + format, - text: result, - }, - }); - }); - }); - }); + filename: `${room.replace('/', '')}.${format}`, + text: result + } + }) + }) + }) + }) } // Export board in json, suitable for import function exportJson(client, data) { - var result = new Array(); - getRoom(client, function (room) { - db.getAllCards(room, function (cards) { - db.getAllColumns(room, function (columns) { - db.getTheme(room, function (theme) { - db.getBoardSize(room, function (size) { - if (theme === null) theme = "bigcards"; - if (size === null) - size = { width: data.width, height: data.height }; + let result = [] + getRoom(client, (room) => { + db.getAllCards(room, (cards) => { + db.getAllColumns(room, (columns) => { + db.getTheme(room, (theme) => { + db.getBoardSize(room, (size) => { + if (theme === null) theme = 'bigcards' + if (size === null) { size = { width: data.width, height: data.height } } result = JSON.stringify({ - cards: cards, - columns: columns, - theme: theme, - size: size, - }); + cards, + columns, + theme, + size + }) client.json.send({ - action: "export", + action: 'export', data: { - filename: room.replace("/", "") + ".json", - text: result, - }, - }); - }); - }); - }); - }); - }); + filename: `${room.replace('/', '')}.json`, + text: result + } + }) + }) + }) + }) + }) + }) } // Import board from json function importJson(client, data) { - getRoom(client, function (room) { - db.clearRoom(room, function () { - db.getAllCards(room, function (cards) { + getRoom(client, (room) => { + db.clearRoom(room, () => { + db.getAllCards(room, (cards) => { for (var i = 0; i < cards.length; i++) { - db.deleteCard(room, cards[i].id); + db.deleteCard(room, cards[i].id) } - cards = data.cards; - var cards2 = new Array(); + cards = data.cards + const cards2 = [] for (var i = 0; i < cards.length; i++) { - var card = cards[i]; + const card = cards[i] if ( - card.id !== undefined && - card.colour !== undefined && - card.rot !== undefined && - card.x !== undefined && - card.y !== undefined && - card.text !== undefined && - card.sticker !== undefined + card.id !== undefined + && card.colour !== undefined + && card.rot !== undefined + && card.x !== undefined + && card.y !== undefined + && card.text !== undefined + && card.sticker !== undefined ) { - var c = { + const c = { id: card.id, colour: card.colour, rot: card.rot, x: card.x, y: card.y, text: scrub(card.text), - sticker: card.sticker, - }; - db.createCard(room, c.id, c); - cards2.push(c); + sticker: card.sticker + } + db.createCard(room, c.id, c) + cards2.push(c) } } - var msg = { action: "initCards", data: cards2 }; - broadcastToRoom(client, msg); - client.json.send(msg); - }); + const msg = { action: 'initCards', data: cards2 } + broadcastToRoom(client, msg) + client.json.send(msg) + }) - db.getAllColumns(room, function (columns) { + db.getAllColumns(room, (columns) => { for (var i = 0; i < columns.length; i++) { - db.deleteColumn(room); + db.deleteColumn(room) } - columns = data.columns; - var columns2 = new Array(); + columns = data.columns + const columns2 = [] for (var i = 0; i < columns.length; i++) { - var column = scrub(columns[i]); - if (typeof column === "string") { - db.createColumn(room, column); - columns2.push(column); + const column = scrub(columns[i]) + if (typeof column === 'string') { + db.createColumn(room, column) + columns2.push(column) } } - msg = { action: "initColumns", data: columns2 }; - broadcastToRoom(client, msg); - client.json.send(msg); - }); + msg = { action: 'initColumns', data: columns2 } + broadcastToRoom(client, msg) + client.json.send(msg) + }) - var size = data.size; + let { size } = data if (size.width !== undefined && size.height !== undefined) { - size = { width: scrub(size.width), height: scrub(size.height) }; - db.setBoardSize(room, size); - msg = { action: "setBoardSize", data: size }; - broadcastToRoom(client, msg); - client.json.send(msg); + size = { width: scrub(size.width), height: scrub(size.height) } + db.setBoardSize(room, size) + msg = { action: 'setBoardSize', data: size } + broadcastToRoom(client, msg) + client.json.send(msg) } - data.theme = scrub(data.theme); - if (data.theme === "smallcards" || data.theme === "bigcards") { - db.setTheme(room, data.theme); - msg = { action: "changeTheme", data: data.theme }; - broadcastToRoom(client, msg); - client.json.send(msg); + data.theme = scrub(data.theme) + if (data.theme === 'smallcards' || data.theme === 'bigcards') { + db.setTheme(room, data.theme) + msg = { action: 'changeTheme', data: data.theme } + broadcastToRoom(client, msg) + client.json.send(msg) } - }); - }); + }) + }) } // function createRevision(client, data) { - var result = new Array(); - getRoom(client, function (room) { - db.getAllCards(room, function (cards) { - db.getAllColumns(room, function (columns) { - db.getTheme(room, function (theme) { - db.getBoardSize(room, function (size) { - if (theme === null) theme = "bigcards"; - if (size === null) - size = { width: data.width, height: data.height }; + let result = [] + getRoom(client, (room) => { + db.getAllCards(room, (cards) => { + db.getAllColumns(room, (columns) => { + db.getTheme(room, (theme) => { + db.getBoardSize(room, (size) => { + if (theme === null) theme = 'bigcards' + if (size === null) { size = { width: data.width, height: data.height } } result = { - cards: cards, - columns: columns, - theme: theme, - size: size, - }; - var timestamp = Date.now(); - db.getRevisions(room, function (revisions) { - if (revisions === null) revisions = {}; - revisions[timestamp + ""] = result; - db.setRevisions(room, revisions); - msg = { action: "addRevision", data: timestamp }; - broadcastToRoom(client, msg); - client.json.send(msg); - }); - }); - }); - }); - }); - }); + cards, + columns, + theme, + size + } + const timestamp = Date.now() + db.getRevisions(room, (revisions) => { + if (revisions === null) revisions = {} + revisions[`${timestamp}`] = result + db.setRevisions(room, revisions) + msg = { action: 'addRevision', data: timestamp } + broadcastToRoom(client, msg) + client.json.send(msg) + }) + }) + }) + }) + }) + }) } function deleteRevision(client, timestamp) { - getRoom(client, function (room) { - db.getRevisions(room, function (revisions) { - if (revisions !== null && revisions[timestamp + ""] !== undefined) { - delete revisions[timestamp + ""]; - db.setRevisions(room, revisions); + getRoom(client, (room) => { + db.getRevisions(room, (revisions) => { + if (revisions !== null && revisions[`${timestamp}`] !== undefined) { + delete revisions[`${timestamp}`] + db.setRevisions(room, revisions) } - msg = { action: "deleteRevision", data: timestamp }; - broadcastToRoom(client, msg); - client.json.send(msg); - }); - }); + msg = { action: 'deleteRevision', data: timestamp } + broadcastToRoom(client, msg) + client.json.send(msg) + }) + }) } function exportRevision(client, timestamp) { - getRoom(client, function (room) { - db.getRevisions(room, function (revisions) { - if (revisions !== null && revisions[timestamp + ""] !== undefined) { + getRoom(client, (room) => { + db.getRevisions(room, (revisions) => { + if (revisions !== null && revisions[`${timestamp}`] !== undefined) { client.json.send({ - action: "export", + action: 'export', data: { - filename: room.replace("/", "") + "-" + timestamp + ".json", - text: JSON.stringify(revisions[timestamp + ""]), - }, - }); + filename: `${room.replace('/', '')}-${timestamp}.json`, + text: JSON.stringify(revisions[`${timestamp}`]) + } + }) } else { client.json.send({ - action: "message", - data: "Unable to find revision " + timestamp + ".", - }); + action: 'message', + data: `Unable to find revision ${timestamp}.` + }) } - }); - }); + }) + }) } -/************** +/** ************ SETUP DATABASE ON FIRST RUN -**************/ +************* */ // (runs only once on startup) -var db = new data(function () { - cleanAndInitializeDemoRoom(); -}); +var db = new data(() => { + cleanAndInitializeDemoRoom() +}) diff --git a/views/home.jade b/views/home.jade index 910e508..594e206 100644 --- a/views/home.jade +++ b/views/home.jade @@ -17,4 +17,4 @@ block body input.text(type="text", name="name") a#go(onclick="return go();") OK p.home Exemple : - p.home!= '
    ' + locals.url + '/demo' + p.home!= '' + locals.url + '/demo' \ No newline at end of file