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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +48 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/middleman-blog-ui.rb +12 -0
- data/lib/middleman/blog/ui/api_server.rb +169 -0
- data/lib/middleman/blog/ui/extension.rb +64 -0
- data/lib/middleman/blog/ui/version.rb +7 -0
- data/middleman-blog-ui.gemspec +38 -0
- data/source/admin/drafts.json.erb +6 -0
- data/source/admin/index.html.haml +18 -0
- data/source/admin/published.json.erb +6 -0
- data/source/javascripts/admin/admin.js +25 -0
- data/source/javascripts/admin/api.coffee +94 -0
- data/source/javascripts/admin/components/admin_navbar.js.coffee +49 -0
- data/source/javascripts/admin/components/app.js.coffee +21 -0
- data/source/javascripts/admin/components/autosize_textarea.js.coffee +60 -0
- data/source/javascripts/admin/components/dashboard.js.coffee +8 -0
- data/source/javascripts/admin/components/dashboard_draft_list.js.coffee +19 -0
- data/source/javascripts/admin/components/dashboard_navbar.js.coffee +51 -0
- data/source/javascripts/admin/components/dashboard_published_list.js.coffee +19 -0
- data/source/javascripts/admin/components/editor.js.coffee +24 -0
- data/source/javascripts/admin/components/editor_navbar.js.coffee +63 -0
- data/source/javascripts/admin/components/markdown_preview.js.coffee +9 -0
- data/source/javascripts/admin/components/metadata_editor.js.coffee +25 -0
- data/source/javascripts/admin/marked.min.js +6 -0
- data/source/javascripts/admin/react-bootstrap.min.js +10 -0
- data/source/javascripts/admin/react-bootstrap.min.js.map +1 -0
- data/source/javascripts/admin/reflux.min.js +1 -0
- data/source/javascripts/admin/stores/article.coffee +71 -0
- data/source/javascripts/admin/stores/command.coffee +83 -0
- data/source/javascripts/admin/stores/drafts.coffee +16 -0
- data/source/javascripts/admin/stores/published.coffee +16 -0
- data/source/javascripts/admin/superagent.js +1318 -0
- data/source/stylesheets/admin.css.scss +55 -0
- metadata +186 -0
@@ -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,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>
|