carte-server 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +49 -0
- data/carte.gemspec +34 -0
- data/config.json +6 -0
- data/config.ru +14 -0
- data/gulpfile.coffee +11 -0
- data/lib/carte.coffee +34 -0
- data/lib/carte.rb +5 -0
- data/lib/carte/client.coffee +14 -0
- data/lib/carte/client/models/card.coffee +19 -0
- data/lib/carte/client/models/cards.coffee +14 -0
- data/lib/carte/client/router.coffee +28 -0
- data/lib/carte/client/views/app.cjsx +21 -0
- data/lib/carte/client/views/card.cjsx +62 -0
- data/lib/carte/client/views/cards.cjsx +45 -0
- data/lib/carte/client/views/content.cjsx +66 -0
- data/lib/carte/client/views/edit.cjsx +77 -0
- data/lib/carte/client/views/footer.cjsx +9 -0
- data/lib/carte/client/views/header.cjsx +44 -0
- data/lib/carte/client/views/list.cjsx +111 -0
- data/lib/carte/server.rb +113 -0
- data/lib/carte/server/models.rb +2 -0
- data/lib/carte/server/models/card.rb +60 -0
- data/lib/carte/server/models/history.rb +20 -0
- data/lib/carte/server/version.rb +7 -0
- data/lib/carte/server/views/cards.builder +18 -0
- data/lib/carte/server/views/index.haml +27 -0
- data/lib/carte/shared/.keep +0 -0
- data/mongoid.yml +10 -0
- data/package.json +36 -0
- data/public/images/icon.png +0 -0
- data/spec/server_spec.rb +174 -0
- metadata +248 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
# @cjsx React.DOM
|
2
|
+
React = require('react')
|
3
|
+
List = require('./list')
|
4
|
+
CardCollection = require('../models/cards')
|
5
|
+
CardModel = require('../models/card')
|
6
|
+
|
7
|
+
module.exports = React.createClass
|
8
|
+
displayName: 'Content'
|
9
|
+
|
10
|
+
componentWillMount: ->
|
11
|
+
console.log 'componentWillMount'
|
12
|
+
@callback = ()=> @forceUpdate()
|
13
|
+
@props.router.on "route", @callback
|
14
|
+
|
15
|
+
componentWillUnmount: ->
|
16
|
+
console.log 'componentWillMount un'
|
17
|
+
@props.router.off "route", @callback
|
18
|
+
|
19
|
+
render: ->
|
20
|
+
console.log 'render component'
|
21
|
+
switch @props.router.current
|
22
|
+
when "list"
|
23
|
+
console.log 'list', @props.router.query
|
24
|
+
cards = new CardCollection()
|
25
|
+
cards.query = @props.router.query
|
26
|
+
cards.query.sort = 'title' if !cards.query.sort
|
27
|
+
cards.query.order = 'asc' if !cards.query.order
|
28
|
+
cards.fetching = true
|
29
|
+
cards.fetch success: ()-> cards.fetching = false
|
30
|
+
title = []
|
31
|
+
for k, v of cards.query
|
32
|
+
if k != 'title'
|
33
|
+
title.push(k + ': ' + v)
|
34
|
+
title = title.join(', ')
|
35
|
+
title = 'search: ' + cards.query.title + ' (' + title + ')' if cards.query.title
|
36
|
+
title += ' - carte'
|
37
|
+
document.title = title
|
38
|
+
<List key='list' cards={cards} showNav=true />
|
39
|
+
when "show"
|
40
|
+
console.log 'show'
|
41
|
+
cards = new CardCollection()
|
42
|
+
cards.fetching = true
|
43
|
+
card = new CardModel(title: @props.router.title)
|
44
|
+
card.fetch
|
45
|
+
success: (card)->
|
46
|
+
console.log card
|
47
|
+
for left in card.get("lefts")
|
48
|
+
cardModel = new CardModel(left)
|
49
|
+
cardModel.set 'focused', false
|
50
|
+
console.log 'adding left', cardModel
|
51
|
+
cards.add cardModel
|
52
|
+
card.set 'focused', true
|
53
|
+
cards.add card
|
54
|
+
for right in card.get("rights")
|
55
|
+
cardModel = new CardModel(right)
|
56
|
+
cardModel.set 'focused', false
|
57
|
+
console.log 'adding right', cardModel
|
58
|
+
cards.add cardModel
|
59
|
+
cards.fetching = false
|
60
|
+
error: (card, response)=>
|
61
|
+
console.log response
|
62
|
+
document.title = card.get('title') + ' - carte'
|
63
|
+
<List key='show' cards={cards} showNav=false />
|
64
|
+
else
|
65
|
+
console.log 'else'
|
66
|
+
<div>Loading ...</div>
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# @cjsx React.DOM
|
2
|
+
$ = require('jquery')
|
3
|
+
React = require('react')
|
4
|
+
Modal = require('react-bootstrap/lib/Modal')
|
5
|
+
Button = require('react-bootstrap/lib/Button')
|
6
|
+
Loader = require('react-loader')
|
7
|
+
|
8
|
+
module.exports = React.createClass
|
9
|
+
displayName: 'Edit'
|
10
|
+
|
11
|
+
getInitialState: ()->
|
12
|
+
updating: false
|
13
|
+
title: @props.card.get('title')
|
14
|
+
content: @props.card.get('content')
|
15
|
+
errors: false
|
16
|
+
|
17
|
+
onChangeTitle: ->
|
18
|
+
@setState title: event.target.value
|
19
|
+
|
20
|
+
onChangeContent: ->
|
21
|
+
@setState content: event.target.value
|
22
|
+
|
23
|
+
onClickOk: ()->
|
24
|
+
event.preventDefault()
|
25
|
+
@setState updating: true
|
26
|
+
if @props.card.isNew()
|
27
|
+
attributes = {title: @state.title, content: @state.content}
|
28
|
+
else
|
29
|
+
attributes = {new_title: @state.title, content: @state.content}
|
30
|
+
@props.card.save attributes,
|
31
|
+
success: ()=>
|
32
|
+
@setState updating: false
|
33
|
+
@props.onRequestHide()
|
34
|
+
@props.card.set 'title', @state.title
|
35
|
+
if @props.card.isNew()
|
36
|
+
location.hash = '/' + @state.title
|
37
|
+
error: (model, response, options)=>
|
38
|
+
console.log response.responseJSON
|
39
|
+
@setState errors: response.responseJSON.card.errors
|
40
|
+
@setState updating: false
|
41
|
+
|
42
|
+
render: ->
|
43
|
+
<Modal {...@props} bsStyle='default' title={if @props.card.isNew() then 'New' else 'Edit'} animation={false}>
|
44
|
+
<div className='modal-body'>
|
45
|
+
{
|
46
|
+
if @state.errors
|
47
|
+
<div className="alert alert-danger" role="alert" style={padding:'5px'}>
|
48
|
+
<ul style={paddingLeft:"20px"}>
|
49
|
+
{
|
50
|
+
for key, errors of @state.errors
|
51
|
+
for error in errors
|
52
|
+
<li>{key + ' ' + error}</li>
|
53
|
+
}
|
54
|
+
</ul>
|
55
|
+
</div>
|
56
|
+
}
|
57
|
+
<div className="form-group">
|
58
|
+
<label class="control-label">Title</label>
|
59
|
+
<input type="text" className="form-control" value={@state.title} onChange={@onChangeTitle} disabled={@state.updating} id="inputError1" />
|
60
|
+
</div>
|
61
|
+
<div className="form-group">
|
62
|
+
<label class="control-label">Content</label>
|
63
|
+
<textarea rows="10" className="form-control" value={@state.content} onChange={@onChangeContent} disabled={@state.updating} />
|
64
|
+
</div>
|
65
|
+
<div className="form-group" style={{paddingBottom:'17px'}}>
|
66
|
+
<button className="btn btn-default pull-right" onClick={@onClickOk} disabled={@state.updating}>
|
67
|
+
|
68
|
+
OK
|
69
|
+
|
70
|
+
{
|
71
|
+
if @state.updating
|
72
|
+
<i className='glyphicon glyphicon-refresh glyphicon-refresh-animate' />
|
73
|
+
}
|
74
|
+
</button>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
</Modal>
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# @cjsx React.DOM
|
2
|
+
React = require('react')
|
3
|
+
Edit = require('./edit')
|
4
|
+
CardModel = require('../models/card')
|
5
|
+
ModalTrigger = require('react-bootstrap/lib/ModalTrigger')
|
6
|
+
config = require('../../shared/config.json')
|
7
|
+
|
8
|
+
module.exports = React.createClass
|
9
|
+
displayName: 'Header'
|
10
|
+
|
11
|
+
componentWillMount: ()->
|
12
|
+
console.log 'header mounted'
|
13
|
+
@card = new CardModel()
|
14
|
+
@card._isNew = true
|
15
|
+
@card.on 'sync', (model)=>
|
16
|
+
console.log 'sync!!!'
|
17
|
+
@card = new CardModel()
|
18
|
+
@card._isNew = true
|
19
|
+
@forceUpdate()
|
20
|
+
|
21
|
+
render: ->
|
22
|
+
<nav className="navbar navbar-default" style={{padding:"0px",backgroundColor:"white"}}>
|
23
|
+
<div className="container-fluid">
|
24
|
+
<div className="navbar-header">
|
25
|
+
<a className="navbar-brand" href="#/" style={{paddingTop:"10px"}}>
|
26
|
+
<img alt="Brand" src="/images/icon.png" width="30" height="30" />
|
27
|
+
</a>
|
28
|
+
<a className="navbar-brand" href="#/">
|
29
|
+
{config.title}
|
30
|
+
</a>
|
31
|
+
</div>
|
32
|
+
<div className="collapse navbar-collapse">
|
33
|
+
<ul className="nav navbar-nav navbar-right">
|
34
|
+
<li>
|
35
|
+
<ModalTrigger modal={<Edit card={@card} />}>
|
36
|
+
<a href="javascript:void(0)">
|
37
|
+
<i className="glyphicon glyphicon-plus" />
|
38
|
+
</a>
|
39
|
+
</ModalTrigger>
|
40
|
+
</li>
|
41
|
+
</ul>
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
</nav>
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# @cjsx React.DOM
|
2
|
+
$ = require('jquery')
|
3
|
+
React = require('react')
|
4
|
+
Cards = require('./cards')
|
5
|
+
CardCollection = require('../models/cards')
|
6
|
+
|
7
|
+
module.exports = React.createClass
|
8
|
+
displayName: 'List'
|
9
|
+
|
10
|
+
componentWillReceiveProps: (nextProps)->
|
11
|
+
console.log 'List: component will receive props'
|
12
|
+
nextProps.cards.on 'sync', @forceUpdate.bind(@, null)
|
13
|
+
|
14
|
+
getInitialState: ()->
|
15
|
+
searchText: ''
|
16
|
+
|
17
|
+
onChangeSearchText: ()->
|
18
|
+
@setState searchText: event.target.value
|
19
|
+
|
20
|
+
onKeyPressSearchText: ()->
|
21
|
+
if event.keyCode == 13 # ENTER
|
22
|
+
console.log '13 enter', @props.cards.query
|
23
|
+
event.preventDefault()
|
24
|
+
query = $.extend {}, @props.cards.query
|
25
|
+
query = $.extend query, {title: @state.searchText}
|
26
|
+
location.hash = '/?' + $.param(query)
|
27
|
+
|
28
|
+
atozParam: ()->
|
29
|
+
query = $.extend {}, @props.cards.query
|
30
|
+
query = $.extend query, {sort: 'title', order: 'asc', page: 1}
|
31
|
+
delete query.seed
|
32
|
+
$.param(query)
|
33
|
+
|
34
|
+
latestParam: ()->
|
35
|
+
query = $.extend {}, @props.cards.query
|
36
|
+
query = $.extend query, {sort: 'updated_at', order: 'desc', page: 1}
|
37
|
+
delete query.seed
|
38
|
+
$.param(query)
|
39
|
+
|
40
|
+
randomParam: ()->
|
41
|
+
query = $.extend {}, @props.cards.query
|
42
|
+
query = $.extend query, {order: 'random', page: 1, seed: new Date().getTime()}
|
43
|
+
delete query.sort
|
44
|
+
delete query.page
|
45
|
+
$.param(query)
|
46
|
+
|
47
|
+
pageParam: (page)->
|
48
|
+
query = $.extend {}, @props.cards.query
|
49
|
+
query = $.extend query, {page: page}
|
50
|
+
$.param(query)
|
51
|
+
|
52
|
+
render: ->
|
53
|
+
console.log 'render', @props.cards.query
|
54
|
+
<div className="container" style={{paddingLeft:"5px",paddingRight:"5px",paddingBottom:"20px"}}>
|
55
|
+
{if @props.showNav
|
56
|
+
<div className="row">
|
57
|
+
<div className="col-sm-12" style={{padding:"5px"}}>
|
58
|
+
<form>
|
59
|
+
<div className="form-group">
|
60
|
+
<input type="text" className="form-control" value={@state.searchText} onChange={@onChangeSearchText} onKeyPress={@onKeyPressSearchText} placeholder='Type search text and press enter ...' />
|
61
|
+
</div>
|
62
|
+
</form>
|
63
|
+
</div>
|
64
|
+
<div className="col-sm-6" style={{padding:"0px"}}>
|
65
|
+
<ul className="nav nav-pills">
|
66
|
+
<li><a href={"/#/?" + @atozParam()} style={{padding:'6px 12px',fontWeight: if @props.cards.query.sort == 'title' and @props.cards.query.order != 'random' then 'bold' else 'normal'}}>A to Z</a></li>
|
67
|
+
<li><a href={"/#/?" + @latestParam()} style={{padding:'6px 12px',fontWeight: if @props.cards.query.sort == 'updated_at' and @props.cards.query.order != 'random' then 'bold' else 'normal'}}>Latest</a></li>
|
68
|
+
<li><a href={"/#/?" + @randomParam()} style={{padding:'6px 12px',fontWeight: if @props.cards.query.order == 'random' then 'bold' else 'normal'}}>Random</a></li>
|
69
|
+
</ul>
|
70
|
+
</div>
|
71
|
+
<div className="col-sm-6" style={{padding:"0px"}}>
|
72
|
+
{
|
73
|
+
if @props.cards.query.order == 'random'
|
74
|
+
<ul className="nav nav-pills pull-right">
|
75
|
+
<li>
|
76
|
+
<a href={"/#/?" + @randomParam()} style={{padding:'6px 12px'}}>
|
77
|
+
<i className="glyphicon glyphicon-refresh" />
|
78
|
+
</a>
|
79
|
+
</li>
|
80
|
+
</ul>
|
81
|
+
else
|
82
|
+
if @props.cards.page
|
83
|
+
<ul className="nav nav-pills pull-right">
|
84
|
+
{
|
85
|
+
if @props.cards.page.current > 1
|
86
|
+
<li>
|
87
|
+
<a href={"/#/?" + @pageParam(@props.cards.page.current - 1)} aria-label="Previous" style={{padding:'6px 12px'}}>
|
88
|
+
<span aria-hidden="true">«</span>
|
89
|
+
</a>
|
90
|
+
</li>
|
91
|
+
}
|
92
|
+
<li>
|
93
|
+
<a href={"/#/?" + @pageParam(@props.cards.page.current)} style={{padding:'6px 12px'}}>
|
94
|
+
{@props.cards.page.current} / {@props.cards.page.total}
|
95
|
+
</a>
|
96
|
+
</li>
|
97
|
+
{
|
98
|
+
if @props.cards.page.current < @props.cards.page.total
|
99
|
+
<li>
|
100
|
+
<a href={"/#/?" + @pageParam(@props.cards.page.current + 1)} aria-label="Next" style={{padding:'6px 12px'}}>
|
101
|
+
<span aria-hidden="true">»</span>
|
102
|
+
</a>
|
103
|
+
</li>
|
104
|
+
}
|
105
|
+
</ul>
|
106
|
+
}
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
}
|
110
|
+
<Cards cards={@props.cards} />
|
111
|
+
</div>
|
data/lib/carte/server.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/namespace'
|
3
|
+
require 'mongoid'
|
4
|
+
require 'mongoid_auto_increment_id'
|
5
|
+
require 'will_paginate_mongoid'
|
6
|
+
require 'carte/server/models'
|
7
|
+
|
8
|
+
module Carte
|
9
|
+
class Server < Sinatra::Base
|
10
|
+
register Sinatra::Namespace
|
11
|
+
include Carte::Server::Models
|
12
|
+
|
13
|
+
configure do
|
14
|
+
set :views, File.join(File.dirname(__FILE__), 'server/views')
|
15
|
+
set :public_folder, 'public'
|
16
|
+
set :script_path, '/app.js'
|
17
|
+
end
|
18
|
+
|
19
|
+
helpers do
|
20
|
+
def json_data
|
21
|
+
request.body.rewind
|
22
|
+
JSON.parse(request.body.read)
|
23
|
+
end
|
24
|
+
|
25
|
+
def search(params)
|
26
|
+
order = (params[:order] && %w(asc desc random).include?(params[:order])) ? params[:order] : 'desc'
|
27
|
+
sort = (params[:sort] && %w(title created_at updated_at).include?(params[:sort])) ? params[:sort] : 'updated_at'
|
28
|
+
if order == 'random'
|
29
|
+
return Card.sample(9)
|
30
|
+
end
|
31
|
+
cards = Card.send(order, sort)
|
32
|
+
if title = params[:title]
|
33
|
+
cards = cards.any_of({title: /#{title}/})
|
34
|
+
end
|
35
|
+
if content = params[:content]
|
36
|
+
cards = cards.any_of({content: /#{content}/})
|
37
|
+
end
|
38
|
+
cards = cards.paginate(per_page: 9, page: params[:page])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/' do
|
43
|
+
haml :index
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/app.js' do
|
47
|
+
File.read(settings.script_path)
|
48
|
+
end
|
49
|
+
|
50
|
+
namespace '/api' do
|
51
|
+
get '/cards.xml' do
|
52
|
+
@cards = search(params)
|
53
|
+
builder :cards
|
54
|
+
end
|
55
|
+
|
56
|
+
get '/cards.json' do
|
57
|
+
cards = search(params)
|
58
|
+
if cards.respond_to?(:current_page) && cards.respond_to?(:total_pages)
|
59
|
+
current_page = cards.current_page.to_i
|
60
|
+
total_pages = cards.total_pages
|
61
|
+
end
|
62
|
+
cards = cards.map {|card| {id: card.id, title: card.title, content: card.content, version: card.version}}
|
63
|
+
{cards: cards, page: {current: current_page, total: total_pages}}.to_json
|
64
|
+
end
|
65
|
+
|
66
|
+
get '/cards/:title.json' do
|
67
|
+
card = Card.where(title: params[:title]).first
|
68
|
+
halt 404 if card.nil?
|
69
|
+
{card: {id: card.id, title: card.title, content: card.content, version: card.version, lefts: card.lefts(4), rights: card.rights(4)}}.to_json
|
70
|
+
end
|
71
|
+
|
72
|
+
post '/cards.json' do
|
73
|
+
card = Card.new(json_data)
|
74
|
+
if card.save
|
75
|
+
status 201
|
76
|
+
{card: {id: card.id}}.to_json
|
77
|
+
else
|
78
|
+
status 400
|
79
|
+
{card: {errors: card.errors}}.to_json
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
put '/cards/:title.json' do
|
84
|
+
card = Card.where(title: params[:title]).first
|
85
|
+
halt 404 if card.nil?
|
86
|
+
card.histories.create!
|
87
|
+
if card.update_attributes(json_data.slice('new_title', 'content').compact)
|
88
|
+
status 201
|
89
|
+
{}.to_json
|
90
|
+
else
|
91
|
+
status 400
|
92
|
+
{card: {errors: card.errors}}.to_json
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
#delete '/cards/:title.json' do
|
97
|
+
# card = Card.where(title: params[:title]).first
|
98
|
+
# halt 404 if card.nil?
|
99
|
+
# card.destroy
|
100
|
+
#end
|
101
|
+
|
102
|
+
get '/cards/:title/history.json' do
|
103
|
+
card = Card.where(title: params[:title]).first
|
104
|
+
halt 404 if card.nil?
|
105
|
+
{history: card.histories}.to_json
|
106
|
+
end
|
107
|
+
|
108
|
+
error(404) do
|
109
|
+
{}.to_json
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Carte
|
2
|
+
class Server < Sinatra::Base
|
3
|
+
module Models
|
4
|
+
class Card
|
5
|
+
include Mongoid::Document
|
6
|
+
include Mongoid::Timestamps
|
7
|
+
include Mongoid::Attributes::Dynamic
|
8
|
+
|
9
|
+
field :title, type: String
|
10
|
+
field :new_title, type: String
|
11
|
+
field :content, type: String
|
12
|
+
|
13
|
+
index({title: 1}, {unique: true, name: "title_index"})
|
14
|
+
|
15
|
+
validates :title,
|
16
|
+
presence: true,
|
17
|
+
on: :create
|
18
|
+
validates :title,
|
19
|
+
uniqueness: true,
|
20
|
+
length: {maximum: 70}
|
21
|
+
validates :content,
|
22
|
+
presence: true,
|
23
|
+
length: {maximum: 560}
|
24
|
+
|
25
|
+
has_many :histories
|
26
|
+
|
27
|
+
def version
|
28
|
+
self.histories.size + 1
|
29
|
+
end
|
30
|
+
|
31
|
+
before_validation(on: :update) do
|
32
|
+
self.title = self.new_title
|
33
|
+
self.new_title = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.sample(size=1)
|
37
|
+
self.in(id: (1...self.count).to_a.sample(size))
|
38
|
+
end
|
39
|
+
|
40
|
+
def lefts(size=1)
|
41
|
+
ids = []
|
42
|
+
count = self.class.all.count
|
43
|
+
1.upto(size) do |i|
|
44
|
+
ids << (self.id - i > 0 ? self.id - i : count + (self.id - i))
|
45
|
+
end
|
46
|
+
self.class.in(id: ids)
|
47
|
+
end
|
48
|
+
|
49
|
+
def rights(size=1)
|
50
|
+
ids = []
|
51
|
+
count = self.class.all.count
|
52
|
+
1.upto(size) do |i|
|
53
|
+
ids << (self.id + i <= count ? self.id + i : self.id + i - count)
|
54
|
+
end
|
55
|
+
self.class.in(id: ids)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|