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:
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>