carte-server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ &nbsp;
68
+ OK
69
+ &nbsp;
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,9 @@
1
+ # @cjsx React.DOM
2
+ React = require('react')
3
+
4
+ module.exports = React.createClass
5
+ displayName: 'Footer'
6
+
7
+ render: ->
8
+ <div style={{paddingTop:'0px',paddingBottom:'100px',paddingRight:"30px",paddingLeft:'30px'}}>
9
+ </div>
@@ -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">&laquo;</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">&raquo;</span>
102
+ </a>
103
+ </li>
104
+ }
105
+ </ul>
106
+ }
107
+ </div>
108
+ </div>
109
+ }
110
+ <Cards cards={@props.cards} />
111
+ </div>
@@ -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,2 @@
1
+ require 'carte/server/models/card'
2
+ require 'carte/server/models/history'
@@ -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