octodmin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.bowerrc +3 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +9 -0
  6. data/CHANGELOG.md +4 -0
  7. data/Gemfile +17 -0
  8. data/LICENSE.md +22 -0
  9. data/README.md +117 -0
  10. data/Rakefile +38 -0
  11. data/_config.yml +27 -0
  12. data/_deploy.yml +13 -0
  13. data/app/.DS_Store +0 -0
  14. data/app/assets/.DS_Store +0 -0
  15. data/app/assets/javascripts/.DS_Store +0 -0
  16. data/app/assets/javascripts/app.js.coffee +13 -0
  17. data/app/assets/javascripts/components/header.js.cjsx +69 -0
  18. data/app/assets/javascripts/components/posts.js.cjsx +309 -0
  19. data/app/assets/javascripts/components/router.js.cjsx +56 -0
  20. data/app/assets/stylesheets/.DS_Store +0 -0
  21. data/app/assets/stylesheets/app.scss +75 -0
  22. data/app/config/environment.rb +7 -0
  23. data/app/config/routes.rb +19 -0
  24. data/app/config/sprockets.rb +36 -0
  25. data/app/controllers/deploys/create.rb +20 -0
  26. data/app/controllers/frontend/index.rb +8 -0
  27. data/app/controllers/posts/create.rb +18 -0
  28. data/app/controllers/posts/destroy.rb +14 -0
  29. data/app/controllers/posts/index.rb +13 -0
  30. data/app/controllers/posts/restore.rb +14 -0
  31. data/app/controllers/posts/revert.rb +14 -0
  32. data/app/controllers/posts/show.rb +13 -0
  33. data/app/controllers/posts/update.rb +23 -0
  34. data/app/controllers/site/show.rb +12 -0
  35. data/app/controllers/syncs/create.rb +62 -0
  36. data/app/controllers/version/show.rb +12 -0
  37. data/app/octodmin.rb +56 -0
  38. data/app/public/.DS_Store +0 -0
  39. data/app/public/assets/app.css +7960 -0
  40. data/app/public/assets/app.css.gz +7960 -0
  41. data/app/public/assets/app.js +39181 -0
  42. data/app/public/assets/app.js.gz +39181 -0
  43. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.eot +0 -0
  44. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.eot.gz +0 -0
  45. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.svg +288 -0
  46. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.svg.gz +288 -0
  47. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf +0 -0
  48. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf.gz +0 -0
  49. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.woff +0 -0
  50. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.woff.gz +0 -0
  51. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 +0 -0
  52. data/app/public/assets/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2.gz +0 -0
  53. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  54. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.eot.gz +0 -0
  55. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  56. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.svg.gz +288 -0
  57. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  58. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.ttf.gz +0 -0
  59. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  60. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.woff.gz +0 -0
  61. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  62. data/app/public/assets/bootstrap/fonts/glyphicons-halflings-regular.woff2.gz +0 -0
  63. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.eot +0 -0
  64. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.eot.gz +0 -0
  65. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.svg +288 -0
  66. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.svg.gz +288 -0
  67. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf +0 -0
  68. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf.gz +0 -0
  69. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff +0 -0
  70. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff.gz +0 -0
  71. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 +0 -0
  72. data/app/public/assets/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2.gz +0 -0
  73. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.eot +0 -0
  74. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.eot.gz +0 -0
  75. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.svg +288 -0
  76. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.svg.gz +288 -0
  77. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.ttf +0 -0
  78. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.ttf.gz +0 -0
  79. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.woff +0 -0
  80. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.woff.gz +0 -0
  81. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.woff2 +0 -0
  82. data/app/public/assets/bootswatch/fonts/glyphicons-halflings-regular.woff2.gz +0 -0
  83. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.eot +0 -0
  84. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.eot.gz +0 -0
  85. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.svg +229 -0
  86. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.svg.gz +229 -0
  87. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.ttf +0 -0
  88. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.ttf.gz +0 -0
  89. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.woff +0 -0
  90. data/app/public/assets/bootswatch-dist/fonts/glyphicons-halflings-regular.woff.gz +0 -0
  91. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.eot +0 -0
  92. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.eot.gz +0 -0
  93. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.svg +565 -0
  94. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.svg.gz +565 -0
  95. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.ttf +0 -0
  96. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.ttf.gz +0 -0
  97. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.woff +0 -0
  98. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.woff.gz +0 -0
  99. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.woff2 +0 -0
  100. data/app/public/assets/fontawesome/fonts/fontawesome-webfont.woff2.gz +0 -0
  101. data/app/public/assets/manifest.json +1 -0
  102. data/app/templates/application.html.erb +13 -0
  103. data/app/templates/frontend/index.html.erb +1 -0
  104. data/app/views/application_layout.rb +5 -0
  105. data/app/views/deploys/create.rb +10 -0
  106. data/app/views/frontend/index.rb +5 -0
  107. data/app/views/posts/create.rb +10 -0
  108. data/app/views/posts/destroy.rb +10 -0
  109. data/app/views/posts/index.rb +10 -0
  110. data/app/views/posts/restore.rb +10 -0
  111. data/app/views/posts/revert.rb +10 -0
  112. data/app/views/posts/show.rb +10 -0
  113. data/app/views/posts/update.rb +10 -0
  114. data/app/views/site/show.rb +10 -0
  115. data/app/views/syncs/create.rb +10 -0
  116. data/app/views/version/show.rb +10 -0
  117. data/bower.json +16 -0
  118. data/config.ru +5 -0
  119. data/lib/octodmin/app.rb +2 -0
  120. data/lib/octodmin/post.rb +161 -0
  121. data/lib/octodmin/site.rb +61 -0
  122. data/lib/octodmin/version.rb +3 -0
  123. data/lib/octodmin.rb +7 -0
  124. data/octodmin.gemspec +24 -0
  125. data/sample/.gitignore +2 -0
  126. data/sample/_includes/footer.html +55 -0
  127. data/sample/_includes/head.html +12 -0
  128. data/sample/_includes/header.html +27 -0
  129. data/sample/_layouts/default.html +20 -0
  130. data/sample/_layouts/page.html +14 -0
  131. data/sample/_layouts/post.html +15 -0
  132. data/sample/_posts/2015-01-29-welcome-to-jekyll.markdown +25 -0
  133. data/sample/_posts/2015-01-30-test.markdown +10 -0
  134. data/sample/_sass/_base.scss +204 -0
  135. data/sample/_sass/_layout.scss +236 -0
  136. data/sample/_sass/_syntax-highlighting.scss +67 -0
  137. data/sample/_templates/draft +4 -0
  138. data/sample/_templates/page +4 -0
  139. data/sample/_templates/post +8 -0
  140. data/sample/about.md +11 -0
  141. data/sample/css/main.scss +52 -0
  142. data/sample/feed.xml +30 -0
  143. data/sample/index.html +23 -0
  144. data/spec/api/deploys_spec.rb +36 -0
  145. data/spec/api/posts_spec.rb +411 -0
  146. data/spec/api/site_spec.rb +18 -0
  147. data/spec/api/syncs_spec.rb +81 -0
  148. data/spec/api/version_spec.rb +14 -0
  149. data/spec/frontend/assets_spec.rb +23 -0
  150. data/spec/frontend/home_spec.rb +16 -0
  151. data/spec/spec_helper.rb +31 -0
  152. data/tmp/.keep +0 -0
  153. metadata +259 -0
@@ -0,0 +1,309 @@
1
+ # @cjsx React.DOM
2
+
3
+ @Posts = React.createClass(
4
+ propTypes:
5
+ site: React.PropTypes.object.isRequired
6
+
7
+ getInitialState: ->
8
+ { alert: null, loading: false, posts: null }
9
+
10
+ fetchPosts: ->
11
+ return if @state.loading
12
+ @setState(loading: true)
13
+ $.get("/api/posts").always(@handleResponse).done(@handleSuccess).fail(@handleError)
14
+
15
+ handleResponse: ->
16
+ @setState(loading: false) if @isMounted()
17
+
18
+ handleSuccess: (response) ->
19
+ @setState(alert: null, posts: response.posts) if @isMounted()
20
+
21
+ handleError: (error) ->
22
+ @setState(alert: "Could not load posts: #{error.statusText} (#{error.status})") if @isMounted()
23
+
24
+ componentWillMount: ->
25
+ @fetchPosts()
26
+ $(document).on("fetchPosts", @fetchPosts)
27
+ @timer = setInterval(@fetchPosts, 5000)
28
+
29
+ componentWillUnmount: ->
30
+ $(document).off("fetchPosts", @fetchPosts)
31
+ clearInterval(@timer)
32
+
33
+ render: ->
34
+ <div>
35
+ {<div className="alert alert-danger">{@state.alert}</div> if @state.alert}
36
+ <NewPostPartial site={@props.site} />
37
+
38
+ <Loader loaded={!!@state.posts}>
39
+ {@state.posts?.map(((post) ->
40
+ <PostPartial key={post.identifier} site={@props.site} post={post} />
41
+ ).bind(this))}
42
+ </Loader>
43
+
44
+ <footer className="row">
45
+ <div className="col-sm-3">
46
+ <iframe src="http://ghbtns.com/github-btn.html?user=krasnoukhov&repo=octodmin&type=watch&count=true" allowTransparency="true" frameBorder="0" scrolling="no" style={border: "none", overflow: "hidden", width: "170px", height: "20px"}></iframe>
47
+ </div>
48
+ <div className="col-sm-9" style={textAlign: "right"}>
49
+ <a href="https://github.com/krasnoukhov/octodmin" target="_blank">Octodmin</a>, open source project by <a href="http://www.krasnoukhov.com" target="_blank">Dmitry Krasnoukhov</a>
50
+ </div>
51
+ </footer>
52
+ </div>
53
+ )
54
+
55
+ @NewPostPartial = React.createClass(
56
+ mixins: [ReactRouter.Navigation]
57
+
58
+ propTypes:
59
+ site: React.PropTypes.object.isRequired
60
+
61
+ getInitialState: ->
62
+ { alert: null, loading: false }
63
+
64
+ form: ->
65
+ $(@refs.form.getDOMNode())
66
+
67
+ handleSubmit: (event) ->
68
+ event.preventDefault()
69
+ return if @state.loading
70
+
71
+ @setState(loading: true)
72
+ $.post("/api/posts", @form().serialize()).always(@handleResponse).done(@handleSuccess).fail(@handleError)
73
+
74
+ handleResponse: ->
75
+ @setState(loading: false)
76
+
77
+ handleSuccess: (response) ->
78
+ @setState(alert: null)
79
+ @form()[0].reset()
80
+ $(document).trigger("fetchPosts")
81
+ @transitionTo("post_edit", post_id: response.posts.identifier)
82
+
83
+ handleError: (error) ->
84
+ @setState(alert: error.responseJSON?.errors.join(", "))
85
+
86
+ render: ->
87
+ <div className="panel panel-default">
88
+ <div className="panel-body">
89
+ {<div className="alert alert-danger">{@state.alert}</div> if @state.alert}
90
+
91
+ <form ref="form" className="form-inline" onSubmit={@handleSubmit}>
92
+ <fieldset className="row" disabled={@state.loading}>
93
+ <div className="col-sm-9 form-group form-group-lg">
94
+ <input className="form-control" style={width: '100%'} name="title" placeholder="Type the title of your new post here" required />
95
+ </div>
96
+ <div className="col-sm-3 buttons">
97
+ <button type="submit" className="btn btn-lg btn-default">Create</button>
98
+ </div>
99
+ </fieldset>
100
+ </form>
101
+ </div>
102
+ </div>
103
+ )
104
+
105
+ @PostPartial = React.createClass(
106
+ mixins: [ReactRouter.Navigation]
107
+
108
+ propTypes:
109
+ site: React.PropTypes.object.isRequired
110
+ post: React.PropTypes.object.isRequired
111
+
112
+ getInitialState: ->
113
+ { alert: null, loading: false }
114
+
115
+ panelClass: ->
116
+ if @props.post.added
117
+ "success"
118
+ else if @props.post.changed
119
+ "warning"
120
+ else if @props.post.deleted
121
+ "danger"
122
+ else
123
+ "default"
124
+
125
+ handleEdit: ->
126
+ @transitionTo("post_edit", post_id: @props.post.identifier)
127
+
128
+ handleRevert: ->
129
+ return if @state.loading
130
+ @setState(loading: true)
131
+
132
+ $.ajax(type: "PATCH", url: "/api/posts/#{@props.post.identifier}/revert").
133
+ always(@handleResponse).
134
+ done(@handleSuccess).
135
+ fail(@handleError)
136
+
137
+ handleDelete: ->
138
+ if @props.post.added
139
+ return unless confirm("Are you sure? This can't be undone")
140
+
141
+ return if @state.loading
142
+ @setState(loading: true)
143
+
144
+ $.ajax(type: "DELETE", url: "/api/posts/#{@props.post.identifier}").
145
+ always(@handleResponse).
146
+ done(@handleSuccess).
147
+ fail(@handleError)
148
+
149
+ handleRestore: ->
150
+ return if @state.loading
151
+ @setState(loading: true)
152
+
153
+ $.ajax(type: "PATCH", url: "/api/posts/#{@props.post.identifier}/restore").
154
+ always(@handleResponse).
155
+ done(@handleSuccess).
156
+ fail(@handleError)
157
+
158
+ handleResponse: ->
159
+ @setState(loading: false)
160
+
161
+ handleSuccess: (response) ->
162
+ @setState(alert: null)
163
+ $(document).trigger("fetchPosts")
164
+
165
+ handleError: (error) ->
166
+ @setState(alert: "Could not load post: #{error.statusText} (#{error.status})")
167
+
168
+ render: ->
169
+ <div className="panel panel-#{@panelClass()}">
170
+ <div className="panel-heading clearfix">
171
+ <h3 className="panel-title pull-left">{@props.post.title}</h3>
172
+ <div className="pull-right">
173
+ {moment((new Date(@props.post.date)).toISOString()).format("LLL")}
174
+ </div>
175
+ </div>
176
+ <div className="panel-body">
177
+ {<div className="alert alert-danger">{@state.alert}</div> if @state.alert}
178
+
179
+ <div className="row">
180
+ <div className="col-sm-9 excerpt" dangerouslySetInnerHTML={{__html: @props.post.excerpt }} />
181
+ <div className="col-sm-3 buttons">
182
+ {if @props.post.deleted
183
+ <div className="btn-group btn-group-sm">
184
+ <button className="btn btn-default #{'disabled' if @state.loading}" onClick={@handleRestore}>Restore</button>
185
+ </div>
186
+ else
187
+ <div className="btn-group btn-group-sm">
188
+ <button className="btn btn-default #{'disabled' if @state.loading}" onClick={@handleEdit}>Edit</button>
189
+ {if @props.post.changed
190
+ <button className="btn btn-warning #{'disabled' if @state.loading}" onClick={@handleRevert}>Revert</button>
191
+ }
192
+ <button className="btn btn-danger #{'disabled' if @state.loading}" onClick={@handleDelete}>Delete</button>
193
+ </div>
194
+ }
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ )
200
+
201
+ @PostEdit = React.createClass(
202
+ mixins: [ReactRouter.State, ReactRouter.Navigation]
203
+
204
+ propTypes:
205
+ site: React.PropTypes.object.isRequired
206
+
207
+ getInitialState: ->
208
+ { alert: null, success: null, loading: false, post: null }
209
+
210
+ form: ->
211
+ $(@refs.form.getDOMNode())
212
+
213
+ frontMatter: ->
214
+ @props.site.octodmin.front_matter
215
+
216
+ fetchPost: ->
217
+ return if @state.loading
218
+ @setState(loading: true)
219
+ $.get("/api/posts/#{@getParams().post_id}").always(@handleResponse).done(@handleSuccess).fail(@handleError)
220
+
221
+ handleBack: (event) ->
222
+ event.preventDefault()
223
+ @transitionTo("app")
224
+
225
+ handleSubmit: (event) ->
226
+ event.preventDefault()
227
+ return if @state.loading
228
+ @setState(loading: true)
229
+
230
+ data = @form().serializeObject()
231
+ $.ajax(type: "PATCH", url: "/api/posts/#{@state.post.identifier}", data: data).
232
+ always(@handleResponse).
233
+ done(@handleFormSuccess).
234
+ fail(@handleError)
235
+
236
+ handleResponse: ->
237
+ @setState(loading: false)
238
+
239
+ handleSuccess: (response) ->
240
+ @setState(alert: null, post: response.posts)
241
+
242
+ handleFormSuccess: (response) ->
243
+ @setState(alert: null, success: "Post is updated")
244
+ setTimeout(@removeSuccess, 5000)
245
+ @transitionTo("post_edit", post_id: response.posts.identifier)
246
+
247
+ handleError: (error) ->
248
+ @setState(alert: error.responseJSON?.errors.join(", "))
249
+
250
+ removeSuccess: ->
251
+ @setState(success: null) if @isMounted()
252
+
253
+ componentWillMount: ->
254
+ @fetchPost()
255
+
256
+ componentDidUpdate: ->
257
+ return if !@state.post || !@refs.editor
258
+ return if !/markdown$/.test(@state.post.path) && !/md$/.test(@state.post.path)
259
+
260
+ $(@refs.editor.getDOMNode()).markdown(
261
+ autofocus: false
262
+ savable: false
263
+ iconlibrary: "fa"
264
+ resize: "vertical"
265
+ fullscreen:
266
+ enable: false
267
+ )
268
+
269
+ render: ->
270
+ <Loader loaded={!!@state.post}>
271
+ {<div className="alert alert-danger">{@state.alert}</div> if @state.alert}
272
+ {<div className="alert alert-success">{@state.success}</div> if @state.success}
273
+
274
+ {if @state.post
275
+ <form ref="form" className="form-horizontal post-edit" onSubmit={@handleSubmit}>
276
+ <fieldset disabled={@state.loading}>
277
+ <div className="form-group">
278
+ <div className="col-sm-8">
279
+ <input type={@frontMatter().title.type} className="form-control" name="title" placeholder="Title" defaultValue={@state.post.title} required />
280
+ </div>
281
+ <div className="col-sm-4">
282
+ <input type={@frontMatter().date.type} className="form-control" name="date" placeholder="Date" defaultValue={@state.post.date} required />
283
+ </div>
284
+ </div>
285
+
286
+ <textarea ref="editor" className="md-content" name="content" rows="15" defaultValue={@state.post.content} placeholder="Post content" required></textarea>
287
+
288
+ {Object.keys(@frontMatter()).map(((key) ->
289
+ if key != "title" && key != "date"
290
+ config = @frontMatter()[key]
291
+ title = key.charAt(0).toUpperCase() + key.slice(1)
292
+
293
+ <div key={key} className="form-group">
294
+ <label htmlFor={key} className="col-sm-1 control-label">{title}</label>
295
+ <div className="col-sm-11">
296
+ <input type={config.type} className="form-control" name={key} placeholder={title} defaultValue={@state.post[key]} />
297
+ </div>
298
+ </div>
299
+ ).bind(this))}
300
+
301
+ <div className="buttons">
302
+ <button type="submit" className="btn btn-lg btn-success pull-right">Save</button>
303
+ <button className="btn btn-lg btn-default pull-right" onClick={@handleBack}>Back</button>
304
+ </div>
305
+ </fieldset>
306
+ </form>
307
+ }
308
+ </Loader>
309
+ )
@@ -0,0 +1,56 @@
1
+ # @cjsx React.DOM
2
+
3
+ @Router = ReactRouter
4
+ @Link = Router.Link
5
+ @Routes = Router.Routes
6
+ @Route = Router.Route
7
+ @DefaultRoute = Router.DefaultRoute
8
+ @RouteHandler = Router.RouteHandler
9
+
10
+ @Container = React.createClass(
11
+ render: ->
12
+ <div className="container">
13
+ {@props.children}
14
+ </div>
15
+ )
16
+
17
+ @App = React.createClass(
18
+ getInitialState: ->
19
+ { site: null }
20
+
21
+ fetchSite: ->
22
+ $.get("/api/site").done(@handleSuccess).fail(@handleError)
23
+
24
+ handleSuccess: (response) ->
25
+ @setState(site: response.sites)
26
+ $("title").text("Octodmin – #{response.sites.title}")
27
+
28
+ handleError: (error) ->
29
+ alert("Could not load site: #{error.statusText} (#{error.status})")
30
+
31
+ componentWillMount: ->
32
+ @fetchSite()
33
+ $(document).on("fetchSite", @fetchSite)
34
+
35
+ componentWillUnmount: ->
36
+ $(document).off("fetchSite", @fetchSite)
37
+
38
+ render: ->
39
+ <Loader loaded={!!@state.site}>
40
+ <Container>
41
+ <Header site={@state.site} />
42
+ <RouteHandler site={@state.site} />
43
+ </Container>
44
+ </Loader>
45
+ )
46
+
47
+ routes =
48
+ <Route path="/" name="app" handler={@App}>
49
+ <Route path="/posts/:post_id/edit" name="post_edit" handler={@PostEdit} />
50
+ <DefaultRoute handler={@Posts} />
51
+ </Route>
52
+
53
+
54
+ Router.run(routes, Router.HistoryLocation, (Handler) ->
55
+ React.render(<Handler />, document.getElementById("app"))
56
+ )
Binary file
@@ -0,0 +1,75 @@
1
+ @charset "utf-8";
2
+ $fa-font-path: "/assets/fontawesome/fonts";
3
+ @import "bootswatch/paper/variables";
4
+ $icon-font-path: "/assets/bootstrap/fonts/";
5
+ //= require bootstrap-markdown
6
+ @import
7
+ "bootstrap-sass/assets/stylesheets/bootstrap",
8
+ "bootswatch/paper/bootswatch",
9
+ "fontawesome/scss/font-awesome"
10
+ ;
11
+
12
+ body {
13
+ padding-bottom: 20px;
14
+ }
15
+
16
+ a, button, input, textarea, *:focus {
17
+ outline: 0 !important;
18
+ outline-style: none !important;
19
+ }
20
+
21
+ .navbar {
22
+ margin-bottom: 20px;
23
+
24
+ .navbar-right {
25
+ margin-right: 0;
26
+
27
+ .btn {
28
+ margin-left: 10px;
29
+ }
30
+ }
31
+ }
32
+
33
+ .panel {
34
+ .panel-heading {
35
+ line-height: 26px;
36
+
37
+ h3 {
38
+ font-size: 26px;
39
+ }
40
+ }
41
+
42
+ .panel-body {
43
+ .excerpt {
44
+ :first-child {
45
+ margin-top: 0;
46
+ }
47
+
48
+ :last-child {
49
+ margin-bottom: 0;
50
+ }
51
+ }
52
+
53
+ .buttons {
54
+ text-align: center;
55
+ }
56
+ }
57
+ }
58
+
59
+ .post-edit {
60
+ .md-editor {
61
+ .md-header {
62
+ margin-bottom: 15px;
63
+ }
64
+ }
65
+
66
+ .md-content {
67
+ width: 100%;
68
+ }
69
+
70
+ .buttons {
71
+ .btn {
72
+ margin-left: 10px;
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,7 @@
1
+ require "lotus/setup"
2
+ ENV["RACK_ENV"] = ENV["LOTUS_ENV"] ||= "development"
3
+
4
+ require_relative "../octodmin"
5
+ Lotus::Container.configure do
6
+ mount Octodmin::App, at: "/"
7
+ end
@@ -0,0 +1,19 @@
1
+ namespace "api" do
2
+ resource :version, only: [:show]
3
+ resource :site, only: [:show]
4
+ resources :posts do
5
+ member do
6
+ patch :restore
7
+ patch :revert
8
+ end
9
+ end
10
+ resources :syncs, only: [:create]
11
+ resources :deploys, only: [:create]
12
+ end
13
+
14
+ if ENV["LOTUS_ENV"] != "production"
15
+ mount Octodmin.sprockets, at: "/assets"
16
+ end
17
+
18
+ get "/posts*", to: "frontend#index"
19
+ get "/", to: "frontend#index", as: :home
@@ -0,0 +1,36 @@
1
+ require "sprockets"
2
+ require "coffee_script"
3
+ require "coffee-react"
4
+ require "bower"
5
+
6
+ module Octodmin
7
+ module CjsxProcessor
8
+ VERSION = '1'
9
+ SOURCE_VERSION = ::CoffeeScript::Source.version
10
+
11
+ def self.cache_key
12
+ @cache_key ||= [name, SOURCE_VERSION, VERSION].freeze
13
+ end
14
+
15
+ def self.call(input)
16
+ data = input[:data]
17
+ input[:cache].fetch(self.cache_key + [data]) do
18
+ ::CoffeeScript.compile(::CoffeeReact.transform(data))
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.sprockets
24
+ assets_path = File.expand_path("../../assets", __FILE__)
25
+ tmp_path = File.expand_path("../../../tmp", __FILE__)
26
+
27
+ sprockets = ::Sprockets::Environment.new
28
+ sprockets.append_path "#{assets_path}/stylesheets"
29
+ sprockets.append_path "#{assets_path}/javascripts"
30
+ sprockets.append_path "#{assets_path}/fonts"
31
+ sprockets.append_path Bower.environment.directory
32
+ sprockets.register_engine ".cjsx", CjsxProcessor
33
+ sprockets.cache = Sprockets::Cache::FileStore.new(tmp_path)
34
+ sprockets
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ module Octodmin::Controllers::Deploys
2
+ class Create
3
+ include Octodmin::Action
4
+ expose :message
5
+
6
+ def call(params)
7
+ self.format = :json
8
+
9
+ site = Octodmin::Site.new
10
+ site.process
11
+
12
+ options = site.config["octodmin"]["deploys"].first
13
+ Octopress::Deploy.push(options)
14
+
15
+ @message = "Deployed successfully"
16
+ rescue SystemExit => e
17
+ halt 400, JSON.dump(errors: [e.message])
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ module Octodmin::Controllers::Frontend
2
+ class Index
3
+ include Octodmin::Action
4
+
5
+ def call(params)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ module Octodmin::Controllers::Posts
2
+ class Create
3
+ include Octodmin::Action
4
+ expose :post
5
+
6
+ params do
7
+ param :title, presence: true
8
+ end
9
+
10
+ def call(params)
11
+ self.format = :json
12
+ halt 400, JSON.dump(errors: ["Required param `title` is not specified"]) unless params.valid?
13
+
14
+ @post = Octodmin::Post.create(title: params[:title])
15
+ halt 400, JSON.dump(errors: ["Post with specified `title` already exists"]) unless @post
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module Octodmin::Controllers::Posts
2
+ class Destroy
3
+ include Octodmin::Action
4
+ expose :post
5
+
6
+ def call(params)
7
+ self.format = :json
8
+
9
+ @post = Octodmin::Post.find(params[:id])
10
+ halt 400, JSON.dump(errors: ["Could not find post"]) unless @post
11
+ @post.delete
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Octodmin::Controllers::Posts
2
+ class Index
3
+ include Octodmin::Action
4
+ expose :posts
5
+
6
+ def call(params)
7
+ self.format = :json
8
+
9
+ site = Octodmin::Site.new
10
+ @posts = site.posts
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Octodmin::Controllers::Posts
2
+ class Restore
3
+ include Octodmin::Action
4
+ expose :post
5
+
6
+ def call(params)
7
+ self.format = :json
8
+
9
+ @post = Octodmin::Post.find(params[:id])
10
+ halt 400, JSON.dump(errors: ["Could not find post"]) unless @post
11
+ @post.restore
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Octodmin::Controllers::Posts
2
+ class Revert
3
+ include Octodmin::Action
4
+ expose :post
5
+
6
+ def call(params)
7
+ self.format = :json
8
+
9
+ @post = Octodmin::Post.find(params[:id])
10
+ halt 400, JSON.dump(errors: ["Could not find post"]) unless @post
11
+ @post.revert
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Octodmin::Controllers::Posts
2
+ class Show
3
+ include Octodmin::Action
4
+ expose :post
5
+
6
+ def call(params)
7
+ self.format = :json
8
+
9
+ @post = Octodmin::Post.find(params[:id])
10
+ halt 400, JSON.dump(errors: ["Could not find post"]) unless @post
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module Octodmin::Controllers::Posts
2
+ class Update
3
+ include Octodmin::Action
4
+ expose :post
5
+
6
+ params do
7
+ param :layout, presence: true
8
+ param :title, presence: true
9
+ param :slug, presence: true
10
+ param :date, presence: true
11
+ param :content, presence: true
12
+ end
13
+
14
+ def call(params)
15
+ self.format = :json
16
+
17
+ @post = Octodmin::Post.find(params.env["router.params"][:id])
18
+ halt 400, JSON.dump(errors: ["Could not find post"]) unless @post
19
+ halt 400, JSON.dump(errors: ["Required params are not specified"]) unless params.valid?
20
+ @post.update(params.env["rack.request.form_hash"].dup)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ module Octodmin::Controllers::Site
2
+ class Show
3
+ include Octodmin::Action
4
+ expose :site
5
+
6
+ def call(params)
7
+ self.format = :json
8
+
9
+ @site = Octodmin::Site.new
10
+ end
11
+ end
12
+ end