veto

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

commit 3c66bb7323452af65dd43e256e03ed0f7444cdac
parent ee0512070d2c443e17da9dfbecf2d96949079f0b
Author: Fredrik <moi@knutsen.co>
Date:   Wed, 31 Oct 2018 02:18:40 +0000

Merge pull request #9 from Demonstrandum/devel

Finish adding charts, URL encoding issues and better share page.
Diffstat:
Mpublic/poller.js | 28++++++++++++++++++++++------
Mserver.rb | 22+++++++++++++---------
Mviews/index.erb | 2+-
Mviews/poll.erb | 49++++++++++++++++++++++++++++++++++++++++++-------
Mviews/share.erb | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
5 files changed, 176 insertions(+), 27 deletions(-)

diff --git a/public/poller.js b/public/poller.js @@ -58,6 +58,9 @@ const total_other = votes => ( const update_votes = () => { chart_empty(pie); const votes = get_votes(POLL_CODE); + const enough_other = (Object.values(votes).filter(v => !v.primary)).length > 1; + if (Object.values(votes).filter(v => v.primary).length === 0) + $('.primary').css({display: 'none'}); $('#primary').empty(); $('#primary').append(` <tr> @@ -67,6 +70,7 @@ const update_votes = () => { <th>Cast</th> </tr> `); + chart_empty(bar); $('#other').empty(); $('#other').append(` <tr> @@ -83,23 +87,36 @@ const update_votes = () => { <tr> <td class="vote-name">${name}</td> <td class="number" >${votes[name].number}</td> - <td class="precent">${dp(100 * votes[name].number / total_vote(votes))}</td> - <td class="cast"><button class="caster" name="${name}" onclick="cast_button('${name}')">Vote</button></td> + <td class="percent">${dp(100 * votes[name].number / total_vote(votes))}</td> + <td class="cast"><button class="caster" name="${name}" onclick="cast_button('${name.replace(/\'/g, '\\\'')}')">Vote</button></td> </tr> `); } else { + push_data(bar, name, votes[name].number) $('#other').append(` <tr> <td class="vote-name">${name}</td> <td class="number" >${votes[name].number}</td> - <td class="precent">${dp(100 * votes[name].number / total_vote(votes))}</td> - <td class="cast"><button class="caster" name="${name}" onclick="cast_button('${name}')">Vote</button></td> + <td class="percent">${dp(100 * votes[name].number / total_vote(votes))}</td> + <td class="cast"><button class="caster" name="${name}" onclick="cast_button('${name.replace(/\'/g, '\\\'')}')">Vote</button></td> </tr> `) } } - if (other_allowed) + if (other_allowed && enough_other) { + $('#bar').show(); push_data(pie, 'Other', Math.round(total_other(votes))); + bar.options.scales.xAxes[0].ticks.min = -1 + Math.min(...( + Object.values(votes) + .filter(v => !v.primary) + .map(v => v.number) + )); + bar.update(); + } + if (!enough_other) { + $('#bar').hide() + } + chart_colors(pie); pie.update(); }; @@ -120,7 +137,6 @@ $('document').ready(() => { url: POLL_CODE + '/has-voted', async: false, success: data => { - console.info('Has already voted?: ', data); if (data === "true") disable_vote(); } }); diff --git a/server.rb b/server.rb @@ -22,11 +22,12 @@ end def make_poll code, name, alt alt = alt.to_s == 'true' poll = { - :code => code, - :name => name, - :votes => {}, - :alt => alt, - :voters => [] + :code => code, + :name => name, + :votes => {}, + :alt => alt, + :voters => [], + :created => Time.now } POLLS.insert_one poll end @@ -72,6 +73,7 @@ get '/share/:code' do end post '/new' do + params[:code] = URI.decode params[:code] return nil if poll_exist? params[:code] make_poll( @@ -84,7 +86,8 @@ post '/new' do :"$set" => { :"votes.#{option}" => { :number => 0, - :primary => true + :primary => true, + :date => Time.now } } }) @@ -107,12 +110,13 @@ post '/poll/:poll/cast' do return nil if request.ip != '::1' && POLLS.find(:"$and" => [{:code => params[:poll]}, {:voters => request.ip}]).to_a.size > 0 POLLS.update_one({:code => params[:poll]}, {:"$push" => {:voters => request.ip}}) unless request.ip == '::1' - if POLLS.find({ :"votes.#{params[:vote]}" => {"$exists": true} }) + if POLLS.find({ :"votes.#{params[:vote]}" => {"$exists": true} }).to_a.size > 0 POLLS.update_one({:code => params[:poll]}, { :"$inc" => { :"votes.#{params[:vote]}.number" => 1 } }) else - POLLS.update_one({:code => params[:poll]}, { :"$set" => { :"vote.#{params[:vote]}" => { + POLLS.update_one({:code => params[:poll]}, { :"$set" => { :"votes.#{params[:vote]}" => { :number => 1, - :primary => false + :primary => false, + :date => Time.now }}}) end end diff --git a/views/index.erb b/views/index.erb @@ -180,7 +180,7 @@ </script> <script> $("#title").on('input', () => { - $("#code").val(encodeURIComponent($("#title").val().toLowerCase().replace(/\s|\//g, '-'))); + $("#code").val(encodeURIComponent($("#title").val().toLowerCase().replace(/\s|\\|\//g, '-'))); }); $('#code')[0].addEventListener('keyup', e => { diff --git a/views/poll.erb b/views/poll.erb @@ -108,7 +108,7 @@ <div class="container"> <h1><%= name %></h1> - <div class="row"> + <div class="row primary"> <div class="seven columns"> <table id="primary" class="live"></table> </div> @@ -162,7 +162,6 @@ chart.data.datasets.forEach((dataset) => { dataset.data.push(data); }); - chart_colors(chart); } $(document).ready(() => { @@ -175,23 +174,59 @@ }); </script> <script type="text/javascript"> - // PIE CHART: + // CHARTS: let pie; + let bar; // Make sure they're global. $(document).ready(() => { - const context = $('#pie'); + const pie_context = $('#pie'); - pie = new Chart(context, { + pie = new Chart(pie_context[0], { type: 'pie', data: { labels: [], datasets: [{ - label: '# of Votes', + label: '№ of Votes', data: [], backgroundColor: [], }] }, - options: { + 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/views/share.erb b/views/share.erb @@ -3,18 +3,112 @@ <head> <title>Share Poll</title> <%= head_tags %> + <style media="screen"> + body { + overflow: hidden; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + } + h3 { + margin: 10px 0; + } + p { + margin: 10px 0; + } + a, i { + padding: 5px 10px; + filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.1)); + border: 1px solid #eee; + border-radius: 4px; + background: #fff; + color: #777; + transition: all .1s ease; + } + a:hover { + color: #444; + } + + i { + cursor: grab; + color: #aaa; + padding: 0.43em 10px; + margin-left: 6px; + position: relative; + display: inline-block; + } + i:hover { + color: #666; + } + + i span { + visibility: hidden; + width: 120px; + background: rgba(0,0,0,0.8); + color: #fff; + text-align: center; + padding: 5px 0; + border-radius: 6px; + + position: absolute; + z-index: 1; + width: 120px; + bottom: 100%; + left: 50%; + margin-left: -60px; + font-family: Rubik; + font-size: 0.7em; + } + + i:hover span { + visibility: visible; + } + + i span::after { + content: " "; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: rgba(0,0,0,0.8) transparent transparent transparent; + } + i:active { + cursor: copy; + } + input { + position: absolute; + right: -300vw; + } + </style> </head> <body> <div> - <p>Share: - <a id="share" href="/poll/<%= params[:code] %>"></a> - </p> + <h3>Share</h3> + <p>Share the link with others, press the copy-paste button.</p> + <a id="share" href=""><%= params[:code] %></a> + <i class="far fa-copy"><span>Copy to Clipboard.</span></i> + <input type="text" value="" /> </div> <script src="/main.js"></script> <script> - $("#share").append(`${window.location.protocol}//${window.location.host}/poll/<%= params[:code] %>`) + $('i').click(() => { + const text = $('#share').text(); + $('input').val(text); + $('input')[0].select(); + document.execCommand("copy"); + $('i span').text('Copied!'); + setTimeout(() => $('i span').text('Copy to Clipboard.'), 700); + }); + const code = $('#share').text(); + $("#share") + .attr('href', `/poll/${encodeURIComponent(code)}`) + .text(`${window.location.protocol}//${window.location.host}/poll/${encodeURIComponent(code)}`) </script> </body> </html>