blogelator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (162) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +86 -0
  4. data/Rakefile +29 -0
  5. data/app/assets/images/blogelator/blogelator-logo.svg +15 -0
  6. data/app/assets/javascripts/blogelator/admin.js +19 -0
  7. data/app/assets/javascripts/blogelator/application.js +2 -0
  8. data/app/assets/javascripts/blogelator/components/markdown_editor_component.js +187 -0
  9. data/app/assets/javascripts/blogelator/components/save_button_component.js +76 -0
  10. data/app/assets/javascripts/blogelator/config/marked.js +10 -0
  11. data/app/assets/javascripts/blogelator/config/router.js +9 -0
  12. data/app/assets/javascripts/blogelator/config/routes.js +13 -0
  13. data/app/assets/javascripts/blogelator/config/serializer.js +10 -0
  14. data/app/assets/javascripts/blogelator/config/store.js +10 -0
  15. data/app/assets/javascripts/blogelator/controllers/application_controller.js +11 -0
  16. data/app/assets/javascripts/blogelator/controllers/posts/posts_edit_controller.js +97 -0
  17. data/app/assets/javascripts/blogelator/controllers/posts/posts_index_controller.js +9 -0
  18. data/app/assets/javascripts/blogelator/controllers/posts/posts_new_controller.js +6 -0
  19. data/app/assets/javascripts/blogelator/controllers/posts/posts_show_controller.js +37 -0
  20. data/app/assets/javascripts/blogelator/models/post.js +34 -0
  21. data/app/assets/javascripts/blogelator/routes/index_route.js +10 -0
  22. data/app/assets/javascripts/blogelator/routes/posts_edit_route.js +36 -0
  23. data/app/assets/javascripts/blogelator/routes/posts_index_route.js +20 -0
  24. data/app/assets/javascripts/blogelator/routes/posts_new_route.js +53 -0
  25. data/app/assets/javascripts/blogelator/routes/posts_route.js +10 -0
  26. data/app/assets/javascripts/blogelator/routes/posts_show_route.js +14 -0
  27. data/app/assets/javascripts/blogelator/templates/application.handlebars +23 -0
  28. data/app/assets/javascripts/blogelator/templates/components/markdown-editor.handlebars +8 -0
  29. data/app/assets/javascripts/blogelator/templates/components/save-button.handlebars +8 -0
  30. data/app/assets/javascripts/blogelator/templates/posts/_footer.handlebars +0 -0
  31. data/app/assets/javascripts/blogelator/templates/posts/_form.handlebars +6 -0
  32. data/app/assets/javascripts/blogelator/templates/posts/_form_action_bar.handlebars +22 -0
  33. data/app/assets/javascripts/blogelator/templates/posts/_show_action_bar.handlebars +20 -0
  34. data/app/assets/javascripts/blogelator/templates/posts/edit.handlebars +1 -0
  35. data/app/assets/javascripts/blogelator/templates/posts/index.handlebars +30 -0
  36. data/app/assets/javascripts/blogelator/templates/posts/new.handlebars +1 -0
  37. data/app/assets/javascripts/blogelator/templates/posts/show.handlebars +22 -0
  38. data/app/assets/stylesheets/blogelator/admin/all.scss +7 -0
  39. data/app/assets/stylesheets/blogelator/admin/components/markdown_editor.scss +142 -0
  40. data/app/assets/stylesheets/blogelator/admin/components/save_button.scss +121 -0
  41. data/app/assets/stylesheets/blogelator/admin/layout.scss +94 -0
  42. data/app/assets/stylesheets/blogelator/admin/posts/_form.scss +12 -0
  43. data/app/assets/stylesheets/blogelator/admin/posts/_form_action_bar.scss +84 -0
  44. data/app/assets/stylesheets/blogelator/admin/posts/_posts_list.scss +43 -0
  45. data/app/assets/stylesheets/blogelator/admin/posts/edit.scss +7 -0
  46. data/app/assets/stylesheets/blogelator/admin/posts/index.scss +14 -0
  47. data/app/assets/stylesheets/blogelator/admin/posts/new.scss +16 -0
  48. data/app/assets/stylesheets/blogelator/admin/posts/show.scss +17 -0
  49. data/app/assets/stylesheets/blogelator/application/all.scss +19 -0
  50. data/app/assets/stylesheets/blogelator/application/layout.scss +49 -0
  51. data/app/assets/stylesheets/blogelator/application/posts/index.scss +50 -0
  52. data/app/assets/stylesheets/blogelator/application/posts/show.scss +64 -0
  53. data/app/assets/stylesheets/blogelator/mixins/blogelator_button.scss +57 -0
  54. data/app/assets/stylesheets/blogelator/mixins/blogelator_logo.scss +12 -0
  55. data/app/assets/stylesheets/blogelator/shared/post.scss +161 -0
  56. data/app/assets/stylesheets/blogelator/shared/typography/code_highlighting.scss +111 -0
  57. data/app/assets/stylesheets/blogelator/shared/typography/forms.scss +94 -0
  58. data/app/assets/stylesheets/blogelator/shared/typography/headings.scss +22 -0
  59. data/app/assets/stylesheets/blogelator/shared/typography/lists.scss +25 -0
  60. data/app/controllers/blogelator/admin/application_controller.rb +15 -0
  61. data/app/controllers/blogelator/admin/images_controller.rb +40 -0
  62. data/app/controllers/blogelator/admin/posts_controller.rb +67 -0
  63. data/app/controllers/blogelator/application_controller.rb +5 -0
  64. data/app/controllers/blogelator/posts_controller.rb +37 -0
  65. data/app/controllers/concerns/blogelator/admin/auth.rb +23 -0
  66. data/app/helpers/blogelator/admin/posts_helper.rb +7 -0
  67. data/app/helpers/blogelator/posts_helper.rb +28 -0
  68. data/app/models/blogelator/ability.rb +11 -0
  69. data/app/models/blogelator/html_renderer.rb +10 -0
  70. data/app/models/blogelator/post.rb +63 -0
  71. data/app/models/blogelator/tag.rb +5 -0
  72. data/app/serializers/blogelator/post_serializer.rb +6 -0
  73. data/app/views/blogelator/admin/posts/index.html.erb +0 -0
  74. data/app/views/blogelator/posts/_post.html.erb +14 -0
  75. data/app/views/blogelator/posts/index.html.erb +39 -0
  76. data/app/views/blogelator/posts/show.html.erb +27 -0
  77. data/app/views/layouts/blogelator/_footer.html.erb +3 -0
  78. data/app/views/layouts/blogelator/_head.html.erb +7 -0
  79. data/app/views/layouts/blogelator/_header.html.erb +7 -0
  80. data/app/views/layouts/blogelator/admin.html.erb +14 -0
  81. data/app/views/layouts/blogelator/application.html.erb +14 -0
  82. data/config/jshint.json +43 -0
  83. data/config/routes.rb +15 -0
  84. data/db/migrate/20140221192000_create_blogelator_posts.rb +12 -0
  85. data/db/migrate/20140221204230_add_slug_and_change_user_to_author.rb +6 -0
  86. data/db/migrate/20140221212026_create_blogelator_tags.rb +10 -0
  87. data/db/migrate/20140224024607_change_posts_properties_to_have_defaults.rb +14 -0
  88. data/db/migrate/20140224175024_add_slugs_to_existing_posts.rb +16 -0
  89. data/db/migrate/20140224192058_add_published_at_to_posts.rb +13 -0
  90. data/db/migrate/20140303015004_add_summary_to_posts.rb +9 -0
  91. data/lib/assets/stylesheets/blogelator/_variables_sample.scss +38 -0
  92. data/lib/blogelator.rb +27 -0
  93. data/lib/blogelator/engine.rb +35 -0
  94. data/lib/blogelator/version.rb +3 -0
  95. data/lib/generators/blogelator/install_generator.rb +94 -0
  96. data/spec/dummy/README.rdoc +28 -0
  97. data/spec/dummy/Rakefile +6 -0
  98. data/spec/dummy/app/assets/javascripts/application.js +1 -0
  99. data/spec/dummy/app/assets/javascripts/features/user_visits_admin_page_spec.js +12 -0
  100. data/spec/dummy/app/assets/javascripts/features/user_visits_post_page_spec.js +12 -0
  101. data/spec/dummy/app/assets/javascripts/fixtures/post.js +17 -0
  102. data/spec/dummy/app/assets/javascripts/test_helper.js +12 -0
  103. data/spec/dummy/app/assets/stylesheets/application.css +12 -0
  104. data/spec/dummy/app/assets/stylesheets/blogelator/_variables.scss +38 -0
  105. data/spec/dummy/app/assets/stylesheets/blogelator/admin.css.scss +4 -0
  106. data/spec/dummy/app/assets/stylesheets/blogelator/application.css.scss +4 -0
  107. data/spec/dummy/app/assets/stylesheets/test_helper.css +21 -0
  108. data/spec/dummy/app/controllers/application_controller.rb +9 -0
  109. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  110. data/spec/dummy/app/models/blogelator/ability.rb +9 -0
  111. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  112. data/spec/dummy/app/views/q_unit/rails/test/index.html.erb +17 -0
  113. data/spec/dummy/app/views/qunit/rails/test/index.html.erb +17 -0
  114. data/spec/dummy/bin/bundle +3 -0
  115. data/spec/dummy/bin/rails +4 -0
  116. data/spec/dummy/bin/rake +4 -0
  117. data/spec/dummy/config.ru +4 -0
  118. data/spec/dummy/config/application.rb +24 -0
  119. data/spec/dummy/config/boot.rb +5 -0
  120. data/spec/dummy/config/database.yml +25 -0
  121. data/spec/dummy/config/environment.rb +5 -0
  122. data/spec/dummy/config/environments/development.rb +32 -0
  123. data/spec/dummy/config/environments/production.rb +80 -0
  124. data/spec/dummy/config/environments/test.rb +36 -0
  125. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  126. data/spec/dummy/config/initializers/blogelator.rb +6 -0
  127. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  128. data/spec/dummy/config/initializers/inflections.rb +16 -0
  129. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  130. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  131. data/spec/dummy/config/initializers/session_store.rb +3 -0
  132. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  133. data/spec/dummy/config/locales/en.yml +23 -0
  134. data/spec/dummy/config/routes.rb +3 -0
  135. data/spec/dummy/db/development.sqlite3 +0 -0
  136. data/spec/dummy/db/migrate/20140221215439_create_blogelator_posts.blogelator.rb +13 -0
  137. data/spec/dummy/db/migrate/20140221215440_add_slug_and_change_user_to_author.blogelator.rb +7 -0
  138. data/spec/dummy/db/migrate/20140224024931_create_blogelator_tags.blogelator.rb +11 -0
  139. data/spec/dummy/db/migrate/20140224025218_change_posts_properties_to_have_defaults.blogelator.rb +15 -0
  140. data/spec/dummy/db/migrate/20140224205646_add_slugs_to_existing_posts.blogelator.rb +17 -0
  141. data/spec/dummy/db/migrate/20140224205647_add_published_at_to_posts.blogelator.rb +14 -0
  142. data/spec/dummy/db/migrate/20140303015133_add_summary_to_posts.blogelator.rb +6 -0
  143. data/spec/dummy/db/production.sqlite3 +0 -0
  144. data/spec/dummy/db/schema.rb +37 -0
  145. data/spec/dummy/db/test.sqlite3 +0 -0
  146. data/spec/dummy/log/development.log +50393 -0
  147. data/spec/dummy/log/production.log +76 -0
  148. data/spec/dummy/log/test.log +11750 -0
  149. data/spec/dummy/public/404.html +58 -0
  150. data/spec/dummy/public/422.html +58 -0
  151. data/spec/dummy/public/500.html +57 -0
  152. data/spec/dummy/public/favicon.ico +0 -0
  153. data/spec/dummy/public/index.html.erb +14 -0
  154. data/spec/dummy/tmp/ember-rails/ember-data.js +11132 -0
  155. data/spec/dummy/tmp/ember-rails/ember.js +42882 -0
  156. data/spec/factories/post.rb +11 -0
  157. data/spec/features/user_visits_blog_post_spec.rb +44 -0
  158. data/spec/features/user_visits_blog_spec.rb +85 -0
  159. data/spec/models/blogelator/post_spec.rb +95 -0
  160. data/spec/qunit_runner.js +148 -0
  161. data/spec/spec_helper.rb +34 -0
  162. metadata +764 -0
@@ -0,0 +1,10 @@
1
+ //= require marked
2
+ //= require prettify
3
+
4
+ marked.setOptions({
5
+ langPrefix: 'highlight lang-',
6
+ smartypants: true,
7
+ highlight: function(code, lang) {
8
+ return prettyPrintOne(code, lang);
9
+ }
10
+ });
@@ -0,0 +1,9 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.Router.reopen({
5
+ rootURL: (App.get('blogelatorPath') === '/' ? '' : App.get('blogelatorPath')) + '/admin',
6
+ location: 'history'
7
+ });
8
+
9
+ })();
@@ -0,0 +1,13 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.Router.map(function() {
5
+ this.route('index', { path: '/' });
6
+ this.resource('posts', { path: '/posts' }, function() {
7
+ this.route('edit', { path: '/:post_id/edit' });
8
+ this.route('new');
9
+ this.route('show', { path: '/:post_id' });
10
+ });
11
+ });
12
+
13
+ })();
@@ -0,0 +1,10 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.ApplicationSerializer = DS.RESTSerializer.extend({
5
+ keyForAttribute: function(attr) {
6
+ return Ember.String.underscore(attr);
7
+ }
8
+ });
9
+
10
+ })();
@@ -0,0 +1,10 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.Store = DS.Store.extend({
5
+ adapter: DS.RESTAdapter.extend({
6
+ namespace: (App.get('blogelatorPath') === '/' ? '' : App.get('blogelatorPath').substring(1) + '/') + 'api'
7
+ })
8
+ });
9
+
10
+ })();
@@ -0,0 +1,11 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.ApplicationController = Ember.ObjectController.extend({
5
+ currentPathDidChange: function() {
6
+ var bodyClass = 'ember-application blogelator admin ' + this.get('currentPath').split('.').join(' ');
7
+ $('body').attr('class', bodyClass);
8
+ }.observes('currentPath')
9
+ });
10
+
11
+ })();
@@ -0,0 +1,97 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsEditController = Ember.ObjectController.extend({
5
+ needs: ['posts'],
6
+ secondsPassed: 0,
7
+
8
+ actions: {
9
+ destroy: function(defer) {
10
+ var post = this.get('content'),
11
+ controller = this.get('controllers.posts');
12
+
13
+ if (Ember.isNone(defer)) {
14
+ post.destroyRecord();
15
+ return post;
16
+ }
17
+
18
+ post.destroyRecord().then(function() {
19
+ defer.resolve();
20
+ Ember.run.later(function() {
21
+ controller.transitionToRoute('posts.index');
22
+ }, 900);
23
+ }, function() {
24
+ defer.reject();
25
+ });
26
+
27
+ return post;
28
+ },
29
+
30
+ save: function(defer) {
31
+ var post = this.get('content');
32
+
33
+ if (Ember.isNone(defer)) {
34
+ post.save();
35
+ return post;
36
+ }
37
+
38
+ if (!post.get('isDirty')) {
39
+ defer.resolve();
40
+ } else {
41
+ post.save().then(function() {
42
+ defer.resolve();
43
+ }, function() {
44
+ defer.reject();
45
+ });
46
+ }
47
+
48
+ return post;
49
+ },
50
+
51
+ togglePublished: function(defer) {
52
+ var post = this.get('content');
53
+
54
+ if (Ember.isNone(post.get('publishedAt'))) {
55
+ post.set('publishedAt', new Date());
56
+ } else {
57
+ post.set('publishedAt', null);
58
+ }
59
+
60
+ if (Ember.isNone(defer)) {
61
+ post.save();
62
+ return post;
63
+ }
64
+
65
+ if (!post.get('isDirty')) {
66
+ defer.resolve();
67
+ } else {
68
+ post.save().then(function() {
69
+ defer.resolve();
70
+ }, function() {
71
+ defer.reject();
72
+ });
73
+ }
74
+
75
+ return post;
76
+ }
77
+ },
78
+
79
+ init: function() {
80
+ this._super();
81
+ this.updateSecondsPassed();
82
+ },
83
+
84
+ isClean: function() {
85
+ return !this.get('isDirty');
86
+ }.property('isDirty'),
87
+
88
+ updateSecondsPassed: function() {
89
+ var self = this;
90
+ this.incrementProperty('secondsPassed', 1);
91
+ Ember.run.later(function() {
92
+ self.updateSecondsPassed();
93
+ }, 1000);
94
+ }
95
+ });
96
+
97
+ })();
@@ -0,0 +1,9 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsIndexController = Ember.ArrayController.extend({
5
+ needs: ['posts'],
6
+ contentBinding: 'controllers.posts.content'
7
+ });
8
+
9
+ })();
@@ -0,0 +1,6 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsNewController = App.PostsEditController.extend();
5
+
6
+ })();
@@ -0,0 +1,37 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsShowController = Ember.ObjectController.extend({
5
+ needs: ['posts'],
6
+
7
+ actions: {
8
+ togglePublished: function(defer) {
9
+ var post = this.get('content');
10
+
11
+ if (Ember.isNone(post.get('publishedAt'))) {
12
+ post.set('publishedAt', new Date());
13
+ } else {
14
+ post.set('publishedAt', null);
15
+ }
16
+
17
+ if (Ember.isNone(defer)) {
18
+ post.save();
19
+ return post;
20
+ }
21
+
22
+ if (!post.get('isDirty')) {
23
+ defer.resolve();
24
+ } else {
25
+ post.save().then(function() {
26
+ defer.resolve();
27
+ }, function() {
28
+ defer.reject();
29
+ });
30
+ }
31
+
32
+ return post;
33
+ }
34
+ }
35
+ });
36
+
37
+ })();
@@ -0,0 +1,34 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.Post = DS.Model.extend({
5
+ bodyHtml: DS.attr('string', { defaultValue: '' }),
6
+ bodyMarkdown: DS.attr('string', { defaultValue: '' }),
7
+ createdAt: DS.attr('date'),
8
+ publishedAt: DS.attr('date'),
9
+ summary: DS.attr('string'),
10
+ title: DS.attr('string'),
11
+ updatedAt: DS.attr('date'),
12
+
13
+ publishedAtFormatted: function() {
14
+ var publishedAt = this.get('publishedAt');
15
+
16
+ if (Ember.isNone(publishedAt)) {
17
+ return "Unpublished";
18
+ } else {
19
+ return moment(publishedAt).format('MMMM D, YYYY');
20
+ }
21
+ }.property('publishedAt'),
22
+
23
+ updatedAtFormatted: function() {
24
+ var updatedAt = this.get('updatedAt');
25
+
26
+ if (Ember.isNone(updatedAt)) {
27
+ return "Never";
28
+ } else {
29
+ return moment(updatedAt).fromNow();
30
+ }
31
+ }.property('secondsPassed', 'updatedAt')
32
+ });
33
+
34
+ })();
@@ -0,0 +1,10 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.IndexRoute = Ember.Route.extend({
5
+ beforeModel: function() {
6
+ this.transitionTo('posts.index');
7
+ }
8
+ });
9
+
10
+ })();
@@ -0,0 +1,36 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsEditRoute = Ember.Route.extend({
5
+ actions: {
6
+ willTransition: function(transition) {
7
+ var record = this.get('controller.content');
8
+
9
+ // Confirm transition if there are unsaved changes
10
+ if (record.get('isDirty')) {
11
+ if (confirm("Are you sure you want to lose unsaved changes?")) {
12
+ record.rollback();
13
+ return true;
14
+ } else {
15
+ transition.abort();
16
+ }
17
+ } else {
18
+ return true;
19
+ }
20
+ }
21
+ },
22
+
23
+ model: function(params) {
24
+ return this.store.find('post', params.post_id);
25
+ },
26
+
27
+ renderTemplate: function() {
28
+ this.render();
29
+ this.render('posts/_form_action_bar', {
30
+ into: 'application',
31
+ outlet: 'footer'
32
+ });
33
+ }
34
+ });
35
+
36
+ })();
@@ -0,0 +1,20 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsIndexRoute = Ember.Route.extend({
5
+ afterModel: function(posts) {
6
+ if (posts.get('length') > 0) {
7
+ this.transitionTo('posts.show', posts.objectAt(0));
8
+ }
9
+ },
10
+
11
+ renderTemplate: function() {
12
+ this.render();
13
+ this.render('posts/_footer', {
14
+ into: 'application',
15
+ outlet: 'footer'
16
+ });
17
+ }
18
+ });
19
+
20
+ })();
@@ -0,0 +1,53 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsNewRoute = Ember.Route.extend({
5
+ actions: {
6
+ willTransition: function(transition) {
7
+ var record = this.get('controller.content');
8
+
9
+ // Allow transition if nothing is entered
10
+ if (Ember.isEmpty(record.get('title')) &&
11
+ Ember.isEmpty(record.get('bodyMarkdown'))
12
+ ) {
13
+ record.destroyRecord();
14
+ return true;
15
+ }
16
+
17
+ // Confirm transition if there are unsaved changes
18
+ if (record.get('isNew')) {
19
+ if (confirm("Are you sure you want to lose unsaved changes?")) {
20
+ record.destroyRecord();
21
+ return true;
22
+ } else {
23
+ transition.abort();
24
+ }
25
+ } else {
26
+ if (record.get('isDirty')) {
27
+ if (confirm("Are you sure you want to lose unsaved changes?")) {
28
+ record.rollback();
29
+ return true;
30
+ } else {
31
+ transition.abort();
32
+ }
33
+ } else {
34
+ return true;
35
+ }
36
+ }
37
+ }
38
+ },
39
+
40
+ model: function() {
41
+ return this.store.createRecord('post');
42
+ },
43
+
44
+ renderTemplate: function() {
45
+ this.render();
46
+ this.render('posts/_form_action_bar', {
47
+ into: 'application',
48
+ outlet: 'footer'
49
+ });
50
+ }
51
+ });
52
+
53
+ })();
@@ -0,0 +1,10 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsRoute = Ember.Route.extend({
5
+ model: function() {
6
+ return this.store.find('post');
7
+ }
8
+ });
9
+
10
+ })();
@@ -0,0 +1,14 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ App.PostsShowRoute = Ember.Route.extend({
5
+ renderTemplate: function() {
6
+ this.render();
7
+ this.render('posts/_show_action_bar', {
8
+ into: 'application',
9
+ outlet: 'footer'
10
+ });
11
+ }
12
+ });
13
+
14
+ })();
@@ -0,0 +1,23 @@
1
+ <header>
2
+ <ul>
3
+ <li>
4
+ <a {{bind-attr href="App.blogelatorPath"}}>Blog</a>
5
+ </li>
6
+ <li>
7
+ {{#link-to "posts" class="posts-index-link"}}
8
+ Content
9
+ {{/link-to}}
10
+ </li>
11
+ <li>
12
+ {{#link-to "posts.new"}}
13
+ New Post
14
+ {{/link-to}}
15
+ </li>
16
+ </ul>
17
+ </header>
18
+ <main>
19
+ {{outlet}}
20
+ </main>
21
+ <footer>
22
+ {{outlet "footer"}}
23
+ </footer>