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.
@@ -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