veto

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

commit 0c1ca6eeed04fb6296396610be2978e0b5ae0e40
parent f72ef76487c75395bd3218ac430c1254c0a624ab
Author: Fredrik <moi@knutsen.co>
Date:   Thu, 25 Oct 2018 00:36:17 +0100

Merge pull request #2 from Demonstrandum/devel

CSS and general features 
Diffstat:
MREADME.md | 53++++++++++++++++++++++++++++++++++++++++++++++++++++-
Apublic/main.js | 38++++++++++++++++++++++++++++++++++++++
Mpublic/new.js | 39++++++++++++++++++++++++++++++++++++---
Mpublic/styles.css | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mserver.rb | 30++++++++++++++++++++++++++----
Mviews/index.erb | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mviews/poll.erb | 5+++--
Mviews/share.erb | 3++-
8 files changed, 401 insertions(+), 37 deletions(-)

diff --git a/README.md b/README.md @@ -1,5 +1,56 @@ # Veto Simple polling site writen in Ruby, using Sinatra. -## HELP +# Running Locally +First of all, one need the Ruby programming language installed. One can install this from the official [Ruby Website](https://www.ruby-lang.org/en/) or one'll most likely have it available through one's main package manager. + +One need to use Ruby Gems to install Bundler, gems should come with one's installation of Ruby. +In one's terminal, type: +```shell +gem install bundler +``` +Make sure one adds the .gem bin/ path to one's `$PATH` variable in one's main shell. +e.g. in one's `~/.bashrc` add +```shell +export PATH="$(ruby -r rubygems -e 'puts Gem.user_dir')/bin:$PATH" +``` +Then restart one's shell session, by typing `bash` or whatever shell one uses. + +One needs to make sure one has `eventmachine` installed, for the `thin` server to work. +One does this by running: +```shell +gem install eventmachine +``` +If one gets errors about failing to build native extensions, read up in libraries needed to build (g++, gcc, musl-dev, make libstdc++, etc). Read about it here https://github.com/eventmachine/eventmachine/wiki/Building-EventMachine. + +Now one clones this repo, or fork it, depending on whether one wants to simply run the server, or contribute to the site, respectively. Either simply type: +```shell +git clone https://github.com/Demonstrandum/Veto.git +``` +in one's shell to clone it, or fork it by pressing the fork button on this webpage, then clone that fork instead, for which one can later submit a pull request. One now wants to enter this directory, one does this by simply typing: +```shell +cd Veto +``` + +--- + +After this, one would want to install the gems from the `Gemfile` file, this one does by running: +```shell +bundle install +``` +whilst in the root directory of this repo. + +--- + +One should now be ready to run the server locally! Whilst in the Veto/ directory, kindly type: +```shell +bundle exec ruby server.rb +``` +One should see Sinatra take the stage with backup from Thin, (their words). If not, and one's getting issues with `eventmachine` and/or trouble finding the Thin server, please revisit the steps above concerning `eventmachine` installation. + +Otherwise, one should be fine, now one can access [localhost:8080](http://localhost:8080/), and one can make any changes locally. One only needs to restart the server if one has made changes to the `server.rb` file, otherwise any changes to any other .erb, .js, .css files, etc. should not require a restart. + + + +## Welp Someone help me make it look pretty. I'm too lazy to write heaps of CSS again diff --git a/public/main.js b/public/main.js @@ -0,0 +1,38 @@ +const ISSUE = Object.freeze({ + "FATAL": 'Error', + "WARN": 'Warning', + "INFO": 'Information' +}); + +let issue_n = 0; +const issue = (type, message) => { + issue_n++; + if ($('body').has('.issues').length > 0) { + $('.issues').append(` + <div class="issue ${type.toLowerCase()}"> + <h3>${type}</h3> + <p>${message}</p> + </div> + `); + const added = $($('.issues .issue').get(-1)); + + setTimeout((cont = added) => { + cont.addClass('show'); + }, 10); + + setTimeout((cont = added) => { + cont.fadeOut(700, () => cont.remove()); + }, 5000); + } else { + $('body').append(`<div class="issues"></div>`); + issue(type, message); + } +}; + +jQuery.fn.attention = function() { + this.each(function(i) { + $(this).select().focus().addClass('problem').effect('shake'); + setTimeout(() => $(this).removeClass('problem'), 1000); + }); + return this; +}; diff --git a/public/new.js b/public/new.js @@ -1,4 +1,6 @@ $('document').ready(() => { + $('.url').text(`${window.location.protocol}//${window.location.host}/poll/`) + $('#create').submit(() => { $.ajax({ type: 'POST', @@ -17,9 +19,40 @@ $('document').ready(() => { return false; }); $('#submit-poll').click(() => { - if (Array(...$('#options li').map((i, e) => e.innerHTML)).length === 0) { - $('#addition').attr('placeholder', 'Add at least 1 option.').select().focus(); - } else { + let polls = []; + $.ajax({ + async: false, + type: 'GET', + url: '/polls.json', + success: data => { + polls = data; + console.info(polls); + } + }); + + // Verify inputs! + if ($('#title').val().trim().length === 0) { + $('#title').attention(); + issue(ISSUE.WARN, ` + The title of your poll cannot be left blank. + `); + } else if ($('#code').val().trim().length === 0) { + $('#code').attention(); + issue(ISSUE.FATAL, ` + The poll link name (the name in the URL) cannot be left blank. + `); + } else if (!$('#other').is(':checked') && Array(...$('#options li').map((i, e) => e.innerHTML)).length === 0) { + $('#addition').attr('placeholder', 'Add at least 1 option.').attention(); + issue(ISSUE.WARN, ` + Unless you allow for 'other' options, + you need to add at least one primary option. + `); + } else if (polls.includes($('#code').val())) { + issue(ISSUE.FATAL, ` + The poll URL: '${$('#code').val()}' has already been taken. + `); + $('#code').attention(); + } else { // All checks passed $('#create').submit(); setTimeout(() => { window.location.href = '/share/' + $('#code').val(); diff --git a/public/styles.css b/public/styles.css @@ -0,0 +1,169 @@ +@import url('https://fonts.googleapis.com/css?family=Rubik:400,400i,500'); +@import url('//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'); + +html, body { + margin: 0; + padding: 0; + font-size: 16px; + font-family: 'Rubik', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif; +} + +body { + overflow-y: scroll; +} + +.container { + margin-top: 30px; +} + +ul { + padding-left: 40px; + margin: 10px 0; +} + +#options { + margin: 20px 0; +} + +#options li { + margin-top: 20px; +} + +label { + font-weight: normal; +} + +input[type=button], button { + padding: 0; + margin: 0 15px; + border: none; + border-radius: 0; + border-bottom: 1px solid #aaa; + height: 36px; + letter-spacing: normal; + font-weight: normal; +} + +input[type=text] { + font-size: 1em; + padding: 0; + border: none; + border-radius: 0px; + border-bottom: 1px solid #aaa; + transition: all .15s ease; +} + +input[type=text]:focus { + outline: none; + border: none; + border-bottom: 1px solid #aaa; +} + +input[type=checkbox] { display:none; } +input[type=checkbox] + label:before { + font-family: FontAwesome; + display: inline-block; +} + +input[type=checkbox] + label:before { content: "\f096"; } +input[type=checkbox] + label:before { letter-spacing: 10px; } + +input[type=checkbox]:checked + label:before { content: "\f046"; } +input[type=checkbox]:checked + label:before { letter-spacing: 8px; } + +#code { + color: #aaa; +} + +.problem:focus { + outline: none; + border: 1px solid #900; + background: #ffd8d2; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 500; +} + +h3 { + font-size: 1.57em; +} + +.in { + margin-left: 0; + margin-top: -7px; +} + +.in input { + width: 100%; +} + +#addition { + margin-left: 22px; +} + +.right { + text-align: right; +} + +.url { + font-size: 1em; + color: #aaa; +} + +.issues { + position: absolute; + right: 0; + bottom: 0; + overflow: hidden; +} + +.issues div > * { + padding: 10px 15px; + margin: 0; +} + +.issues div { + height: 0; + width: 15em; + opacity: 0; + margin: 10px; + border-radius: 5px; + border-left: 8px solid; + transition: all .2s ease-out; + overflow: hidden; +} + +.issues p { + font-size: 0.8em; + width: auto; + border-top: 2px solid; + border-bottom: 2px solid; +} + +.issues .show { + height: auto; + opacity: 1; +} + +.issues .error { + background: #ffc8b3; + border-color: #ff5436; + color: #800; +} + +.issues .error p { + background: #ffd8d2; + border-color: #faa; +} + +.issues .warning { + background: #ffeab3; + border-color: #ffc136; + color: #885a00; +} + +.issues .warning p { + background: #fff0d2; + border-color: #ffe2aa; +} diff --git a/server.rb b/server.rb @@ -9,6 +9,8 @@ set :server, %w{ thin } set :port, 8080 enable :sessions +before { request.path_info.sub! %r{/$}, "" } + $INDIFFERENT = proc do |h, k| case k when String then sym = k.to_sym; h[sym] if h.key?(sym) @@ -17,7 +19,7 @@ $INDIFFERENT = proc do |h, k| end $polls = { - 'this-is-a-poll-name' => { + :'this-is-a-poll-name' => { :name => 'This is a poll name', :votes => {}, :alt => true, @@ -62,8 +64,24 @@ def make_poll code, name, alt save_json end +$HEAD_TAGS = <<-HTML + <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css" /> + <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" /> + <link rel="stylesheet" type="text/css" href="/styles.css" /> + <script + src="https://code.jquery.com/jquery-3.3.1.min.js" + integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" + crossorigin="anonymous"> + </script> + <script + src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" + integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" + crossorigin="anonymous"> + </script> +HTML + get '/' do - erb :index + erb :index, :locals => {:head_tags => $HEAD_TAGS} end get '/poll' do @@ -71,7 +89,7 @@ get '/poll' do end get '/share/:code' do - erb :share + erb :share, :locals => {:head_tags => $HEAD_TAGS} end post '/new' do @@ -96,7 +114,7 @@ get '/poll/:poll' do return "This poll has not been created/does not exist!" end - local = {:code => params[:poll]} + local = {:code => params[:poll], :head_tags => $HEAD_TAGS} local.merge! $polls[params[:poll]] erb :poll, :locals => local end @@ -120,6 +138,10 @@ get '/poll/:poll/votes.json' do $polls[params[:poll]][:votes].to_json end +get '/polls.json' do + $polls.keys.map(&:to_s).to_json +end + get '/exported.json' do $polls.to_json end diff --git a/views/index.erb b/views/index.erb @@ -1,44 +1,74 @@ <!DOCTYPE html> <html> <head> - <title>Lopper</title> + <title>Veto - Create Poll</title> + <%= head_tags %> </head> <body> - <h1>Make a poll</h1> + <div class="container"> + <div class="row"> + <h1>Create a poll</h1> + </div> - <form id="create" action="/new" method="POST"> - <label>Title</label> - <input required type="text" name="title" id="title" placeholder="Name of your poll"> + <form id="create" action="/new" method="POST"> + <div class="row"> + <div class="six columns"> + Poll Title + </div> + <div class="six columns in"> + <input type="text" name="title" id="title" autocomplete="off" placeholder="Name of your poll"> + </div> + </div> - <br /> - <label>Name in URL</label> - <input required type="text" name="code" id="code" placeholder="Code name"> + <div class="row"> + <div class="three columns"> + Poll URL name + </div> + <div class="three columns right"> + <span class="url"><span> + </div> + <div class="six columns in"> + <input type="text" name="code" id="code" autocomplete="off" placeholder=""> + </div> + </div> - <br /> - <label>Allow 'other' option?</label> - <input type="checkbox" id="other" name="other"> + <div class="row"> + <div class="twelve columns" style="margin-bottom: 1em"> + <input type="checkbox" checked id="other" name="other"> + <label for="other" style="cursor: pointer">Allow 'other' option?</label> + </div> + </div> - <br /> - Primary Options: - <ul id="options"> - <input type="text" name="addition" id="addition" placeholder="..."> - </ul> - <br /> - <input id="submit-poll" type="submit" name="submit" value="Make Poll"> - </form> + <div class="row"> + <div class="twelve columns"> + Primary Options/Choices: + <ul id="options"> + <div class="option"> + <input type="text" autocomplete="off" name="addition" id="addition" placeholder="..."> + <input type="button" class="add-option" value="Add option"> + </div> + </ul> + </div> + </div> - <script src="/jquery.min.js"></script> + <br /> + <input id="submit-poll" type="submit" name="submit" value="Make Poll"> + </form> + </div> + + + <script src="/main.js"></script> <script src="/new.js"></script> <script> const add_list = option => { - let addition = $("#addition").detach(); - addition.val(''); + $('#addition').val(''); + let adder = $(".option").detach(); $("#options") .append(`<li>${option}</li>`) - .append(addition); - addition.focus().select(); + .append(adder); + $('#addition').focus().select(); }; $("#addition").on('keypress', key => { @@ -46,9 +76,28 @@ let values = Array(...$('#options li').map((i, e) => e.innerHTML)); if (key.which === 13) { key.preventDefault(); - if (value.length > 0 && !values.includes(value)) - add_list(value); + if (value.length === 0) { + return $('#addition').attention(); + } + if (values.includes(value)) { + issue(ISSUE.FATAL, `Cannot have duplicate (primary) options/choices!`); + return $('#addition').attention(); + } + return add_list(value); + } + }); + $('.add-option').click(() => { + let value = $("#addition").val().trim(); + let values = Array(...$('#options li').map((i, e) => e.innerHTML)); + if (value.length === 0) { + return $('#addition').attention(); } + if (values.includes(value)) { + issue(ISSUE.FATAL, `Cannot have duplicate (primary) options/choices!`); + return $('#addition').attention(); + } + + return add_list(value); }); </script> <script> diff --git a/views/poll.erb b/views/poll.erb @@ -1,7 +1,8 @@ <!DOCTYPE html> <html> <head> - <title><%= name %> | Lopper</title> + <title><%= name %> | Veto</title> + <%= head_tags %> </head> <body> <h1><%= name %></h1> @@ -22,7 +23,7 @@ </style> <% end %> - <script src="/jquery.min.js"></script> + <script src="/main.js"></script> <script src="/poller.js"></script> </body> </html> diff --git a/views/share.erb b/views/share.erb @@ -2,6 +2,7 @@ <html> <head> <title>Share Poll</title> + <%= head_tags %> </head> <body> <div> @@ -11,7 +12,7 @@ </div> - <script src="/jquery.min.js"></script> + <script src="/main.js"></script> <script> $("#share").append(`${window.location.protocol}//${window.location.host}/poll/<%= params[:code] %>`) </script>