Merge pull request #33 from ZeusWPI/layout

Layout
This commit is contained in:
benji 2017-01-16 22:28:02 +01:00 committed by GitHub
commit e35fe64b9e
31 changed files with 660 additions and 86 deletions

View file

@ -95,3 +95,4 @@ gem 'high_voltage', '~> 2.4.0'
gem 'airbrake'
gem 'bootstrap-sass', '~> 3.3.5'
gem 'react-rails'

View file

@ -46,6 +46,10 @@ GEM
autoprefixer-rails (6.0.2)
execjs
json
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
bcrypt (3.1.10)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
@ -79,6 +83,7 @@ GEM
execjs
coffee-script-source (1.9.1.1)
colorize (0.7.7)
connection_pool (2.2.1)
coveralls (0.8.2)
json (~> 1.8)
rest-client (>= 1.6.8, < 2)
@ -205,6 +210,13 @@ GEM
thor (>= 0.18.1, < 2.0)
rake (10.4.2)
rdoc (4.2.0)
react-rails (1.10.0)
babel-transpiler (>= 0.7.0)
coffee-script-source (~> 1.8)
connection_pool
execjs
railties (>= 3.2)
tilt
responders (2.1.0)
railties (>= 4.2.0, < 5)
rest-client (1.8.0)
@ -313,6 +325,7 @@ DEPENDENCIES
omniauth-oauth2
purecss-rails
rails (= 4.2.4)
react-rails
rspec-rails
sass-rails (~> 5.0)
sdoc (~> 0.4.0)
@ -324,4 +337,4 @@ DEPENDENCIES
web-console (~> 2.0)
BUNDLED WITH
1.10.6
1.13.7

View file

@ -12,12 +12,16 @@
//
//= require jquery
//= require jquery_ujs
//= require bootstrap-sprockets
//= require dataTables/jquery.dataTables
//= require dataTables/extras/dataTables.responsive
//= require dataTables/jquery.dataTables
//= require select2
//= require jquery-dateFormat
//= require turbolinks
//= require react
//= require react_ujs
//= require components
//= require_tree .
ready = function() {

View file

@ -0,0 +1 @@
//= require_tree ./components

View file

@ -0,0 +1,213 @@
{ button, div, form, h3, input, option, select } = React.DOM
Action = React.createFactory React.createClass
buttonClass: (b) ->
{ giving } = @props
c = ['btn', 'btn-default']
c.push 'active' if b == giving
c.join ' '
onClick: (b) ->
=>
@props.setAction b
render: ->
{ giving } = @props
div className: 'btn-group btn-group-lg',
button type: 'button', className: @buttonClass(true), onClick: @onClick(true),
'Give Money'
button type: 'button', className: @buttonClass(false), onClick: @onClick(false),
'Request Money'
Amount = React.createFactory React.createClass
onChange: (ref) ->
@props.setAmount ref.target.value
format: (ref) ->
t = ref.target
t.value = parseFloat(t.value).toFixed(2) if t.value
render: ->
div className: 'row',
div className: 'col-xs-4',
div className: 'input-group',
div className: 'input-group-addon', ''
input {
className: 'form-control input-lg',
name: 'transaction[euros]'
onBlur: @format,
onChange: @onChange,
placeholder: '0.00',
type: 'number',
}
Peer = React.createFactory React.createClass
onChange: (ref) ->
@props.setPeer ref.target.value
options: ->
{ peer, peers } = @props
if peer == '' or peers.includes(peer)
[]
else
re = new RegExp peer
peers.filter (s) ->
s.match(re) != null
inputClass: (n) ->
c = ['form-control', 'input-lg']
c.push 'active' if n > 0
c.join ' '
setPeer: (p) ->
=>
@props.setPeer p
render: ->
options = @options()
div className: 'row',
div className: 'col-xs-4',
div className: 'suggestions-wrapper',
input {
className: @inputClass(options.length),
onChange: @onChange,
placeholder: 'Zeus member',
type: 'text',
value: (@props.peer || '')
}
if options.length != 0
div className: 'suggestions',
@options().map (s, i) =>
div className: 'suggestion', key: i, onClick: @setPeer(s),
s
Message = React.createFactory React.createClass
onChange: (ref) ->
@props.setMessage ref.target.value
render: ->
div className: 'row',
div className: 'col-xs-8',
input {
className: 'form-control input-lg',
name: 'transaction[message]',
onChange: @onChange,
placeholder: 'Message'
type: 'text',
}
Submit = React.createFactory React.createClass
render: ->
{ onClick } = @props
div className: 'row',
div className: 'col-xs-4 col-xs-offset-4',
button {
className: 'btn btn-default btn-lg btn-block',
onClick: onClick,
type: 'submit',
}, 'Confirm'
Step = React.createFactory React.createClass
render: ->
{ error } = @props
div className: 'form-step',
div className: 'form-step-counter', @props.step
div className: 'form-step-content',
div className: 'form-step-title',
@props.title,
div className: 'form-step-error',
error
div className: 'clear-both'
@props.children
div className: 'clear-both'
@TransactionForm = React.createClass
getInitialState: ->
step: 1, giving: null, amount: null, peer: null, message: null
setAction: (b) ->
@setState giving: b
@setState step: 2 unless @state.step > 1
setAmount: (a) ->
@setState amount: a
@setState step: 3 unless @state.step > 2
setPeer: (p) ->
@setState peer: p
@setState step: 4 unless @state.step > 3
setMessage: (m) ->
@setState message: m
@setState step: 5 unless @state.step > 4
submit: (e) ->
e.preventDefault()
{ giving, peer } = @state
{ user } = @props
errors = @errors()
if Object.keys(errors).length != 0
return
if giving
debtor = user.name
creditor = peer
else
debtor = peer
creditor = user.name
$('<input />')
.attr('name', 'transaction[debtor]')
.attr('value', debtor)
.attr('type', 'hidden')
.appendTo(@refs.form)
$('<input />')
.attr('name', 'transaction[creditor]')
.attr('value', creditor)
.attr('type', 'hidden')
.appendTo(@refs.form)
@refs.form.submit()
errors: ->
{ amount, giving, message, peer } = @state
{ peers, user } = @props
errors = {}
errors['giving'] = 'Please select an action.' unless giving != null
unless amount
errors['amount'] = 'Please fill in an amount.'
else if parseFloat(amount) <= 0
errors['amount'] = 'Please fill in a positive number.'
unless message && message != ""
errors['message'] = 'Please fill in a message.'
unless peer && peers.includes(peer) && peer != user
errors['peer'] = 'Please select a valid Zeus member.'
errors
render: ->
{ step, amount, giving, message, peer } = @state
{ peers } = @props
errors = @errors()
div id: 'transaction-form',
h3 null, 'Transfer some money'
form ref: 'form', action: '/transactions', acceptCharset: 'UTF-8', method: 'post',
Step step: 1, title: 'What do you want to do?',
Action giving: giving, setAction: @setAction
if step >= 2
Step {
step: 2,
title: "How much do you want to #{if giving then 'give' else 'receive'}?",
error: errors['amount'] if step > 2
},
Amount setAmount: @setAmount
if step >= 3
Step {
step: 3,
title: "Who do you want to #{if giving then 'give it to' else 'receive it from'}?",
error: errors['peer'] if step > 3
},
Peer peer: peer, peers: peers, setPeer: @setPeer
if step >= 4
Step {
step: 4,
title: "Why do you want to #{if giving then 'give' else 'receive'} this?",
error: errors['message'] if step > 4
},
Message setMessage: @setMessage
if step >= 5
Submit onClick: @submit
div className: 'clear-both'

View file

@ -21,6 +21,6 @@
@import "bootstrap-sprockets";
@import "bootstrap";
body {
padding: 30px;
.clear-both {
clear: both;
}

View file

@ -0,0 +1,17 @@
.card-wrapper {
padding: 0 20px;
margin-bottom: 10px;
.card {
box-shadow: 0 1px 3px rgba(0,0,0, 0.12), 0 1px 2px rgba(0,0,0, 0.24);
border-radius: 2px;
h1, .h1, h2, .h2, h3, .h3 {
margin-top: 0;
}
}
}
.padded {
padding: 10px;
}

View file

@ -42,3 +42,76 @@ a.login-button {
.shame-percentage {
text-align: right
}
.transaction {
border-bottom: 1px solid #c7d0d5;
padding: 15px 10px;
font-size: 16px;
color: rgb(45, 54, 59);
.transaction-calendar {
float: left;
text-align: center;
color: #adbac2;
line-height: 1.1;
margin-top: 0.125em;
.transaction-day {
font-size: 1.125em;
display: block;
}
.transaction-month {
font-size: 0.750em;
text-transform: uppercase;
display: block;
}
}
.transaction-block {
padding-left: 3.500em;
.transaction-block-l {
width: 75%;
float: left;
.transaction-message {
margin: 0;
color: #0072ae;
font-size: 1em;
text-transform: uppercase;
}
.transaction-issuer {
display: inline-block;
color: #90949c;
font-size: 70%;
}
}
.transaction-block-r {
width: 25%;
float: left;
text-align: right;
}
}
}
.request {
h4 {
margin: 0;
color: #0072ae;
font-size: 1em;
text-transform: uppercase;
}
}
.notification ,.request {
border-top: 1px solid #c7d0d5;
padding: 15px 10px;
.actions {
text-align: right;
}
}

View file

@ -0,0 +1,7 @@
#content {
padding: 30px;
}
.info-message {
color: rgb(144, 148, 156);
}

View file

@ -0,0 +1,72 @@
$background-color: #f3f3f3;
$border-color: #cfcfcf;
$color: #777;
.menu {
width: 100%;
height: 70px;
background: $background-color;
border-bottom: 1px solid $border-color;
.menu-item {
display: inline-block;
vertical-align: middle;
height: 100%;
padding: 20px;
color: $color;
font-size: 1.875rem;
line-height: 1.4;
font-weight: 300;
&:hover {
text-decoration: none;
}
}
.menu-heading {
font-weight: bold;
font-size: 20px;
}
.menu-list {
display: inline-block;
border-left: 1px solid $border-color;
height: 100%;
margin: 0;
padding: 0;
&.menu-right {
float: right;
.menu-item {
&.menu-item-signout:hover {
background-color: red;
color: white;
}
&:last-child {
border-right: 0;
}
}
}
.menu-item {
border-right: 1px dotted $border-color;
.badge {
display: inline-block;
min-width: 10px;
padding: 3px 7px;
font-size: 12px;
font-weight: bold;
color: white;
line-height: 1;
vertical-align: baseline;
white-space: nowrap;
text-align: center;
background-color: $color;
border-radius: 10px;
}
}
}
}

View file

@ -0,0 +1,93 @@
$step-color: #b5b5b5;;
$step-padding-top: 3px;
$step-width: 26px;
#transaction-form {
.form-step {
margin-bottom: 10px;
.form-step-counter {
float: left;
font-size: 16px;
font-weight: bold;
vertical-align: middle;
white-space: nowrap;
text-align: center;
display: inline-block;
min-width: 10px;
padding: $step-padding-top 7px;
height: $step-width;
width: $step-width;
background: $step-color;
color: #fff;
position: relative;
border-radius: 50%;
}
.form-step-content {
margin-left: $step-width + 10px;
width: 100%;
.form-step-title {
display: inline-block;
font-weight: bold;
margin-bottom: 5px;
}
.form-step-error {
color: red;
font-size: 11px;
height: 15px;
}
.form-options {
& > div {
border: 1px solid #000;
display: inline-block;
width: 40%;
margin-right: 10px;
padding: 20px;
text-align: center;
}
}
.suggestions-wrapper {
position: relative;
input.active {
border-radius: 6px 6px 0 0;
}
.suggestions {
position: absolute;
top: 100%;
width: 100%;
border: 1px solid #ccc;
border-top: 0;
border-radius: 0 0 6px 6px;
background: #fff;
z-index: 2;
.suggestion {
cursor: pointer;
padding: 5px 8px;
&:hover {
background-color: #f7f7f7;
}
&:last-child {
border-radius: 0 0 6px 6px;
}
}
}
}
.btn {
&:focus, &:active {
outline: 0;
}
}
}
}
}

View file

@ -27,6 +27,6 @@ class ApplicationController < ActionController::Base
end
def after_sign_in_path_for(resource)
current_user
root_path
end
end

View file

@ -6,7 +6,7 @@ class TransactionsQuery
@transactions = Arel::Table.new(:transactions)
@perspectived = Arel::Table.new(:perspectived_transactions)
@peers = Arel::Table.new(:users).alias('peers')
@arel_table = Arel::Table.new(@user.name.concat('_transactions'))
@arel_table = Arel::Table.new("#{@user.name}_transactions")
end
def query

View file

@ -10,7 +10,7 @@ class NotificationsController < ApplicationController
def read
@notification.read!
redirect_to user_notifications_path(@notification.user)
redirect_to root_path
end
private

View file

@ -1,8 +1,15 @@
class PagesController < ApplicationController
require 'statistics'
def landing
query = TransactionsQuery.new(current_user)
@transactions = ActiveRecord::Base.connection.exec_query(query.query.order(query.arel_table[:time].desc).take(10).project(Arel.star).to_sql)
@requests = current_user.incoming_requests.open.includes(:creditor).take(10)
@outgoing_requests = current_user.outgoing_requests.open.includes(:debtor).take(10)
@notifications = current_user.notifications.unread
end
def sign_in_page
@statistics = Statistics.new
end
end

View file

@ -10,12 +10,12 @@ class RequestsController < ApplicationController
def confirm
@request.confirm!
redirect_to user_requests_path(@request.debtor)
redirect_to root_path
end
def decline
@request.decline!
redirect_to user_requests_path(@request.debtor)
redirect_to root_path
end
private

View file

@ -10,21 +10,21 @@ class TransactionsController < ApplicationController
@transaction = Transaction.new(transaction_params)
@transaction.reverse if @transaction.amount < 0
if can? :create, @transaction
if @transaction.save
render json: @transaction, status: :created
else
render json: @transaction.errors.full_messages,
status: :unprocessable_entity
unless can? :create, @transaction
@transaction = Request.new @transaction.info
authorize!(:create, @transaction)
end
if @transaction.save
respond_to do |format|
format.html { redirect_to root_path }
format.json { render json: @transaction, status: :created }
end
else
request = Request.new @transaction.info
authorize!(:create, request)
if request.save
render json: request, status: :created
else
render json: request.errors.full_messages,
status: :unprocessable_entity
respond_to do |format|
format.html { redirect_to root_path }
format.json { render json: @transaction.errors.full_messages,
status: :unprocessable_entity }
end
end
end

View file

@ -1,13 +1,16 @@
module BaseTransaction
extend ActiveSupport::Concern
include ActionView::Helpers::NumberHelper
include ApplicationHelper
included do
belongs_to :debtor, class_name: 'User'
belongs_to :creditor, class_name: 'User'
belongs_to :issuer, polymorphic: true
validates :amount, numericality: { greater_than: 0 }
validates :debtor, presence: true
validates :creditor, presence: true
validates :amount, numericality: { greater_than: 0 }
validate :different_debtor_creditor
end
@ -18,7 +21,7 @@ module BaseTransaction
end
def amount_f
number_to_currency amount/100.0, unit: '€'
euro_from_cents amount
end
private

View file

@ -13,6 +13,8 @@
class Notification < ActiveRecord::Base
belongs_to :user
scope :unread, -> { where read: false }
def read!
update_attributes read: true
end

View file

@ -21,7 +21,7 @@ class User < ActiveRecord::Base
has_many :incoming_requests,
class_name: 'Request', foreign_key: 'debtor_id'
has_many :outgoing_requests,
class_name: 'Request', foreign_key: 'debtor_id'
class_name: 'Request', foreign_key: 'creditor_id'
has_many :notifications
has_many :issued_transactions, as: :issuer, class_name: 'Transaction'

View file

@ -1,17 +1,22 @@
.pure-u-1
.pure-menu.pure-menu-horizontal
= link_to "Tab", root_path, class: "pure-menu-heading pure-menu-link"
%ul.pure-menu-list
%li.pure-menu-item
= link_to current_user.name.capitalize, current_user, class: "pure-menu-link"
- if current_user.penning
%li.pure-menu-item
=link_to "Zeus", User.zeus, class: "pure-menu-link"
%li.pure-menu-item
= link_to "Requests (#{User.zeus.incoming_requests.size})", user_requests_path(User.zeus), class: 'pure-menu-link'
%li.pure-menu-item
= link_to "Notifications (#{User.zeus.notifications.size})", user_notifications_path(User.zeus), class: 'pure-menu-link'
%li.pure-menu-item
= link_to "Requests (#{current_user.incoming_requests.size})", user_requests_path(current_user), class: 'pure-menu-link'
%li.pure-menu-item
= link_to "Notifications (#{current_user.notifications.size})", user_notifications_path(current_user), class: 'pure-menu-link'
.menu
= link_to 'Tab', root_path, class: 'menu-heading menu-item'
.menu-list
= link_to 'Transactions', current_user, class: 'menu-item'
= link_to user_requests_path(current_user), class: 'menu-item' do
Requests
%span.badge= current_user.incoming_requests.open.size
= link_to user_notifications_path(current_user), class: 'menu-item' do
Notifications
%span.badge= current_user.notifications.unread.size
- if current_user.penning
= link_to 'Zeus', User.zeus, class: 'menu-item'
= link_to user_requests_path(User.zeus), class: 'menu-item' do
Zeus Requests
%span.badge= User.zeus.incoming_requests.size
= link_to user_notifications_path(User.zeus), class: 'menu-item' do
Zeus Notifications
%span.badge= User.zeus.notifications.size
.menu-list.menu-right
%span.menu-item= euro_from_cents current_user.balance
= link_to 'Sign out', sign_out_path, method: :delete, class: 'menu-item menu-item-signout'

View file

@ -10,6 +10,6 @@
%body
.pure-g
= render 'menu' if current_user
.pure-u-1
#content.pure-u-1
= render 'flash'
= yield

View file

@ -0,0 +1,16 @@
.card-wrapper
- if @notifications.any?
.card
.padded
%h3 Notifications
- @notifications.each do |n|
.notification.pure-g
.pure-u-11-12
= n.message
.pure-u-1-12.actions
= link_to notification_read_path(n), method: :post do
%span.glyphicon.glyphicon-eye-open
- else
.card.padded
%span.info-message
You have no unread notifications.

View file

@ -0,0 +1,17 @@
.card-wrapper
- if @outgoing_requests.any?
.card
.padded
%h3 Outgoing Requests
- @outgoing_requests.each do |r|
.request.pure-g
.pure-u-2-3
%h4= r.message
= r.debtor.name
.pure-u-1-3.actions
= euro_from_cents r.amount
.clear-both
- else
.card.padded
%span.info-message
You have no open outgoing requests at the moment.

View file

@ -0,0 +1,23 @@
.card-wrapper
- if @requests.any?
.card
.padded
%h3 Requests
- @requests.each do |r|
.request.pure-g
.pure-u-1-3
%h4= r.message
= r.creditor.name
.pure-u-1-3
= euro_from_cents r.amount
.pure-u-1-3.actions
.btn-group
= link_to request_confirm_path(r), method: :post, class: 'btn btn-default btn-success' do
%span.glyphicon.glyphicon-ok
= link_to request_decline_path(r), method: :post, class: 'btn btn-default btn-danger' do
%span.glyphicon.glyphicon-remove
.clear-both
- else
.card.padded
%span.info-message
You have no open requests at the moment.

View file

@ -0,0 +1,3 @@
.card-wrapper
.card.padded
= react_component 'TransactionForm', user: current_user, peers: User.all.order(:name).pluck(:name)

View file

@ -0,0 +1,21 @@
.card-wrapper
.card
- @transactions.each do |t|
- t.symbolize_keys!
- date = Date.parse t[:time]
.transaction
.transaction-calendar
%span.transaction-day= date.strftime('%d')
%span.transaction-month= Date::MONTHNAMES[date.month][0..2]
.transaction-block
.transaction-block-l
%h4.transaction-message
= t[:message]
.transaction-peer
= t[:peer]
- if t[:peer] != t[:issuer]
.transaction-issuer
= "issued by #{t[:issuer]}"
.transaction-block-r
= euro_from_cents t[:amount]
.clear-both

View file

@ -1,43 +1,8 @@
%h1.columns-title Tab
= javascript_include_tag "//www.google.com/jsapi", "chartkick"
- unless user_signed_in?
.pure-g.landing_columns
.pure-u-1.pure-u-md-1-2
%h2 Authentication
%p Log een keer in en betaal uw schulden!
%p= link_to "Log in met Zeus WPI", user_omniauth_authorize_path(:zeuswpi), class: "pure-button pure-button-primary login-button"
.pure-u-1.pure-u-md-1-2
%h2 Pie of Shame
= pie_chart @statistics.shamehash
- else
%h2.columns-title Cute Little Statistics
.pure-g
.pure-u-1.pure-u-md-1-2.landing-column
%h3.columns-title Pie of Shame
= pie_chart @statistics.shamehash
%h3.columns-title Table of Shame
%table.pure-table.full-table
%thead
%th Shame on
%th Contribution to Zeus' lack of money
%tbody
- @statistics.shameful_users.each do |user|
%tr
%td.shameful-person= user.name
// Won't divide by zero because there won't be any users with
// a shameful debt if the total debt is zero.
%td.shame-percentage= "#{-100 * user.balance / @statistics.total_debt}%"
.pure-u-1.pure-u-md-1-2.landing-column
%h3.columns-title Distribution of Debt Sources
= pie_chart @statistics.by_issuer
%h3.columns-title Top Debt Creators
%table.pure-table.full-table
%thead
%th Issuer
%th Number of Transactions issued
%tbody
- @statistics.creation_counts.each do |name, count|
%tr
%td.shameful-person= name
%td.shame-percentage= count
.pure-g
.pure-u-7-12
= render 'transactions'
.pure-u-5-12
= render 'transaction_form'
= render 'requests'
= render 'notifications'
= render 'outgoing_requests'

View file

@ -0,0 +1,10 @@
%h1.columns-title Tab
= javascript_include_tag "//www.google.com/jsapi", "chartkick"
.pure-g.landing_columns
.pure-u-1.pure-u-md-1-2
%h2 Authentication
%p Log een keer in en betaal uw schulden!
%p= link_to "Log in met Zeus WPI", user_omniauth_authorize_path(:zeuswpi), class: "pure-button pure-button-primary login-button"
.pure-u-1.pure-u-md-1-2
%h2 Pie of Shame
= pie_chart @statistics.shamehash

View file

@ -3,7 +3,15 @@ Rails.application.routes.draw do
omniauth_callbacks: 'callbacks'
}
root to: 'pages#landing'
devise_scope :user do
delete '/sign_out', to: 'devise/sessions#destroy'
end
authenticated :user do
root 'pages#landing', as: :authenticated_root
end
root to: 'pages#sign_in_page'
resources :transactions, only: [:index, :create]
resources :users, only: [:index, :show] do