middleman-blog-ui 0.1.0

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +48 -0
  8. data/Rakefile +1 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/lib/middleman-blog-ui.rb +12 -0
  12. data/lib/middleman/blog/ui/api_server.rb +169 -0
  13. data/lib/middleman/blog/ui/extension.rb +64 -0
  14. data/lib/middleman/blog/ui/version.rb +7 -0
  15. data/middleman-blog-ui.gemspec +38 -0
  16. data/source/admin/drafts.json.erb +6 -0
  17. data/source/admin/index.html.haml +18 -0
  18. data/source/admin/published.json.erb +6 -0
  19. data/source/javascripts/admin/admin.js +25 -0
  20. data/source/javascripts/admin/api.coffee +94 -0
  21. data/source/javascripts/admin/components/admin_navbar.js.coffee +49 -0
  22. data/source/javascripts/admin/components/app.js.coffee +21 -0
  23. data/source/javascripts/admin/components/autosize_textarea.js.coffee +60 -0
  24. data/source/javascripts/admin/components/dashboard.js.coffee +8 -0
  25. data/source/javascripts/admin/components/dashboard_draft_list.js.coffee +19 -0
  26. data/source/javascripts/admin/components/dashboard_navbar.js.coffee +51 -0
  27. data/source/javascripts/admin/components/dashboard_published_list.js.coffee +19 -0
  28. data/source/javascripts/admin/components/editor.js.coffee +24 -0
  29. data/source/javascripts/admin/components/editor_navbar.js.coffee +63 -0
  30. data/source/javascripts/admin/components/markdown_preview.js.coffee +9 -0
  31. data/source/javascripts/admin/components/metadata_editor.js.coffee +25 -0
  32. data/source/javascripts/admin/marked.min.js +6 -0
  33. data/source/javascripts/admin/react-bootstrap.min.js +10 -0
  34. data/source/javascripts/admin/react-bootstrap.min.js.map +1 -0
  35. data/source/javascripts/admin/reflux.min.js +1 -0
  36. data/source/javascripts/admin/stores/article.coffee +71 -0
  37. data/source/javascripts/admin/stores/command.coffee +83 -0
  38. data/source/javascripts/admin/stores/drafts.coffee +16 -0
  39. data/source/javascripts/admin/stores/published.coffee +16 -0
  40. data/source/javascripts/admin/superagent.js +1318 -0
  41. data/source/stylesheets/admin.css.scss +55 -0
  42. metadata +186 -0
@@ -0,0 +1,6 @@
1
+ <% d = blog.articles.collect do |d|
2
+ { path: d.path, title: d.title }
3
+ end
4
+ %><%=
5
+ {articles: d }.to_json
6
+ %>
@@ -0,0 +1,25 @@
1
+ //= require react
2
+ //= require ./react-bootstrap.min
3
+ //= require ./marked.min.js
4
+ //= require ./reflux.min
5
+ //= require ./superagent.js
6
+ //= require ./api
7
+ //= require_tree ./stores
8
+ //= require_tree ./components
9
+
10
+ var request = window.superagent;
11
+
12
+ var Button = ReactBootstrap.Button;
13
+ var ButtonToolbar = ReactBootstrap.ButtonToolbar;
14
+ var Table = ReactBootstrap.Table;
15
+ var Input = ReactBootstrap.Input;
16
+ var Modal = ReactBootstrap.Modal;
17
+ var OverlayMixin = ReactBootstrap.OverlayMixin;
18
+ var ProgressBar = ReactBootstrap.ProgressBar;
19
+
20
+ var Nav = ReactBootstrap.Nav;
21
+ var Navbar = ReactBootstrap.Navbar;
22
+ var NavItem = ReactBootstrap.NavItem;
23
+ var MenuItem = ReactBootstrap.MenuItem;
24
+ var CollapsibleNav = ReactBootstrap.CollapsibleNav;
25
+ var DropdownButton = ReactBootstrap.DropdownButton;
@@ -0,0 +1,94 @@
1
+ @API =
2
+ loadUrl: (url) ->
3
+ console.log "Loading", url
4
+ unless @promises
5
+ console.log "Creating promise"
6
+ @promises = {}
7
+
8
+ unless @promises[url]
9
+ console.log "Fetching", url
10
+ @promises[url] = $.Deferred()
11
+
12
+ $.ajax( url ).success (data) =>
13
+ console.log "Got back: ", data
14
+ console.log @promises[url]
15
+ @promises[url].resolve data
16
+ , (error) =>
17
+ @promises[url].reject error
18
+
19
+ @promises[url]
20
+
21
+ loadPost: (path) ->
22
+ ret = $.Deferred()
23
+
24
+ $.ajax( '/api/post', {data: {path: path}} ).success (data) =>
25
+ console.log "Got post back", data
26
+ ret.resolve( data )
27
+ .fail (e) =>
28
+ ret.reject( e.responseJSON )
29
+
30
+ ret
31
+
32
+ savePost: (path, meta, body) ->
33
+ ret = $.Deferred()
34
+
35
+ $.ajax( '/api/post', {method: 'POST', data: {path: path, meta: meta, body: body} } ).success (data) =>
36
+ console.log "Saved post"
37
+ ret.resolve data
38
+ .fail (e) =>
39
+ console.log "Error saving post"
40
+ ret.reject( e.responseJSON )
41
+
42
+ ret
43
+
44
+ newDraft: (metadata) ->
45
+ $.post( '/api/drafts', metadata )
46
+
47
+ publishDraft: (path) ->
48
+ $.post( '/api/publish', { path: path } )
49
+
50
+ uploadFile: ( path, nativeEvent, process_cb ) ->
51
+ console.log "Uploading image to path"
52
+
53
+ fd = new FormData()
54
+ fd.append 'path', path
55
+ fd.append 'file', nativeEvent.dataTransfer.files[0]
56
+
57
+ $.ajax
58
+ type: "post"
59
+ url: '/api/images'
60
+ xhr: ->
61
+ xhr = new XMLHttpRequest()
62
+ xhr.upload.onprogress = process_cb
63
+ xhr
64
+ cache: false
65
+ contentType: false
66
+ # complete: uploadCompleted
67
+ processData: false
68
+ data: fd
69
+
70
+ runCommand: (cmd, path) ->
71
+ console.log "Running command", cmd, path
72
+
73
+ callback = (e) ->
74
+ console.log "Got change"
75
+ console.log e
76
+ true
77
+
78
+ # $.post( '/api/' + cmd )
79
+ $.ajax
80
+ type: 'post'
81
+ url: '/api/' + cmd
82
+ data: {path: path}
83
+ xhr: ->
84
+ xhr = new XMLHttpRequest()
85
+ xhr.onprogress = callback # process_cb
86
+ # xhr.upload.onprogress = process_cb
87
+ xhr
88
+
89
+
90
+ loadDrafts: ->
91
+ @loadUrl "/admin/drafts.json"
92
+
93
+ loadPublished: ->
94
+ @loadUrl "/admin/published.json"
@@ -0,0 +1,49 @@
1
+ @AdminNavbar = React.createClass
2
+ mixins: [Reflux.connect(commandResultStore)]
3
+
4
+ getDefaultProps: ->
5
+ path: ""
6
+ draft: false
7
+
8
+ getInitialState: ->
9
+ command: ""
10
+
11
+ render: ->
12
+ subnav = unless @props.path
13
+ <DashboardNavbar />
14
+ else
15
+ <EditorNavbar />
16
+
17
+ <Navbar brand={<a href="/admin">Blog Admin</a>} fixedTop>
18
+ {@commandResult()}
19
+ {subnav}
20
+ <CollapsibleNav>
21
+ <Nav navbar right>
22
+ <DropdownButton title='Site Commands'>
23
+ <NavItem onClick={runLater( 'diff', true )}>Diff</NavItem>
24
+ <NavItem onClick={runLater( 'status' )}>Git Status</NavItem>
25
+ <NavItem onClick={runLater( 'update' )}>Update</NavItem>
26
+ <NavItem onClick={runLater( 'build' ) }>Build</NavItem>
27
+ <NavItem onClick={runLater( 'deploy' )}>Deploy</NavItem>
28
+ </DropdownButton>
29
+ <NavItem href={"/" + @props.path}>Preview</NavItem>
30
+ </Nav>
31
+ </CollapsibleNav>
32
+ </Navbar>
33
+
34
+ closeResult: ->
35
+ @state.command = ""
36
+ @setState @state
37
+
38
+ commandResult: ->
39
+ return <span/> if @state.command == ""
40
+
41
+ <Modal title={@state.command} onRequestHide={@closeResult} bsSize='large' >
42
+ <div className='modal-body'>
43
+ <pre>{@state.result}</pre>
44
+ </div>
45
+ <div className='modal-footer'>
46
+ <Button onClick={@closeResult}>OK</Button>
47
+ </div>
48
+ </Modal>
49
+
@@ -0,0 +1,21 @@
1
+ @App = React.createClass
2
+ mixins: [Reflux.connect(pathStore)]
3
+
4
+ getInitialState: ->
5
+ loading: false
6
+ dirty: false
7
+ path: null
8
+ metadata: {}
9
+ markdown: ""
10
+
11
+ render: ->
12
+ mainPanel = unless @state.path
13
+ <Dashboard/>
14
+ else
15
+ <Editor/>
16
+
17
+ <div>
18
+ <AdminNavbar path={@state.path}/>
19
+ {mainPanel}
20
+ </div>
21
+
@@ -0,0 +1,60 @@
1
+ @AutosizeTextarea = React.createClass
2
+ getInitialState: ->
3
+ @shrink = 0
4
+ value: @props.value,
5
+ style:
6
+ boxSizing: "border-box"
7
+ minHeight: "31px"
8
+ overflowX: "hidden"
9
+ height: 50
10
+ resize: 'none'
11
+
12
+ componentWillReceiveProps: (props) ->
13
+ @state.value = props.value
14
+ @state.trigger_resize = true
15
+ @setState @state
16
+
17
+ componentWillUpdate: () ->
18
+ @position = React.findDOMNode( @refs.myInput ).selectionStart || @position
19
+ # console.log "Position", @position
20
+
21
+ componentDidUpdate: ->
22
+ # console.log "Setting position", @position
23
+ React.findDOMNode( @refs.myInput ).setSelectionRange( @position, @position )
24
+ if @state.trigger_resize
25
+ @state.trigger_resize = false
26
+ requestAnimationFrame =>
27
+ @resize React.findDOMNode( @refs.myInput )
28
+
29
+ inputHandler: (e)->
30
+ @resize( e.target )
31
+ if( @props.onChange )
32
+ @position = e.target.selectionStart
33
+ @props.onChange( e.target.value )
34
+
35
+ resize: (target) ->
36
+ unless @diff
37
+ @compStyle = window.getComputedStyle(target);
38
+ @diff = parseFloat(@compStyle.getPropertyValue('border-bottom-width')) + parseFloat(@compStyle.getPropertyValue('border-top-width'));
39
+
40
+ line_diff = target.value.length - @state.value.length
41
+ if line_diff < 0 # Removed content
42
+ @shrink = 1
43
+ setTimeout( @startShrinking, 0 )
44
+
45
+ new_height = target.scrollHeight + @diff - @shrink
46
+
47
+ if new_height >= @state.style.height
48
+ @shrink = 0
49
+
50
+ @state.value = target.value
51
+ @state.style.height = new_height
52
+ @setState @state
53
+
54
+ startShrinking: ->
55
+ # console.log "Shrinking"
56
+ @resize( React.findDOMNode( @refs.myInput ))
57
+ requestAnimationFrame( @startShrinking ) if @shrink != 0
58
+
59
+ render: ->
60
+ <textarea className="form-control" onChange={this.inputHandler} value={this.state.value} style={this.state.style} ref="myInput"/>
@@ -0,0 +1,8 @@
1
+ @Dashboard = React.createClass
2
+ render: ->
3
+ <div className="dashboard">
4
+ <div className="row">
5
+ <DashboardDraftList />
6
+ <DashboardPublishedList />
7
+ </div>
8
+ </div>
@@ -0,0 +1,19 @@
1
+ @DashboardDraftList = React.createClass
2
+ mixins: [Reflux.connect(draftStore)],
3
+
4
+ getInitialState: ->
5
+ drafts: []
6
+
7
+ componentDidMount: -> updateDraftList()
8
+
9
+ render: ->
10
+ drafts = @state.drafts.map (item) ->
11
+ <li key={item.path}><a onClick={viewPath.bind(this, item.path)}>{item.title}</a></li>
12
+
13
+ <div className="maincontent">
14
+ <h1>Drafts</h1>
15
+
16
+ <ul className="nav nav-pills nav-stacked">
17
+ {drafts}
18
+ </ul>
19
+ </div>
@@ -0,0 +1,51 @@
1
+ @DashboardNavbar = React.createClass
2
+ getInitialState: ->
3
+ newDraftModal: false
4
+ metadata:
5
+ title: ""
6
+ subtitle: ""
7
+ tags: ""
8
+
9
+ toggleModal: ->
10
+ @state.newDraftModal = !@state.newDraftModal
11
+ @state.metadata =
12
+ title: ""
13
+ subtitle: ""
14
+ tags: ""
15
+ @setState @state
16
+
17
+ updateMeta: (metadata) ->
18
+ @state.metadata = metadata
19
+ @setState @state
20
+
21
+ onCreateNewDraft: ->
22
+ console.log "Running create new draft"
23
+ @state.newDraftModal = false
24
+ createNewDraft( @state.metadata )
25
+
26
+ render: ->
27
+ <Nav navbar>
28
+ {@newDraftModal()}
29
+ <NavItem href='#' onClick={@toggleModal}>New Draft</NavItem>
30
+ </Nav>
31
+
32
+ closeModal: ->
33
+ @state.newDraftModal = false
34
+ @setState @state
35
+
36
+ updateMeta: (metadata) ->
37
+ @state.metadata = metadata
38
+ @setState @state
39
+
40
+ newDraftModal: ->
41
+ return <span/> unless @state.newDraftModal
42
+
43
+ <Modal title='New Draft' onRequestHide={@toggleModal}>
44
+ <div className='modal-body'>
45
+ <MetadataEditor metadata={@state.metadata} onChange={@updateMeta}/>
46
+ </div>
47
+ <div className='modal-footer'>
48
+ <Button onClick={@closeModal}>Cancel</Button>
49
+ <Button bsStyle='primary' onClick={@onCreateNewDraft} disabled={@state.metadata.title.length < 5}>New Draft</Button>
50
+ </div>
51
+ </Modal>
@@ -0,0 +1,19 @@
1
+ @DashboardPublishedList = React.createClass
2
+ mixins: [Reflux.connect(publishedStore)],
3
+
4
+ getInitialState: ->
5
+ articles: []
6
+
7
+ componentDidMount: -> updatePublishedList()
8
+
9
+ render: ->
10
+ articles = @state.articles.map (item) ->
11
+ <li key={item.path}><a onClick={viewPath.bind(this, item.path)}>{item.title}</a></li>
12
+
13
+ <div className="sidebar">
14
+ <h1>Published</h1>
15
+
16
+ <ul className="nav nav-pills nav-stacked">
17
+ {articles}
18
+ </ul>
19
+ </div>
@@ -0,0 +1,24 @@
1
+ @Editor = React.createClass
2
+ mixins: [Reflux.connect(pathStore)]
3
+
4
+ handleChange: (value) ->
5
+ updateMarkdown value
6
+ @restartTimer()
7
+
8
+ restartTimer: () ->
9
+ clearTimeout( @timer ) if( @timer )
10
+ @timer = setTimeout =>
11
+ saveCurrentArticle()
12
+ , 2000
13
+
14
+ render: ->
15
+ <div className="editor">
16
+ <div className="row">
17
+ <div className="editorPane">
18
+ <AutosizeTextarea value={@state.markdown} onChange={@handleChange}/>
19
+ </div>
20
+ <div className="previewPane">
21
+ <MarkdownPreview markdown={@state.markdown} />
22
+ </div>
23
+ </div>
24
+ </div>
@@ -0,0 +1,63 @@
1
+ @EditorNavbar = React.createClass
2
+ mixins: [Reflux.connect(pathStore)]
3
+
4
+ getInitialState: ->
5
+ metadata: {}
6
+ dirty: false
7
+ saving: false
8
+
9
+ toggleModal: ->
10
+ @state.newDraftModal = !@state.newDraftModal
11
+ console.log @state.newDraftModal
12
+ @setState @state
13
+
14
+ updateMeta: (metadata) ->
15
+ @state.metadata = metadata
16
+ @setState @state
17
+
18
+ updateMetadata: ->
19
+ console.log "Running update meta"
20
+ @state.newDraftModal = false
21
+ @setState @state
22
+ updateMetadata( @state.metadata )
23
+ saveCurrentArticle()
24
+
25
+ onPublish: ->
26
+ publishDraft( @state.path )
27
+
28
+ render: ->
29
+ metadata = for k,v of @state.metadata
30
+ <MenuItem onClick={@toggleModal} key={k}>
31
+ {k}: {v}
32
+ </MenuItem>
33
+
34
+ text = "Save"
35
+ text = "Saving..." if @state.saving
36
+
37
+ publish = <span/>
38
+
39
+ if @state.draft
40
+ publish = <NavItem href="#" onClick={@onPublish}>Publish Article</NavItem>
41
+
42
+ <Nav navbar>
43
+ {@newDraftModal()}
44
+ <NavItem href='#' onClick={saveCurrentArticle} disabled={!@state.dirty || @state.saving}>{text}</NavItem>
45
+ <DropdownButton title='Metadata'>
46
+ {metadata}
47
+ </DropdownButton>
48
+ {publish}
49
+ <NavItem disabled>{@state.path}</NavItem>
50
+ </Nav>
51
+
52
+ newDraftModal: ->
53
+ return <span/> unless @state.newDraftModal
54
+
55
+ <Modal title='Update' onRequestHide={@toggleModal}>
56
+ <div className='modal-body'>
57
+ <MetadataEditor metadata={@state.metadata} onChange={@updateMeta}/>
58
+ </div>
59
+ <div className='modal-footer'>
60
+ <Button onClick={@closeModal}>Cancel</Button>
61
+ <Button bsStyle='primary' onClick={@updateMetadata} disabled={@state.metadata.title.length < 5}>Update Data</Button>
62
+ </div>
63
+ </Modal>