carte-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|