tent-status 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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}}