tent-status 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.
Files changed (93) hide show
  1. data/.gitignore +21 -0
  2. data/.kick +8 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +232 -0
  5. data/LICENSE.txt +22 -0
  6. data/Procfile +1 -0
  7. data/README.md +14 -0
  8. data/Rakefile +43 -0
  9. data/assets/images/.gitkeep +0 -0
  10. data/assets/images/chosen-sprite.png +0 -0
  11. data/assets/images/glyphicons-halflings-white.png +0 -0
  12. data/assets/images/glyphicons-halflings.png +0 -0
  13. data/assets/javascripts/.gitkeep +0 -0
  14. data/assets/javascripts/application.js.coffee +122 -0
  15. data/assets/javascripts/backbone.js +1443 -0
  16. data/assets/javascripts/backbone_sync.js.coffee +60 -0
  17. data/assets/javascripts/boot.js.coffee +3 -0
  18. data/assets/javascripts/chosen.jquery.js +1129 -0
  19. data/assets/javascripts/collections/.gitkeep +0 -0
  20. data/assets/javascripts/collections/followers.js.coffee +5 -0
  21. data/assets/javascripts/collections/followings.js.coffee +5 -0
  22. data/assets/javascripts/collections/groups.js.coffee +5 -0
  23. data/assets/javascripts/collections/posts.js.coffee +5 -0
  24. data/assets/javascripts/fetch_pool.js.coffee +25 -0
  25. data/assets/javascripts/helpers/.gitkeep +0 -0
  26. data/assets/javascripts/helpers/formatting.js.coffee +17 -0
  27. data/assets/javascripts/hogan.js +706 -0
  28. data/assets/javascripts/http.js.coffee +18 -0
  29. data/assets/javascripts/jquery.js +9301 -0
  30. data/assets/javascripts/models/.gitkeep +0 -0
  31. data/assets/javascripts/models/follower.js.coffee +27 -0
  32. data/assets/javascripts/models/following.js.coffee +27 -0
  33. data/assets/javascripts/models/group.js.coffee +4 -0
  34. data/assets/javascripts/models/post.js.coffee +32 -0
  35. data/assets/javascripts/models/profile.js.coffee +30 -0
  36. data/assets/javascripts/moment.js +1106 -0
  37. data/assets/javascripts/paginator.js.coffee +67 -0
  38. data/assets/javascripts/router.js.coffee +71 -0
  39. data/assets/javascripts/routers/.gitkeep +0 -0
  40. data/assets/javascripts/routers/followers.js.coffee +14 -0
  41. data/assets/javascripts/routers/followings.js.coffee +14 -0
  42. data/assets/javascripts/routers/posts.js.coffee +98 -0
  43. data/assets/javascripts/templates/.gitkeep +0 -0
  44. data/assets/javascripts/templates/_follower.js.mustache.slim +17 -0
  45. data/assets/javascripts/templates/_following.js.mustache.slim +17 -0
  46. data/assets/javascripts/templates/_new_post_form.js.mustache.slim +10 -0
  47. data/assets/javascripts/templates/_post.js.mustache.slim +10 -0
  48. data/assets/javascripts/templates/_post_inner.js.mustache.slim +71 -0
  49. data/assets/javascripts/templates/_profile_stats.js.mustache.slim +11 -0
  50. data/assets/javascripts/templates/_reply_form.js.mustache.slim +13 -0
  51. data/assets/javascripts/templates/conversation.js.mustache.slim +13 -0
  52. data/assets/javascripts/templates/followers.js.mustache.slim +19 -0
  53. data/assets/javascripts/templates/followings.js.mustache.slim +22 -0
  54. data/assets/javascripts/templates/posts.js.mustache.slim +25 -0
  55. data/assets/javascripts/templates/profile.js.mustache.slim +39 -0
  56. data/assets/javascripts/underscore.js +1059 -0
  57. data/assets/javascripts/view.js.coffee +140 -0
  58. data/assets/javascripts/views/.gitkeep +0 -0
  59. data/assets/javascripts/views/container.js.coffee +6 -0
  60. data/assets/javascripts/views/expanding_textarea.js.coffee +14 -0
  61. data/assets/javascripts/views/fetch_posts_pool.js.coffee +61 -0
  62. data/assets/javascripts/views/follower_groups_form.js.coffee +26 -0
  63. data/assets/javascripts/views/followers.js.coffee +28 -0
  64. data/assets/javascripts/views/following_groups_form.js.coffee +25 -0
  65. data/assets/javascripts/views/followings.js.coffee +27 -0
  66. data/assets/javascripts/views/new_following_form.js.coffee +15 -0
  67. data/assets/javascripts/views/new_post_form.js.coffee +178 -0
  68. data/assets/javascripts/views/post.js.coffee +139 -0
  69. data/assets/javascripts/views/posts.js.coffee +55 -0
  70. data/assets/javascripts/views/posts/conversation.js.coffee +18 -0
  71. data/assets/javascripts/views/profile.js.coffee +29 -0
  72. data/assets/javascripts/views/profile_follow_button.js.coffee +29 -0
  73. data/assets/javascripts/views/profile_stats.js.coffee +38 -0
  74. data/assets/javascripts/views/remove_follower_btn.js.coffee +18 -0
  75. data/assets/javascripts/views/reply_post_form.js.coffee +30 -0
  76. data/assets/javascripts/views/unfollow_btn.js.coffee +18 -0
  77. data/assets/stylesheets/.gitkeep +0 -0
  78. data/assets/stylesheets/application.css.sass +117 -0
  79. data/assets/stylesheets/bootstrap-responsive.css +1040 -0
  80. data/assets/stylesheets/bootstrap.css.erb +5624 -0
  81. data/assets/stylesheets/chosen.css.erb +397 -0
  82. data/config.ru +14 -0
  83. data/config/asset_sync.rb +12 -0
  84. data/config/evergreen.rb +16 -0
  85. data/lib/tent-status.rb +6 -0
  86. data/lib/tent-status/app.rb +263 -0
  87. data/lib/tent-status/models/user.rb +39 -0
  88. data/lib/tent-status/sprockets/environment.rb +25 -0
  89. data/lib/tent-status/sprockets/helpers.rb +5 -0
  90. data/lib/tent-status/views/application.slim +69 -0
  91. data/lib/tent-status/views/auth.slim +8 -0
  92. data/tent-status.gemspec +34 -0
  93. metadata +415 -0
@@ -0,0 +1,67 @@
1
+ class TentStatus.Paginator
2
+ sinceId: null
3
+ limit: TentStatus.PER_PAGE || 50
4
+ onLastPage: false
5
+ isPaginator: true
6
+
7
+ constructor: (@collection, @options = {}) ->
8
+ @url = @options.url if @options.url
9
+ @url ||= @collection.url?()
10
+ @url ||= @collection.url
11
+
12
+ fetch: (options = {}) =>
13
+ sinceId = @sinceId
14
+ limit = @limit
15
+
16
+ @trigger 'fetch:start'
17
+
18
+ loadedCount = @collection.length
19
+ expectedCount = loadedCount + limit
20
+
21
+ _options = {
22
+ url: @urlForOffsetAndLimit(sinceId, limit)
23
+ add: true
24
+ success: (items) =>
25
+ if (loadedCount == @collection.length) or (@collection.length < expectedCount)
26
+ @onLastPage = true
27
+ @sinceId = items.last()?.get('id')
28
+ @options.success?()
29
+ @trigger 'fetch:success'
30
+ error: =>
31
+ @trigger 'fetch:error'
32
+ }
33
+ options = _.extend _options, options
34
+
35
+ @collection.fetch(options)
36
+
37
+ urlForOffsetAndLimit: (sinceId, limit) =>
38
+ separator = if @url.indexOf("?") != -1 then "&" else "?"
39
+ @url + separator + @serializeParams(@paramsForOffsetAndLimit sinceId, limit)
40
+
41
+ serializeParams: (params = {}) =>
42
+ res = []
43
+ for k, v of params
44
+ continue if @url.match("#{k}=")
45
+ res.push "#{k}=#{v}"
46
+ res.join("&")
47
+
48
+ paramsForOffsetAndLimit: (sinceId, limit) =>
49
+ params = { limit: limit }
50
+ params.before_id = sinceId if sinceId
51
+ params
52
+
53
+ nextPage: =>
54
+ @fetch()
55
+
56
+ toJSON: => @collection.toJSON()
57
+ toArray: => @collection.toArray()
58
+ find: => @collection.find(arguments...)
59
+ filter: => @collection.filter(arguments...)
60
+ sortBy: => @collection.sortBy(arguments...)
61
+ get: => @collection.get(arguments...)
62
+ unshift: => @collection.unshift(arguments...)
63
+ push: => @collection.push(arguments...)
64
+ first: => @collection.first(arguments...)
65
+ last: => @collection.last(arguments...)
66
+
67
+ _.extend TentStatus.Paginator::, Backbone.Events
@@ -0,0 +1,71 @@
1
+ # All routers should extend TentStatus.Router
2
+ # Any abstractions should go in here
3
+ class TentStatus.Router extends Backbone.Router
4
+ keyForAction: (actionName) =>
5
+ "#{@routerKey}:#{actionName}"
6
+
7
+ setCurrentAction: (actionName, actionFn) =>
8
+ if TentStatus.freezeRouter
9
+ TentStatus.on 'router:unfreeze', (shouldPlay) => @setCurrentAction(actionName, actionFn) if shouldPlay
10
+
11
+ key = @keyForAction(actionName)
12
+
13
+ @_dataFetches ||= {}
14
+ @_dataFetches[actionName] = 0
15
+
16
+ @view?.empty()
17
+
18
+ @currentActionName = actionName
19
+
20
+ window.scrollTo window.scrollX, 0
21
+
22
+ TentStatus.setPageTitle? key
23
+ TentStatus.setCurrentRoute? @, actionName
24
+
25
+ actionFn()
26
+
27
+ isCurrentAction: (actionName) =>
28
+ return true # currentRoute not currently setup, TODO set this up
29
+ TentStatus.currentRoute?.key == @keyForAction(actionName)
30
+
31
+ fetchData: (dataKey, dataFn) =>
32
+ actionName = @currentActionName
33
+
34
+ if dataFn.length == 0
35
+ @_fetchData(dataKey, dataFn(), actionName)
36
+ else
37
+ dataFn (res) => @_fetchData(dataKey, res, actionName)
38
+
39
+ _fetchData: (dataKey, res, actionName) =>
40
+ loaded = =>
41
+ @view?.set dataKey, res[dataKey]
42
+
43
+ # yield for other fetchData calls to start
44
+ setTimeout (=> @fetchSuccess actionName), 0
45
+
46
+ if res.loaded is true
47
+ @fetchStart actionName
48
+ loaded()
49
+ else
50
+ res[dataKey].on 'fetch:start', (=> @fetchStart actionName)
51
+ res[dataKey].on 'fetch:success', loaded
52
+ if res[dataKey].isPaginator is true
53
+ # TentStatus.Paginator triggers 'fetch:start' and 'fetch:success' events
54
+ res[dataKey].fetch()
55
+ else
56
+ res[dataKey].trigger 'fetch:start'
57
+ res[dataKey].fetch success: (=> res[dataKey].trigger 'fetch:success')
58
+
59
+ fetchStart: (actionName) =>
60
+ return unless @isCurrentAction(actionName)
61
+ @_dataFetches[actionName]++
62
+ TentStatus.Views.loading?.show()
63
+
64
+ fetchSuccess: (actionName) =>
65
+ return unless @isCurrentAction(actionName)
66
+ @_dataFetches[actionName]-- unless @_dataFetches[actionName] == 0
67
+ if @_dataFetches[actionName] == 0
68
+ @view?.once 'ready', =>
69
+ TentStatus.Views.loading?.hide()
70
+ @view?.render()
71
+
File without changes
@@ -0,0 +1,14 @@
1
+ TentStatus.Routers.followers = new class Followers extends TentStatus.Router
2
+ routerKey: 'followers'
3
+
4
+ routes:
5
+ "followers" : "index"
6
+
7
+ index: =>
8
+ return if TentStatus.guest_authenticated || !TentStatus.authenticated
9
+ @view = new TentStatus.Views.Followers
10
+ @setCurrentAction 'index', =>
11
+ @fetchData 'followers', =>
12
+ { followers: new TentStatus.Paginator( TentStatus.Collections.followers ), loaded: false }
13
+ @fetchData 'groups', =>
14
+ { groups: new TentStatus.Paginator( TentStatus.Collections.groups ), loaded: false }
@@ -0,0 +1,14 @@
1
+ TentStatus.Routers.followings = new class FollowingsRouter extends TentStatus.Router
2
+ routerKey: 'followings'
3
+
4
+ routes:
5
+ "followings" : "index"
6
+
7
+ index: =>
8
+ return if TentStatus.guest_authenticated || !TentStatus.authenticated
9
+ @view = new TentStatus.Views.Followings
10
+ @setCurrentAction 'index', =>
11
+ @fetchData 'groups', =>
12
+ { groups: new TentStatus.Paginator( TentStatus.Collections.groups ), loaded: false }
13
+ @fetchData 'followings', =>
14
+ { followings: new TentStatus.Paginator( TentStatus.Collections.followings ), loaded: false }
@@ -0,0 +1,98 @@
1
+ TentStatus.Routers.posts = new class PostsRouter extends TentStatus.Router
2
+ routerKey: 'posts'
3
+
4
+ routes:
5
+ "" : "root"
6
+ "profile" : "myProfile"
7
+ "posts" : "index"
8
+ "entities/:entity" : "profile"
9
+ "entities/:entity/:post_id" : "conversation"
10
+
11
+ index: =>
12
+ unless TentStatus.current_entity == TentStatus.domain_entity
13
+ @profile(encodeURIComponent(TentStatus.domain_entity))
14
+ return
15
+ @view = new TentStatus.Views.Posts
16
+ window.view = @view
17
+ @setCurrentAction 'index', =>
18
+ @fetchData 'posts', =>
19
+ { posts: new TentStatus.Paginator( TentStatus.Collections.posts ), loaded: false }
20
+ @fetchData 'followers', =>
21
+ { followers: new TentStatus.Paginator( TentStatus.Collections.followers ), loaded: false }
22
+ @fetchData 'followings', =>
23
+ { followings: new TentStatus.Paginator( TentStatus.Collections.followings ), loaded: false }
24
+ @fetchData 'profile', =>
25
+ { profile: TentStatus.Models.profile, loaded: false }
26
+
27
+ root: => @index(arguments...)
28
+
29
+ conversation: (entity, post_id) =>
30
+ @view = new TentStatus.Views.Conversation
31
+ @setCurrentAction 'conversation', =>
32
+ @fetchData 'post', =>
33
+ _post = new TentStatus.Models.Post { id: post_id }
34
+ { post: _post, loaded: false }
35
+
36
+ @fetchData 'posts', =>
37
+ query_string = "mentioned_entity=#{entity}&mentioned_post=#{post_id}"
38
+ options =
39
+ url: "#{(new TentStatus.Collections.Posts).url}?#{query_string}"
40
+
41
+ _posts = new TentStatus.Paginator( new TentStatus.Collections.Posts, options )
42
+ { posts: _posts, loaded: false }
43
+
44
+ @fetchData 'followers', =>
45
+ { followers: new TentStatus.Paginator( TentStatus.Collections.followers ), loaded: false }
46
+ @fetchData 'followings', =>
47
+ { followings: new TentStatus.Paginator( TentStatus.Collections.followings ), loaded: false }
48
+ @fetchData 'profile', =>
49
+ { profile: TentStatus.Models.profile, loaded: false }
50
+
51
+ myProfile: =>
52
+ @profile(TentStatus.current_entity)
53
+
54
+ profile: (entity) =>
55
+ @view = new TentStatus.Views.Profile
56
+ @setCurrentAction 'profile', =>
57
+ @fetchData 'currentProfile', (callback) =>
58
+ _loadedCalled = 0
59
+ _loaded = (data) =>
60
+ return unless data
61
+ return if _loadedCalled
62
+ _loadedCalled = true
63
+
64
+ _profile = new TentStatus.Models.Profile data
65
+ callback({ currentProfile: _profile, loaded: true })
66
+
67
+ _following = new TentStatus.Models.Following entity: decodeURIComponent(entity)
68
+ _following.fetch
69
+ url: "#{TentStatus.api_root}/followings?entity=#{entity}"
70
+ success: =>
71
+ return _loaded(false) unless _profile = _following.toJSON()[0]?.profile
72
+ _loaded _.extend({follow_type: 'followings'}, _profile)
73
+
74
+ _follower = new TentStatus.Models.Follower entity: decodeURIComponent(entity)
75
+ _follower.fetch
76
+ url: "#{TentStatus.api_root}/followers?entity=#{entity}"
77
+ success: =>
78
+ return _loaded(false) unless _profile = _follower.toJSON()[0]?.profile
79
+ _loaded _.extend({follow_type: 'followers'}, _profile)
80
+
81
+ @fetchData 'profile', (profileCallback) =>
82
+ TentStatus.Models.profile.fetch
83
+ success: (profile) =>
84
+ if profile.entity() == decodeURIComponent(entity)
85
+ _loadedCalled = true
86
+ callback({ currentProfile: profile, loaded: true })
87
+ profileCallback({ profile: TentStatus.Models.profile, loaded: true })
88
+
89
+ @fetchData 'posts', (callback) =>
90
+ options = { url: "#{TentStatus.api_root}/posts?entity=#{entity}" }
91
+ _posts = new TentStatus.Paginator( new TentStatus.Collections.Posts, options )
92
+ callback({ posts: _posts, loaded: false })
93
+
94
+ @fetchData 'followers', =>
95
+ { followers: new TentStatus.Paginator( TentStatus.Collections.followers ), loaded: false }
96
+ @fetchData 'followings', =>
97
+ { followings: new TentStatus.Paginator( TentStatus.Collections.followings ), loaded: false }
98
+
File without changes
@@ -0,0 +1,17 @@
1
+ tr
2
+ td
3
+ =="{{#avatar}}"
4
+ a href='{{entity}}'
5
+ img.img-rounded src='{{avatar}}' style='height:50px;width:50px;margin-right:4px;'
6
+ =="{{/avatar}}"
7
+ a href='{{entity}}'
8
+ =="{{name}}"
9
+ td =="{{entity}}"
10
+ td
11
+ form data-view='FollowerGroupsForm' data-follower-id='{{id}}' method='POST'
12
+ select multiple=true name='groups' data-placeholder='Select some groups'
13
+ =="{{#groups}}"
14
+ |<option value='{{id}}' {{#selected}}selected='selected'{{/selected}}>{{name}}</option>
15
+ =="{{/groups}}"
16
+ td
17
+ .btn.btn-danger data-view='RemoveFollowerBtn' data-id='{{id}}' data-confirm='Remove {{name}}?' Remove
@@ -0,0 +1,17 @@
1
+ tr
2
+ td
3
+ =="{{#avatar}}"
4
+ a href='{{entity}}'
5
+ img.img-rounded src='{{avatar}}' style='height:50px;width:50px;margin-right:4px;'
6
+ =="{{/avatar}}"
7
+ a href='{{entity}}'
8
+ =="{{name}}"
9
+ td =="{{entity}}"
10
+ td
11
+ form data-view='FollowingGroupsForm' data-following-id='{{id}}' method='POST'
12
+ select multiple=true name='groups' data-placeholder='Select some groups'
13
+ =="{{#groups}}"
14
+ |<option value='{{id}}' {{#selected}}selected='selected'{{/selected}}>{{name}}</option>
15
+ =="{{/groups}}"
16
+ td
17
+ .btn.btn-danger data-view='UnfollowBtn' data-id='{{id}}' data-confirm='Unfollow {{name}}?' Unfollow
@@ -0,0 +1,10 @@
1
+ | {{#authenticated}}
2
+ form.form-horizontal data-view='NewPostForm' method='POST'
3
+ .span6
4
+ .alert.alert-error &nbsp;
5
+
6
+ textarea.span6 name='text' placeholder="What's on your mind?" rows='3'
7
+ span.char-limit 140
8
+
9
+ input.btn.btn-success.pull-right type='submit' value='Post'
10
+ | {{/authenticated}}
@@ -0,0 +1,10 @@
1
+ | {{#isValid}}
2
+ | {{^isRepost}}
3
+ li.post.clearfix data-id='{{id}}' data-parent-id='{{parent.id}}' data-post-found='{{#parent}}yes{{/parent}}'
4
+ | {{> _post_inner}}
5
+ | {{/isRepost}}
6
+ | {{#isRepost}}
7
+ li.post.clearfix data-parent-id='{{id}}' data-post-found='no'
8
+ | {{> _post_inner}}
9
+ | {{/isRepost}}
10
+ | {{/isValid}}
@@ -0,0 +1,71 @@
1
+ .row
2
+ .span1
3
+ aFollowing href='{{entity}}'
4
+ img.img-rounded.pull-left.avatar src='{{avatar}}'
5
+
6
+ .span3.no-offset
7
+ h5 style='margin: 0px;'
8
+ a href='{{entity}}' {{name}} {{#hasName}}<small>{{formatted.entity}}</small>{{/hasName}}
9
+
10
+ .pull-right
11
+ a href='{{url}}' class='{{#parent}}gray{{/parent}}' title='{{formatted.full_published_at}}' {{formatted.published_at}}
12
+
13
+ | {{#inReplyTo}}
14
+ | {{#authenticated}}
15
+ .span5
16
+ a href='{{url}}'
17
+ small in reply to {{name}}
18
+ | {{/authenticated}}
19
+ | {{/inReplyTo}}
20
+
21
+ | {{^repost}}
22
+ | {{#parent}}
23
+ p.span5.no-offset
24
+ | {{content.text}}
25
+ | {{/parent}}
26
+ | {{^parent}}
27
+ p.span6.no-offset
28
+ | {{content.text}}
29
+ | {{/parent}}
30
+ | {{/repost}}
31
+
32
+ | {{#repost}}
33
+ | {{#authenticated}}
34
+ .span7.repost.no-offset style='width:560px;'
35
+ | {{> _post_inner}}
36
+ .span6.offset1
37
+ | Reposted by {{parent.name}}
38
+ small.pull-right via {{app.name}}
39
+ | {{/authenticated}}
40
+ | {{/repost}}
41
+
42
+ | {{#authenticated}}
43
+ .row.offset0-6.showOnHover.navigation
44
+ | {{^repost}}
45
+ | {{^currentUserOwnsPost}}
46
+ a.reply href='#' Reply
47
+ a.repost data-confirm='Repost {{nmae}}?' href='#' Repost
48
+ | {{/currentUserOwnsPost}}
49
+ | {{/repost}}
50
+ | {{#currentUserOwnsPost}}
51
+ a.delete data-confirm='Delete Post?' href='#' Delete
52
+ | {{/currentUserOwnsPost}}
53
+ | {{^repost}}
54
+ | {{^parent}}
55
+ small.pull-right via {{app.name}}
56
+ | {{/parent}}
57
+ | {{/repost}}
58
+ | {{/authenticated}}
59
+
60
+ | {{^repost}}
61
+ | {{#authenticated}}
62
+ | {{^parent}}
63
+ .row.offset0-5.reply-container.clearfix
64
+ | {{> _reply_form}}
65
+ | {{/parent}}
66
+ | {{#parent}}
67
+ .row.reply-container.clearfix
68
+ | {{> _reply_form}}
69
+ | {{/parent}}
70
+ | {{/authenticated}}
71
+ | {{/repost}}
@@ -0,0 +1,11 @@
1
+ .span2 &nbsp;
2
+ .span4.no-offset
3
+ .stat
4
+ .value {{postsCount}}
5
+ .title Posts
6
+ .stat
7
+ .title Following
8
+ .value {{followingsCount}}
9
+ .stat
10
+ .title Followed by
11
+ .value {{followersCount}}
@@ -0,0 +1,13 @@
1
+ | {{#authenticated}}
2
+ form.form-horizontal data-view='ReplyPostForm' method='POST'
3
+ input type='hidden' name='mentions_post_id' value='{{id}}'
4
+ input type='hidden' name='mentions_post_entity' value='{{entity}}'
5
+ .span7
6
+ .alert.alert-error &nbsp;
7
+
8
+ textarea.span7 name='text' placeholder="What's on your mind?" rows='3' ^{{entity}}
9
+ .clearfix
10
+ span.char-limit 140
11
+
12
+ input.btn.btn-success.pull-right type='submit' value='Post'
13
+ | {{/authenticated}}
@@ -0,0 +1,13 @@
1
+ .row
2
+ .span2 &nbsp;
3
+ .span8.well
4
+ ul.unstyled.posts
5
+ | {{#post}}
6
+ | {{#isValid}}
7
+ li.post.clearfix data-id='{{id}}'
8
+ | {{> _post_inner}}
9
+ | {{/isValid}}
10
+ | {{/post}}
11
+ | {{#posts}}
12
+ | {{> _post}}
13
+ | {{/posts}}