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,139 @@
1
+ class TentStatus.Views.Post extends TentStatus.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+ @template = @parentView?.partials['_post']
5
+
6
+ @postId = @$el.attr('data-parent-id') || ""
7
+ @postId = @$el.attr 'data-id' if @postId == ""
8
+ @post = @parentView.posts.get(@postId)
9
+ @parentPost = @post
10
+
11
+ if @post?.isRepost() && @$el.attr('data-post-found') != 'yes'
12
+ @$el.hide()
13
+ post = new TentStatus.Models.Post { id: @post.get('content')['id'] }
14
+ post.fetch
15
+ success: =>
16
+ return unless post.entity()
17
+ @$el.show()
18
+ @render @context(@post, @repostContext(@post, post))
19
+ @post = post
20
+ else
21
+ @setup()
22
+
23
+ @on 'ready', @setup
24
+ @on 'ready', @bindViews
25
+ @on 'ready', @bindEvents
26
+
27
+ setup: =>
28
+ @buttons = {
29
+ reply: ($ '.navigation .reply', @$el)
30
+ repost: ($ '.navigation .repost', @$el)
31
+ delete: ($ '.navigation .delete', @$el)
32
+ }
33
+
34
+ @buttons.reply.on 'click', (e) =>
35
+ e.preventDefault()
36
+ @showReply()
37
+ false
38
+
39
+ @$reply = ($ '.reply-container', @$el).hide()
40
+
41
+ @buttons.repost.on 'click', (e) =>
42
+ e.preventDefault()
43
+ @repost()
44
+ false
45
+
46
+ @buttons.delete.on 'click', (e) =>
47
+ e.preventDefault()
48
+ @delete()
49
+ false
50
+
51
+ showReply: =>
52
+ return if @post.get('entity') == TentStatus.current_entity
53
+ @$reply.toggle()
54
+
55
+ repost: =>
56
+ return if @buttons.repost.hasClass 'disabled'
57
+ return if @post.get('entity') == TentStatus.current_entity
58
+ shouldRepost = confirm(@buttons.repost.attr 'data-confirm')
59
+ return unless shouldRepost
60
+ post = new TentStatus.Models.Post {
61
+ type: "https://tent.io/types/post/repost/v0.1.0"
62
+ content:
63
+ entity: @post.get('entity')
64
+ id: @post.get('id')
65
+ }
66
+ post.once 'sync', =>
67
+ @buttons.repost.addClass 'disabled'
68
+ TentStatus.Collections.posts.unshift(post)
69
+ @parentView.emptyPool()
70
+ @parentView.fetchPoolView.createPostView(post)
71
+ post.save()
72
+
73
+ delete: =>
74
+ post = @post
75
+ unless post == @parentPost
76
+ post = @parentPost
77
+ return unless post.get('entity') == TentStatus.current_entity
78
+ shouldDelete = confirm(@buttons.delete.attr 'data-confirm')
79
+ return unless shouldDelete
80
+ @$el.hide()
81
+ @post.destroy
82
+ success: =>
83
+ @$el.remove()
84
+ error: =>
85
+ @$el.show()
86
+
87
+ replyToPost: (post) =>
88
+ return unless post.get('mentions')?.length
89
+ for mention in post.get('mentions')
90
+ if mention.entity and mention.post
91
+ mention.url = "#{TentStatus.url_root}entities/#{encodeURIComponent(mention.entity)}/#{mention.post}"
92
+ mention.name = (_.find @parentView.follows(), (follow) => follow.get('entity') == mention.entity)?.name()
93
+ mention.name ?= (_.find [@parentView.profile], (profile) => profile.entity() == mention.entity)?.name()
94
+ return mention
95
+ null
96
+
97
+ repostContext: (post, repost) =>
98
+ return false unless post.isRepost()
99
+
100
+ repost ?= _.find @parentView.posts.toArray(), ((p) => p.get('id') == post.get('content')['id'])
101
+ return false unless repost
102
+ _.extend( @context(repost), { parent: { name: post.name(), id: post.get('id') } } )
103
+
104
+ licenseName: (url) =>
105
+ for l in @licenses || []
106
+ return l.name if l.url == url
107
+ url
108
+
109
+ context: (post, repostContext) =>
110
+ _.extend( post.toJSON(),
111
+ isValid: true
112
+ shouldShowReply: true
113
+ isRepost: post.isRepost()
114
+ repost: repostContext || @repostContext(post)
115
+ inReplyTo: @replyToPost(post)
116
+ url: "#{TentStatus.url_root}entities/#{encodeURIComponent(post.get('entity'))}/#{post.get('id')}"
117
+ profileUrl: "#{TentStatus.url_root}entities/#{encodeURIComponent(post.get('entity'))}"
118
+ name: post.name()
119
+ hasName: post.hasName()
120
+ avatar: post.avatar()
121
+ licenses: _.map( post.get('licenses') || [], (url) => { name: @licenseName(url), url: url } )
122
+ escaped:
123
+ entity: encodeURIComponent(post.get('entity'))
124
+ formatted:
125
+ entity: TentStatus.Helpers.formatUrl post.get('entity')
126
+ published_at: TentStatus.Helpers.formatTime post.get('published_at')
127
+ full_published_at: TentStatus.Helpers.rawTime post.get('published_at')
128
+ authenticated: TentStatus.authenticated
129
+ guest_authenticated: TentStatus.guest_authenticated
130
+ currentUserOwnsPost: TentStatus.current_entity == post.get('entity')
131
+ )
132
+
133
+ render: (context) =>
134
+ html = @template.render(context, @parentView.partials)
135
+ el = ($ html)
136
+ @$el.replaceWith(el)
137
+ @$el = el
138
+ @trigger 'ready'
139
+
@@ -0,0 +1,55 @@
1
+ class TentStatus.Views.Posts extends TentStatus.View
2
+ templateName: 'posts'
3
+ partialNames: ['_post', '_new_post_form', '_reply_form', '_post_inner']
4
+
5
+ dependentRenderAttributes: ['posts', 'followers', 'followings', 'profile']
6
+
7
+ initialize: ->
8
+ @container = TentStatus.Views.container
9
+ super
10
+
11
+ @on 'ready', @initPostViews
12
+ @on 'ready', @initFetchPool
13
+ @on 'ready', @initAutoPaginate
14
+
15
+ sortedPosts: => @posts.sortBy (post) -> -post.get('published_at')
16
+
17
+ uniqueFollowings: =>
18
+ @followings?.filter (following) =>
19
+ !@followers.find (follower) =>
20
+ follower.get('entity') == following.get('entity')
21
+
22
+ follows: =>
23
+ (@followers?.toArray() || []).concat(@uniqueFollowings() || [])
24
+
25
+ context: =>
26
+ @licenses = [{ url: "http://creativecommons.org/licenses/by-nc-sa/3.0/", name: "Creative Commons by-nc-sa 3.0" }]
27
+
28
+ follows: _.map(@follows(), (follow) -> _.extend follow.toJSON(), {
29
+ name: follow.name()
30
+ })
31
+ licenses: @licenses
32
+ posts: (_.map @posts.toArray(), (post) =>
33
+ view = new TentStatus.Views.Post parentView: @
34
+ view.context(post)
35
+ )
36
+
37
+ initPostViews: =>
38
+ _.each ($ 'li.post'), (el) =>
39
+ new TentStatus.Views.Post el: el, parentView: @
40
+
41
+ initFetchPool: =>
42
+ el = ($ '.fetch-pool', @container.$el).hide()
43
+ @fetchPoolView = new TentStatus.Views.FetchPostsPool el: el, parentView: @
44
+
45
+ initAutoPaginate: =>
46
+ ($ window).off 'scroll.posts'
47
+ ($ window).on 'scroll.posts', (e)=>
48
+ height = $(document).height() - $(window).height()
49
+ delta = height - window.scrollY
50
+ if delta < 300
51
+ @posts?.nextPage() unless @posts.onLastPage
52
+
53
+ emptyPool: =>
54
+ @fetchPoolView.emptyPool()
55
+
@@ -0,0 +1,18 @@
1
+ class TentStatus.Views.Conversation extends TentStatus.Views.Posts
2
+ templateName: 'conversation'
3
+
4
+ initialize: ->
5
+ @dependentRenderAttributes.push 'post'
6
+ super
7
+
8
+ context: =>
9
+ @posts.unshift(@post)
10
+ data = super
11
+ posts = []
12
+ for post in data.posts
13
+ if post.id == @post.get('id')
14
+ data.post = post
15
+ else
16
+ posts.push post
17
+ data.posts = posts
18
+ data
@@ -0,0 +1,29 @@
1
+ class TentStatus.Views.Profile extends TentStatus.Views.Posts
2
+ templateName: 'profile'
3
+ partialNames: ['_post', '_post_inner', '_reply_form']
4
+
5
+ initialize: ->
6
+ @dependentRenderAttributes.push 'currentProfile'
7
+ super
8
+
9
+ replyToPost: (post) =>
10
+ return unless post.get('mentions')?.length
11
+ for mention in post.get('mentions')
12
+ if mention.entity and mention.post
13
+ mention.url = "#{TentStatus.url_root}#{encodeURIComponent(mention.entity)}/#{mention.post}"
14
+ return mention
15
+ null
16
+
17
+ context: =>
18
+ _.extend super,
19
+ profile: _.extend( @currentProfile.toJSON(),
20
+ name: @currentProfile.name()
21
+ bio: @currentProfile.bio()
22
+ nameIsEntity: @currentProfile.name() == TentStatus.Helpers.formatUrl(@currentProfile.entity())
23
+ avatar: @currentProfile.avatar()
24
+ entity: @currentProfile.entity()
25
+ encoded:
26
+ entity: encodeURIComponent(@currentProfile.entity())
27
+ formatted:
28
+ entity: TentStatus.Helpers.formatUrl @currentProfile.entity()
29
+ )
@@ -0,0 +1,29 @@
1
+ class TentStatus.Views.ProfileFollowButton extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ @buttons = {}
6
+ @buttons.submit = ($ '[type=submit]', @$el)
7
+
8
+ following = new TentStatus.Models.Following
9
+ following.fetch
10
+ url: "#{TentStatus.api_root}/followings?entity=#{encodeURIComponent(TentStatus.domain_entity)}&guest=true"
11
+ success: (f, res) =>
12
+ if res.length
13
+ @setFollowing()
14
+
15
+ @$el.on 'submit', @submit
16
+
17
+ submit: (e) =>
18
+ e.preventDefault()
19
+ entity = TentStatus.domain_entity
20
+ @buttons.submit.attr 'disabled', 'disabled'
21
+ following = new TentStatus.Models.Following { entity: entity }
22
+ following.once 'sync', =>
23
+ @setFollowing()
24
+ following.save()
25
+ false
26
+
27
+ setFollowing: =>
28
+ @buttons.submit.val 'Following'
29
+ @buttons.submit.attr 'disabled', 'disabled'
@@ -0,0 +1,38 @@
1
+ class TentStatus.Views.ProfileStats extends TentStatus.View
2
+ templateName: '_profile_stats'
3
+
4
+ initialize: (options) ->
5
+ super
6
+ @container = null
7
+
8
+ api_root = if TentStatus.guest_authenticated
9
+ TentStatus.tent_api_root
10
+ else
11
+ TentStatus.api_root
12
+
13
+ @countKeys = ['postsCount', 'followingsCount', 'followersCount']
14
+ for key in @countKeys
15
+ @once "change:#{key}", @render
16
+
17
+ $.getJSON "#{api_root}/posts/count", (count) =>
18
+ @set 'postsCount', count
19
+
20
+ $.getJSON "#{api_root}/followers/count", (count) =>
21
+ @set 'followersCount', count
22
+
23
+ $.getJSON "#{api_root}/followings/count", (count) =>
24
+ @set 'followingsCount', count
25
+
26
+ @render()
27
+
28
+ context: =>
29
+ postsCount: @postsCount
30
+ followersCount: @followersCount
31
+ followingsCount: @followingsCount
32
+
33
+ render: =>
34
+ for key in @countKeys
35
+ val = @get(key)
36
+ return if val == null
37
+ html = super
38
+ @$el.html(html)
@@ -0,0 +1,18 @@
1
+ class TentStatus.Views.RemoveFollowerBtn extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ followerId = @$el.attr 'data-id'
6
+ @follower = TentStatus.Collections.followers.get(followerId)
7
+
8
+ @confirmMsg = @$el.attr 'data-confirm'
9
+
10
+ @$el.on 'click', @confirmUnfollow
11
+
12
+ confirmUnfollow: =>
13
+ shouldRemove = confirm @confirmMsg
14
+ return unless shouldRemove
15
+ @follower.destroy
16
+ success: =>
17
+ TentStatus.Collections.followers.remove(@follower)
18
+ @parentView.render()
@@ -0,0 +1,30 @@
1
+ class TentStatus.Views.ReplyPostForm extends TentStatus.Views.NewPostForm
2
+ initialize: (options = {}) ->
3
+ super
4
+
5
+ ## reply fields
6
+ @replyToPostId = ($ '[name=mentions_post_id]', @$el).val()
7
+ @replyToEntity = ($ '[name=mentions_post_entity]', @$el).val()
8
+
9
+ submit: (e) =>
10
+ e.preventDefault()
11
+ data = @getData()
12
+ return false unless @validate data
13
+
14
+ post = new TentStatus.Models.Post data
15
+ post.once 'sync', =>
16
+ window.location.reload() unless @parentView.emptyPool
17
+ @parentView.emptyPool()
18
+ TentStatus.Collections.posts.unshift(post)
19
+ @parentView.posts.unshift(post)
20
+ @parentView.render()
21
+ post.save()
22
+ false
23
+
24
+ buildMentions: (data) =>
25
+ data = super
26
+ if @replyToPostId and @replyToEntity
27
+ data.mentions ||= []
28
+ data.mentions.push { entity: @replyToEntity, post: @replyToPostId }
29
+ data
30
+
@@ -0,0 +1,18 @@
1
+ class TentStatus.Views.UnfollowBtn extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ followingId = @$el.attr 'data-id'
6
+ @following = TentStatus.Collections.followings.get(followingId)
7
+
8
+ @confirmMsg = @$el.attr 'data-confirm'
9
+
10
+ @$el.on 'click', @confirmUnfollow
11
+
12
+ confirmUnfollow: =>
13
+ shouldUnfollow = confirm @confirmMsg
14
+ return unless shouldUnfollow
15
+ @following.destroy
16
+ success: =>
17
+ TentStatus.Collections.followings.remove(@following)
18
+ @parentView.render()
File without changes
@@ -0,0 +1,117 @@
1
+ //= require bootstrap
2
+ //= require bootstrap-responsive
3
+ //= require chosen
4
+ //= require_self
5
+
6
+ @mixin rounded($amount)
7
+ -webkit-border-radius: $amount
8
+ -moz-border-radius: $amount
9
+ border-radius: $amount
10
+
11
+ textarea::-webkit-input-placeholder
12
+ padding-top: 20px
13
+ text-align: center
14
+
15
+ form input,
16
+ form select,
17
+ form textarea
18
+ &.error
19
+ color: #b94a48
20
+ border-color: #b94a48
21
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075)
22
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075)
23
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075)
24
+
25
+ &:focus
26
+ border-color: #953b39
27
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392
28
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392
29
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392
30
+
31
+ .permissions
32
+ .control-label
33
+ width: 20px
34
+ .controls
35
+ margin-left: 30px
36
+
37
+ .post .navigation ul.nav
38
+ margin-bottom: 0px
39
+ padding: 0px
40
+
41
+ li.post
42
+ border-bottom: 1px solid #eee
43
+ margin-bottom: 19px
44
+ &:hover
45
+ margin-bottom: 20px
46
+ background-color: #f5f5f5
47
+ border: 0px
48
+ @include rounded(4px)
49
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05)
50
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05)
51
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05)
52
+
53
+ .showOnHover
54
+ visibility: hidden
55
+
56
+ img.avatar
57
+ width: 50px
58
+ height: 50px
59
+ margin-right: 4px
60
+
61
+ &:hover
62
+ .showOnHover
63
+ visibility: visible
64
+
65
+ div.repost
66
+ margin-top: 6px
67
+ @include rounded(4px)
68
+ background-color: #eee
69
+
70
+ .navigation a
71
+ margin-right: 6px
72
+
73
+ .avatar-large
74
+ max-width: 100px
75
+ max-height: 150px
76
+ min-width: 100px
77
+ min-height: 100px
78
+
79
+ .gray
80
+ color: gray
81
+
82
+ h2.condensed
83
+ line-height: 24px
84
+
85
+ .offset0-5
86
+ margin-left: 42px
87
+
88
+ .offset0-6
89
+ margin-left: 62px
90
+
91
+ .no-offset
92
+ margin-left: 0px
93
+
94
+ .profile-stats
95
+ .stat
96
+ text-align: center
97
+ float: left
98
+ line-height: 32px
99
+ margin-right: 20px
100
+ .title
101
+ font-size: 16px
102
+ font-weight: 500
103
+ .value
104
+ font-size: 20px
105
+ font-weight: 600
106
+
107
+ body
108
+ margin-bottom: 60px
109
+
110
+ footer.navbar-fixed-bottom
111
+ background-color: #fff
112
+
113
+ text-align: center
114
+ margin-top: 20px
115
+ padding-top: 10px
116
+ padding-bottom: 10px
117
+ border-top: 1px grey solid