veto

Simple Sinatra voting website.
git clone git://git.knutsen.co/veto
Log | Files | Refs | README | LICENSE

commit 88286804eaaea2c862cef333a997dbc707ec2c98
parent 30ffddfa72d7e358fa344de7eeda31b56fa459da
Author: Fredrik <moi@knutsen.co>
Date:   Tue, 20 Nov 2018 19:14:27 +0000

Merge pull request #23 from Demonstrandum/devel

Seperate code to appropriate files.
Diffstat:
Mpublic/new.js | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apublic/poll.css | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apublic/stats.js | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpublic/styles.css | 37+++++++++++++++++++++++++++++++++++++
Mviews/index.erb | 144-------------------------------------------------------------------------------
Mviews/poll.erb | 241+------------------------------------------------------------------------------
6 files changed, 372 insertions(+), 383 deletions(-)

diff --git a/public/new.js b/public/new.js @@ -66,3 +66,146 @@ $('document').ready(() => { return false; }); }); + + +let editing = NaN; + +const add_list = option => { + $('#addition').val(''); + let adder = $(".option").detach(); + $("#options") + .append(` + <li class="option-item"> + <span class="value">${option}</span> + <div class="symbols"> + <i class="option-edit far fa-edit"></i> + <i class="option-remove fas fa-times"></i> + </div> + </li> + `) + .append(adder); + $('#addition').focus().select(); +}; + + +$('#options').on('click', '.option-remove', e => { + const li = $(e.target).closest('li'); + li.slideUp(100, function() { $(this).remove(); }); + console.warn(li[0], 'Element been removed!'); +}); + +const save = () => { + const inputs = $('#options li').children('input'); + let values = Array(...$('#options li span') + .filter((i, e) => e.style.display !== 'none') + .map((i, e) => e.innerHTML)); + console.log(values); + + let bad_input = null; + inputs.each(function() { + const value = this.value; + if (value.length === 0) { + issue(ISSUE.FATAL, `Cannot leave new value empty. Delete it if you don't want it.`); + bad_input = $(this); + return; + } + if (values.includes(value)) { + issue(ISSUE.FATAL, `Cannot have duplicate (primary) options/choices!`); + bad_input = $(this); + return; + } + + $(this).parent('li').removeClass('dark-option'); + $(this).siblings('span').show().text(value); + $(this).siblings('.symbols').find('.option-edit') + .removeClass('fa-save') + .addClass('fa-edit'); + this.remove(); + }); + + if (bad_input !== null) { + bad_input.attention(); + return false; + } + + let value = $("#addition").val().trim(); + + if (values.includes(value)) { + issue(ISSUE.FATAL, `Cannot have duplicate (primary) options/choices!`); + $('#addition').attention(); + return false; + } + + + if (value.length > 0) add_list(value); + return true; +}; + +$(document).on('click', e => { + if (!( $(e.target).hasClass('editor') + || $(e.target).hasClass('option-item') + || $(e.target).hasClass('fa-save') + || $(e.target).hasClass('value') + )) { + save(); + editing = NaN; + } +}); + +const edit_mode = li => { + const span = li.find('span'); + span.hide(); + li.addClass('dark-option'); + li.find('.option-edit') + .removeClass('fa-edit') + .addClass('fa-save'); + li.prepend(`<input class="editor" type="text" value="${span.text()}" />`); + li.find('input').focus().select(); +}; + +$('#options').on('click', 'li', e => { + if (e.target.nodeName === 'INPUT') return; + let li = $(e.currentTarget); + save(); + + if (editing !== li.index()) { + editing = li.index(); + edit_mode(li); + } else { + editing = NaN; + } +}); + +$(document).on('keypress', '.editor', key => { + if (key.which === 13 || key.which === 10) { + key.preventDefault(); + save(); + editing = NaN; + } +}); + +$("#addition").on('keypress', key => { + let value = $("#addition").val().trim(); + if (key.which === 13 || key.which === 10) { + if (value.length === 0) { + return $('#addition').attention(); + } + key.preventDefault(); + save(); + } +}); +$('.add-option').click(() => { + if ($('#addition').val().length === 0) { + $('#addition').attention(); + return; + } + save(); +}); + +$("#title").on('input', () => { + $("#code").val(encodeURIComponent($("#title").val().toLowerCase().replace(/\s|\\|\//g, '-'))); +}); + +$('#code')[0].addEventListener('keyup', e => { + $("#code").val(encodeURIComponent($("#code").val().replace(/[^A-Za-z\d\-\.\!\']/g, '-'))); +}); diff --git a/public/poll.css b/public/poll.css @@ -0,0 +1,93 @@ +table { + width: 100%; + border-collapse:separate; + border-spacing: 0 0.5em; +} + +td, th { + border: none; + padding: 0; + vertical-align: middle; +} +th:not(:first-child) { + width: 4em; +} + +td:not(:first-child), th:not(:first-child) { + text-align: center; +} + +td:first-child { + padding: 0 0 0 10px !important; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +td:last-child { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +tr:not(:first-child) { + border: none; + background: #f5f5f5; + margin: 0; + padding: 0; +} + +tr td:nth-child(2) { + background: #eeeeee; /* Gradient-ish */ +} +tr td:nth-child(3) { + background: #e1e1e1; +} +tr td:nth-child(4) { + background: #d7d7d7; +} +.row { + display: block; + align-items: center; + justify-content: center; +} +@media (min-width:550px) { + .row { + display: flex; + } +} +.row .columns { + height: 100%; + overflow: auto; +} + +#vote { + width: calc(100% - 11em); + float: left; +} +#submit { + width: 11em; + padding: 0; + float: right; +} +.disabled { + user-select: none !important; + cursor: default !important; +} +.caster { + border-color: transparent; + padding: 5px 10px; + background: #f5f5f5; + height: 1.4em; + line-height: 0.7em; + margin: 0; + border-radius: 4px; +} + +.caster:active { + background: #aaa; +} + +.chart-area { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/public/stats.js b/public/stats.js @@ -0,0 +1,97 @@ +Chart.defaults.global.elements.arc.borderColor = '#ddd'; +const ALPHA_RANGE = { + min: 0.08, + max: 1 +} + +const chart_colors = chart => { + const range = ALPHA_RANGE; + const N = chart.data.datasets[0].data.length; + backgrounds = Array(N); + for (let mono = range.min; mono <= range.max; mono += range.max / (N + 1)) + backgrounds[Math.round((mono - range.min) * N / range.max)] = `rgba(0,0,0,${mono})`; + chart.data.datasets[0].backgroundColor = backgrounds; + return backgrounds; +}; + +const chart_empty = chart => { + chart.data.labels = []; + chart.data.datasets[0].data = []; +}; + +const push_data = (chart, label, data) => { + chart.data.labels.push(label); + chart.data.datasets.forEach((dataset) => { + dataset.data.push(data); + }); +} + +$(document).ready(() => { + $('.home').click(() => { + window.location.href = '/'; + }) + for (const canvas of ['#pie', '#bar']) { + const context = $(canvas)[0]; + const dimension = $(canvas).closest('div').innerWidth() - 22; + context.height = dimension; + context.width = dimension; + } +}); + + +// CHARTS: +let pie; +let bar; // Make sure they're global. +$(document).ready(() => { + const pie_context = $('#pie'); + + pie = new Chart(pie_context[0], { + type: 'pie', + data: { + labels: [], + datasets: [{ + label: '№ of Votes', + data: [], + backgroundColor: [], + }] + }, + options: {} + }); + + const bar_context = $('#bar') + bar = new Chart(bar_context[0], { + type: 'horizontalBar', + data: { + labels: [], + datasets: [{ + label: '№ of Votes', + data: [], + backgroundColor: [], + }] + }, + options: { + title: { + display: true, + text: 'Distribution of alternative/other votes.' + }, + scales: { + yAxes: [{ + categoryPercentage: 0.9, + barPercentage: 1.0, + ticks: { + mirror: true, + padding: -10, + } + }], + xAxes: [{ + ticks: { + stepSize: 1 + } + }] + }, + legend: { + display: false, + } + } + }); +}); diff --git a/public/styles.css b/public/styles.css @@ -206,6 +206,43 @@ h3 { text-align: right; } +footer { + margin-top: 2em; + border-top: 1px solid #eee; + display: flex; + align-items: center; + height: 4em; + user-select: none; +} +footer .container { + padding-top: 0; + margin-top: 0; +} +footer .row { + display: flex; + align-items: center; + justify-content: unset; +} +footer .row .left { + margin-right: auto; +} +footer span { + font-weight: bold; + font-size: 2em; + vertical-align: middle; + margin-right: 1em; +} +.home { + padding: 10px 20px; + cursor: pointer; + color: #fff; + text-transform: uppercase; + font-size: 11px; + background: #000; + border-radius: 4px; + float: right +} + .url { word-break: break-all; display: block; diff --git a/views/index.erb b/views/index.erb @@ -73,149 +73,5 @@ <script src="/main.js"></script> <script src="/new.js"></script> - <script> - let editing = NaN; - - const add_list = option => { - $('#addition').val(''); - let adder = $(".option").detach(); - $("#options") - .append(` - <li class="option-item"> - <span class="value">${option}</span> - <div class="symbols"> - <i class="option-edit far fa-edit"></i> - <i class="option-remove fas fa-times"></i> - </div> - </li> - `) - .append(adder); - $('#addition').focus().select(); - }; - - - $('#options').on('click', '.option-remove', e => { - const li = $(e.target).closest('li'); - li.slideUp(100, function() { $(this).remove(); }); - console.warn(li[0], 'Element been removed!'); - }); - - const save = () => { - const inputs = $('#options li').children('input'); - let values = Array(...$('#options li span') - .filter((i, e) => e.style.display !== 'none') - .map((i, e) => e.innerHTML)); - console.log(values); - - let bad_input = null; - inputs.each(function() { - const value = this.value; - if (value.length === 0) { - issue(ISSUE.FATAL, `Cannot leave new value empty. Delete it if you don't want it.`); - bad_input = $(this); - return; - } - if (values.includes(value)) { - issue(ISSUE.FATAL, `Cannot have duplicate (primary) options/choices!`); - bad_input = $(this); - return; - } - - $(this).parent('li').removeClass('dark-option'); - $(this).siblings('span').show().text(value); - $(this).siblings('.symbols').find('.option-edit') - .removeClass('fa-save') - .addClass('fa-edit'); - this.remove(); - }); - - if (bad_input !== null) { - bad_input.attention(); - return false; - } - - let value = $("#addition").val().trim(); - - if (values.includes(value)) { - issue(ISSUE.FATAL, `Cannot have duplicate (primary) options/choices!`); - $('#addition').attention(); - return false; - } - - - if (value.length > 0) add_list(value); - return true; - }; - - $(document).on('click', e => { - if (!( $(e.target).hasClass('editor') - || $(e.target).hasClass('option-item') - || $(e.target).hasClass('fa-save') - || $(e.target).hasClass('value') - )) { - save(); - editing = NaN; - } - }); - - const edit_mode = li => { - const span = li.find('span'); - span.hide(); - li.addClass('dark-option'); - li.find('.option-edit') - .removeClass('fa-edit') - .addClass('fa-save'); - li.prepend(`<input class="editor" type="text" value="${span.text()}" />`); - li.find('input').focus().select(); - }; - - $('#options').on('click', 'li', e => { - if (e.target.nodeName === 'INPUT') return; - let li = $(e.currentTarget); - save(); - - if (editing !== li.index()) { - editing = li.index(); - edit_mode(li); - } else { - editing = NaN; - } - }); - - $(document).on('keypress', '.editor', key => { - if (key.which === 13 || key.which === 10) { - key.preventDefault(); - save(); - editing = NaN; - } - }); - - $("#addition").on('keypress', key => { - let value = $("#addition").val().trim(); - if (key.which === 13 || key.which === 10) { - if (value.length === 0) { - return $('#addition').attention(); - } - key.preventDefault(); - save(); - } - }); - $('.add-option').click(() => { - if ($('#addition').val().length === 0) { - $('#addition').attention(); - return; - } - save(); - }); - </script> - <script> - $("#title").on('input', () => { - $("#code").val(encodeURIComponent($("#title").val().toLowerCase().replace(/\s|\\|\//g, '-'))); - }); - - $('#code')[0].addEventListener('keyup', e => { - $("#code").val(encodeURIComponent($("#code").val().replace(/[^A-Za-z\d\-\.\!\']/g, '-'))); - }); - </script> </body> </html> diff --git a/views/poll.erb b/views/poll.erb @@ -16,107 +16,7 @@ } </style> <% end %> - <style media="screen"> - table { - width: 100%; - border-collapse:separate; - border-spacing: 0 0.5em; - } - - td, th { - border: none; - padding: 0; - vertical-align: middle; - } - th:not(:first-child) { - width: 4em; - } - - td:not(:first-child), th:not(:first-child) { - text-align: center; - } - - td:first-child { - padding: 0 0 0 10px !important; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - } - - td:last-child { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - } - - tr:not(:first-child) { - border: none; - background: #f5f5f5; - margin: 0; - padding: 0; - } - - tr td:nth-child(2) { - background: #eeeeee; /* Gradient-ish */ - } - tr td:nth-child(3) { - background: #e1e1e1; - } - tr td:nth-child(4) { - background: #d7d7d7; - } - .row { - display: block; - align-items: center; - justify-content: center; - } - @media (min-width:550px) { - .row { - display: flex; - } - } - .row .columns { - height: 100%; - overflow: auto; - } - - #vote { - width: calc(100% - 11em); - float: left; - } - #submit { - width: 11em; - padding: 0; - float: right; - } - .disabled { - user-select: none !important; - cursor: default !important; - } - .caster { - border-color: transparent; - padding: 5px 10px; - background: #f5f5f5; - height: 1.4em; - line-height: 0.7em; - margin: 0; - border-radius: 4px; - } - - .caster:active { - background: #aaa; - } - - .chart-area { - display: flex; - align-items: center; - justify-content: center; - } - canvas { - /*border: 1px solid #f9f9f9; - background: #fff; - border-radius: 4px; - filter: drop-shadow(0 5px 10px rgba(0, 0, 0, 0.2));*/ - } - </style> + <link rel="stylesheet" type="text/css" href="/poll.css" /> </head> <body> <div class="container"> @@ -148,45 +48,6 @@ <br /> </div> - <style> - footer { - margin-top: 2em; - border-top: 1px solid #eee; - display: flex; - align-items: center; - height: 4em; - user-select: none; - } - footer .container { - padding-top: 0; - margin-top: 0; - } - footer .row { - display: flex; - align-items: center; - justify-content: unset; - } - footer .row .left { - margin-right: auto; - } - footer span { - font-weight: bold; - font-size: 2em; - vertical-align: middle; - margin-right: 1em; - } - .home { - padding: 10px 20px; - cursor: pointer; - color: #fff; - text-transform: uppercase; - font-size: 11px; - background: #000; - border-radius: 4px; - float: right - } - </style> - <footer> <div class="container"> <div class="row"> @@ -206,105 +67,7 @@ </footer> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js" charset="utf-8"></script> - <script type="text/javascript"> - Chart.defaults.global.elements.arc.borderColor = '#ddd'; - const ALPHA_RANGE = { - min: 0.08, - max: 1 - } - - const chart_colors = chart => { - const range = ALPHA_RANGE; - const N = chart.data.datasets[0].data.length; - backgrounds = Array(N); - for (let mono = range.min; mono <= range.max; mono += range.max / (N + 1)) - backgrounds[Math.round((mono - range.min) * N / range.max)] = `rgba(0,0,0,${mono})`; - chart.data.datasets[0].backgroundColor = backgrounds; - return backgrounds; - }; - - const chart_empty = chart => { - chart.data.labels = []; - chart.data.datasets[0].data = []; - }; - - const push_data = (chart, label, data) => { - chart.data.labels.push(label); - chart.data.datasets.forEach((dataset) => { - dataset.data.push(data); - }); - } - - $(document).ready(() => { - $('.home').click(() => { - window.location.href = '/'; - }) - for (const canvas of ['#pie', '#bar']) { - const context = $(canvas)[0]; - const dimension = $(canvas).closest('div').innerWidth() - 22; - context.height = dimension; - context.width = dimension; - } - }); - </script> - <script type="text/javascript"> - // CHARTS: - let pie; - let bar; // Make sure they're global. - $(document).ready(() => { - const pie_context = $('#pie'); - - pie = new Chart(pie_context[0], { - type: 'pie', - data: { - labels: [], - datasets: [{ - label: '№ of Votes', - data: [], - backgroundColor: [], - }] - }, - options: {} - }); - - const bar_context = $('#bar') - bar = new Chart(bar_context[0], { - type: 'horizontalBar', - data: { - labels: [], - datasets: [{ - label: '№ of Votes', - data: [], - backgroundColor: [], - }] - }, - options: { - title: { - display: true, - text: 'Distribution of alternative/other votes.' - }, - scales: { - yAxes: [{ - categoryPercentage: 0.9, - barPercentage: 1.0, - ticks: { - mirror: true, - padding: -10, - } - }], - xAxes: [{ - ticks: { - stepSize: 1 - } - }] - }, - legend: { - display: false, - } - } - }); - }); - </script> + <script src="/stats.js"></script> <script src="/main.js"></script> <script src="/poller.js"></script>