commit
e35fe64b9e
31 changed files with 660 additions and 86 deletions
1
Gemfile
1
Gemfile
|
@ -95,3 +95,4 @@ gem 'high_voltage', '~> 2.4.0'
|
||||||
gem 'airbrake'
|
gem 'airbrake'
|
||||||
|
|
||||||
gem 'bootstrap-sass', '~> 3.3.5'
|
gem 'bootstrap-sass', '~> 3.3.5'
|
||||||
|
gem 'react-rails'
|
||||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -46,6 +46,10 @@ GEM
|
||||||
autoprefixer-rails (6.0.2)
|
autoprefixer-rails (6.0.2)
|
||||||
execjs
|
execjs
|
||||||
json
|
json
|
||||||
|
babel-source (5.8.35)
|
||||||
|
babel-transpiler (0.7.0)
|
||||||
|
babel-source (>= 4.0, < 6)
|
||||||
|
execjs (~> 2.0)
|
||||||
bcrypt (3.1.10)
|
bcrypt (3.1.10)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
|
@ -79,6 +83,7 @@ GEM
|
||||||
execjs
|
execjs
|
||||||
coffee-script-source (1.9.1.1)
|
coffee-script-source (1.9.1.1)
|
||||||
colorize (0.7.7)
|
colorize (0.7.7)
|
||||||
|
connection_pool (2.2.1)
|
||||||
coveralls (0.8.2)
|
coveralls (0.8.2)
|
||||||
json (~> 1.8)
|
json (~> 1.8)
|
||||||
rest-client (>= 1.6.8, < 2)
|
rest-client (>= 1.6.8, < 2)
|
||||||
|
@ -205,6 +210,13 @@ GEM
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
rake (10.4.2)
|
rake (10.4.2)
|
||||||
rdoc (4.2.0)
|
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)
|
responders (2.1.0)
|
||||||
railties (>= 4.2.0, < 5)
|
railties (>= 4.2.0, < 5)
|
||||||
rest-client (1.8.0)
|
rest-client (1.8.0)
|
||||||
|
@ -313,6 +325,7 @@ DEPENDENCIES
|
||||||
omniauth-oauth2
|
omniauth-oauth2
|
||||||
purecss-rails
|
purecss-rails
|
||||||
rails (= 4.2.4)
|
rails (= 4.2.4)
|
||||||
|
react-rails
|
||||||
rspec-rails
|
rspec-rails
|
||||||
sass-rails (~> 5.0)
|
sass-rails (~> 5.0)
|
||||||
sdoc (~> 0.4.0)
|
sdoc (~> 0.4.0)
|
||||||
|
@ -324,4 +337,4 @@ DEPENDENCIES
|
||||||
web-console (~> 2.0)
|
web-console (~> 2.0)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.10.6
|
1.13.7
|
||||||
|
|
|
@ -12,12 +12,16 @@
|
||||||
//
|
//
|
||||||
//= require jquery
|
//= require jquery
|
||||||
//= require jquery_ujs
|
//= require jquery_ujs
|
||||||
|
//= require bootstrap-sprockets
|
||||||
//= require dataTables/jquery.dataTables
|
//= require dataTables/jquery.dataTables
|
||||||
//= require dataTables/extras/dataTables.responsive
|
//= require dataTables/extras/dataTables.responsive
|
||||||
//= require dataTables/jquery.dataTables
|
//= require dataTables/jquery.dataTables
|
||||||
//= require select2
|
//= require select2
|
||||||
//= require jquery-dateFormat
|
//= require jquery-dateFormat
|
||||||
//= require turbolinks
|
//= require turbolinks
|
||||||
|
//= require react
|
||||||
|
//= require react_ujs
|
||||||
|
//= require components
|
||||||
//= require_tree .
|
//= require_tree .
|
||||||
|
|
||||||
ready = function() {
|
ready = function() {
|
||||||
|
|
1
app/assets/javascripts/components.js
Normal file
1
app/assets/javascripts/components.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
//= require_tree ./components
|
0
app/assets/javascripts/components/.gitkeep
Normal file
0
app/assets/javascripts/components/.gitkeep
Normal file
213
app/assets/javascripts/components/transaction_form.jsx.coffee
Normal file
213
app/assets/javascripts/components/transaction_form.jsx.coffee
Normal 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'
|
|
@ -21,6 +21,6 @@
|
||||||
@import "bootstrap-sprockets";
|
@import "bootstrap-sprockets";
|
||||||
@import "bootstrap";
|
@import "bootstrap";
|
||||||
|
|
||||||
body {
|
.clear-both {
|
||||||
padding: 30px;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
17
app/assets/stylesheets/card.scss
Normal file
17
app/assets/stylesheets/card.scss
Normal 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;
|
||||||
|
}
|
|
@ -42,3 +42,76 @@ a.login-button {
|
||||||
.shame-percentage {
|
.shame-percentage {
|
||||||
text-align: right
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
7
app/assets/stylesheets/layout.scss
Normal file
7
app/assets/stylesheets/layout.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-message {
|
||||||
|
color: rgb(144, 148, 156);
|
||||||
|
}
|
72
app/assets/stylesheets/menu.scss
Normal file
72
app/assets/stylesheets/menu.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
app/assets/stylesheets/transaction_form.scss
Normal file
93
app/assets/stylesheets/transaction_form.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,6 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_in_path_for(resource)
|
def after_sign_in_path_for(resource)
|
||||||
current_user
|
root_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ class TransactionsQuery
|
||||||
@transactions = Arel::Table.new(:transactions)
|
@transactions = Arel::Table.new(:transactions)
|
||||||
@perspectived = Arel::Table.new(:perspectived_transactions)
|
@perspectived = Arel::Table.new(:perspectived_transactions)
|
||||||
@peers = Arel::Table.new(:users).alias('peers')
|
@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
|
end
|
||||||
|
|
||||||
def query
|
def query
|
||||||
|
|
|
@ -10,7 +10,7 @@ class NotificationsController < ApplicationController
|
||||||
|
|
||||||
def read
|
def read
|
||||||
@notification.read!
|
@notification.read!
|
||||||
redirect_to user_notifications_path(@notification.user)
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
class PagesController < ApplicationController
|
class PagesController < ApplicationController
|
||||||
|
|
||||||
require 'statistics'
|
require 'statistics'
|
||||||
|
|
||||||
def landing
|
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
|
@statistics = Statistics.new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,12 +10,12 @@ class RequestsController < ApplicationController
|
||||||
|
|
||||||
def confirm
|
def confirm
|
||||||
@request.confirm!
|
@request.confirm!
|
||||||
redirect_to user_requests_path(@request.debtor)
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def decline
|
def decline
|
||||||
@request.decline!
|
@request.decline!
|
||||||
redirect_to user_requests_path(@request.debtor)
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -10,21 +10,21 @@ class TransactionsController < ApplicationController
|
||||||
@transaction = Transaction.new(transaction_params)
|
@transaction = Transaction.new(transaction_params)
|
||||||
@transaction.reverse if @transaction.amount < 0
|
@transaction.reverse if @transaction.amount < 0
|
||||||
|
|
||||||
if can? :create, @transaction
|
unless can? :create, @transaction
|
||||||
|
@transaction = Request.new @transaction.info
|
||||||
|
authorize!(:create, @transaction)
|
||||||
|
end
|
||||||
|
|
||||||
if @transaction.save
|
if @transaction.save
|
||||||
render json: @transaction, status: :created
|
respond_to do |format|
|
||||||
else
|
format.html { redirect_to root_path }
|
||||||
render json: @transaction.errors.full_messages,
|
format.json { render json: @transaction, status: :created }
|
||||||
status: :unprocessable_entity
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
request = Request.new @transaction.info
|
respond_to do |format|
|
||||||
authorize!(:create, request)
|
format.html { redirect_to root_path }
|
||||||
if request.save
|
format.json { render json: @transaction.errors.full_messages,
|
||||||
render json: request, status: :created
|
status: :unprocessable_entity }
|
||||||
else
|
|
||||||
render json: request.errors.full_messages,
|
|
||||||
status: :unprocessable_entity
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
module BaseTransaction
|
module BaseTransaction
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include ActionView::Helpers::NumberHelper
|
include ActionView::Helpers::NumberHelper
|
||||||
|
include ApplicationHelper
|
||||||
|
|
||||||
included do
|
included do
|
||||||
belongs_to :debtor, class_name: 'User'
|
belongs_to :debtor, class_name: 'User'
|
||||||
belongs_to :creditor, class_name: 'User'
|
belongs_to :creditor, class_name: 'User'
|
||||||
belongs_to :issuer, polymorphic: true
|
belongs_to :issuer, polymorphic: true
|
||||||
|
|
||||||
|
validates :debtor, presence: true
|
||||||
|
validates :creditor, presence: true
|
||||||
validates :amount, numericality: { greater_than: 0 }
|
validates :amount, numericality: { greater_than: 0 }
|
||||||
validate :different_debtor_creditor
|
validate :different_debtor_creditor
|
||||||
end
|
end
|
||||||
|
@ -18,7 +21,7 @@ module BaseTransaction
|
||||||
end
|
end
|
||||||
|
|
||||||
def amount_f
|
def amount_f
|
||||||
number_to_currency amount/100.0, unit: '€'
|
euro_from_cents amount
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
class Notification < ActiveRecord::Base
|
class Notification < ActiveRecord::Base
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
scope :unread, -> { where read: false }
|
||||||
|
|
||||||
def read!
|
def read!
|
||||||
update_attributes read: true
|
update_attributes read: true
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :incoming_requests,
|
has_many :incoming_requests,
|
||||||
class_name: 'Request', foreign_key: 'debtor_id'
|
class_name: 'Request', foreign_key: 'debtor_id'
|
||||||
has_many :outgoing_requests,
|
has_many :outgoing_requests,
|
||||||
class_name: 'Request', foreign_key: 'debtor_id'
|
class_name: 'Request', foreign_key: 'creditor_id'
|
||||||
has_many :notifications
|
has_many :notifications
|
||||||
|
|
||||||
has_many :issued_transactions, as: :issuer, class_name: 'Transaction'
|
has_many :issued_transactions, as: :issuer, class_name: 'Transaction'
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
.pure-u-1
|
.pure-u-1
|
||||||
.pure-menu.pure-menu-horizontal
|
.menu
|
||||||
= link_to "Tab", root_path, class: "pure-menu-heading pure-menu-link"
|
= link_to 'Tab', root_path, class: 'menu-heading menu-item'
|
||||||
%ul.pure-menu-list
|
.menu-list
|
||||||
%li.pure-menu-item
|
= link_to 'Transactions', current_user, class: 'menu-item'
|
||||||
= link_to current_user.name.capitalize, current_user, class: "pure-menu-link"
|
= 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
|
- if current_user.penning
|
||||||
%li.pure-menu-item
|
= link_to 'Zeus', User.zeus, class: 'menu-item'
|
||||||
=link_to "Zeus", User.zeus, class: "pure-menu-link"
|
= link_to user_requests_path(User.zeus), class: 'menu-item' do
|
||||||
%li.pure-menu-item
|
Zeus Requests
|
||||||
= link_to "Requests (#{User.zeus.incoming_requests.size})", user_requests_path(User.zeus), class: 'pure-menu-link'
|
%span.badge= User.zeus.incoming_requests.size
|
||||||
%li.pure-menu-item
|
= link_to user_notifications_path(User.zeus), class: 'menu-item' do
|
||||||
= link_to "Notifications (#{User.zeus.notifications.size})", user_notifications_path(User.zeus), class: 'pure-menu-link'
|
Zeus Notifications
|
||||||
%li.pure-menu-item
|
%span.badge= User.zeus.notifications.size
|
||||||
= link_to "Requests (#{current_user.incoming_requests.size})", user_requests_path(current_user), class: 'pure-menu-link'
|
.menu-list.menu-right
|
||||||
%li.pure-menu-item
|
%span.menu-item= euro_from_cents current_user.balance
|
||||||
= link_to "Notifications (#{current_user.notifications.size})", user_notifications_path(current_user), class: 'pure-menu-link'
|
= link_to 'Sign out', sign_out_path, method: :delete, class: 'menu-item menu-item-signout'
|
||||||
|
|
|
@ -10,6 +10,6 @@
|
||||||
%body
|
%body
|
||||||
.pure-g
|
.pure-g
|
||||||
= render 'menu' if current_user
|
= render 'menu' if current_user
|
||||||
.pure-u-1
|
#content.pure-u-1
|
||||||
= render 'flash'
|
= render 'flash'
|
||||||
= yield
|
= yield
|
||||||
|
|
16
app/views/pages/_notifications.html.haml
Normal file
16
app/views/pages/_notifications.html.haml
Normal 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.
|
17
app/views/pages/_outgoing_requests.html.haml
Normal file
17
app/views/pages/_outgoing_requests.html.haml
Normal 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.
|
23
app/views/pages/_requests.html.haml
Normal file
23
app/views/pages/_requests.html.haml
Normal 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.
|
3
app/views/pages/_transaction_form.html.haml
Normal file
3
app/views/pages/_transaction_form.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.card-wrapper
|
||||||
|
.card.padded
|
||||||
|
= react_component 'TransactionForm', user: current_user, peers: User.all.order(:name).pluck(:name)
|
21
app/views/pages/_transactions.html.haml
Normal file
21
app/views/pages/_transactions.html.haml
Normal 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
|
|
@ -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-g
|
||||||
.pure-u-1.pure-u-md-1-2.landing-column
|
.pure-u-7-12
|
||||||
%h3.columns-title Pie of Shame
|
= render 'transactions'
|
||||||
= pie_chart @statistics.shamehash
|
.pure-u-5-12
|
||||||
%h3.columns-title Table of Shame
|
= render 'transaction_form'
|
||||||
%table.pure-table.full-table
|
= render 'requests'
|
||||||
%thead
|
= render 'notifications'
|
||||||
%th Shame on
|
= render 'outgoing_requests'
|
||||||
%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
|
|
||||||
|
|
||||||
|
|
10
app/views/pages/sign_in_page.html.haml
Normal file
10
app/views/pages/sign_in_page.html.haml
Normal 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
|
|
@ -3,7 +3,15 @@ Rails.application.routes.draw do
|
||||||
omniauth_callbacks: 'callbacks'
|
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 :transactions, only: [:index, :create]
|
||||||
resources :users, only: [:index, :show] do
|
resources :users, only: [:index, :show] do
|
||||||
|
|
Loading…
Reference in a new issue