diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index 8adb6c5b9e6..f16f6ed761b 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -293,3 +293,8 @@ class WebsiteBlog(http.Controller): if not post_id: return False return request.env['blog.post'].browse(int(post_id)).write({'cover_properties': json.dumps(cover_properties)}) + + @http.route(['/blog/render_latest_posts'], type='json', auth='public', website=True) + def render_latest_posts(self, template, domain, limit=None, order='published_date desc'): + posts = request.env['blog.post'].search(domain, limit=limit, order=order) + return request.env.ref(template).render({'posts': posts}) diff --git a/addons/website_blog/static/src/img/s_latest_posts.jpg b/addons/website_blog/static/src/img/s_latest_posts.jpg new file mode 100644 index 00000000000..87d14400e43 Binary files /dev/null and b/addons/website_blog/static/src/img/s_latest_posts.jpg differ diff --git a/addons/website_blog/static/src/img/s_latest_posts_big_picture.jpg b/addons/website_blog/static/src/img/s_latest_posts_big_picture.jpg new file mode 100644 index 00000000000..38d2ca15a39 Binary files /dev/null and b/addons/website_blog/static/src/img/s_latest_posts_big_picture.jpg differ diff --git a/addons/website_blog/static/src/js/s_latest_posts_editor.js b/addons/website_blog/static/src/js/s_latest_posts_editor.js new file mode 100644 index 00000000000..10052de87b7 --- /dev/null +++ b/addons/website_blog/static/src/js/s_latest_posts_editor.js @@ -0,0 +1,106 @@ +odoo.define('website_blog.s_latest_posts_editor', function (require) { +'use strict'; + +var core = require('web.core'); +var sOptions = require('web_editor.snippets.options'); + +var _t = core._t; + +sOptions.registry.js_get_posts_limit = sOptions.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for parameters + */ + postsLimit: function (previewMode, value, $opt) { + value = parseInt(value); + this.$target.attr('data-posts-limit', value).data('postsLimit', value); + this.trigger_up('animation_start_demand', { + editableMode: true, + $target: this.$target, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _setActive: function () { + var self = this; + this._super.apply(this, arguments); + this.$('[data-posts-limit]').addBack('[data-posts-limit]') + .removeClass('active') + .filter(function () { + return (self.$target.data('postsLimit') || 3) == $(this).data('postsLimit'); + }) + .addClass('active'); + }, +}); + +sOptions.registry.js_get_posts_selectBlog = sOptions.Class.extend({ + /** + * @override + */ + start: function () { + var self = this; + + var def = this._rpc({ + model: 'blog.blog', + method: 'search_read', + args: [[], ['name', 'id']], + }).then(function (blogs) { + var $menu = self.$el.find('[data-filter-by-blog-id="0"]').parent(); + _.each(blogs, function (blog) { + $menu.append($('', { + class: 'dropdown-item', + 'data-filter-by-blog-id': blog.id, + 'data-no-preview': 'true', + text: blog.name, + })); + }); + }); + + return $.when(this._super.apply(this, arguments), def); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for parameters + */ + filterByBlogId: function (previewMode, value, $opt) { + value = parseInt(value); + this.$target.attr('data-filter-by-blog-id', value).data('filterByBlogId', value); + this.trigger_up('animation_start_demand', { + editableMode: true, + $target: this.$target, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _setActive: function () { + var self = this; + this._super.apply(this, arguments); + this.$('[data-filter-by-blog-id]').addBack('[data-filter-by-blog-id]') + .removeClass('active') + .filter(function () { + return (self.$target.data('filterByBlogId') || 0) == $(this).data('filterByBlogId'); + }) + .addClass('active'); + }, +}); +}); diff --git a/addons/website_blog/static/src/js/s_latest_posts_frontend.js b/addons/website_blog/static/src/js/s_latest_posts_frontend.js new file mode 100644 index 00000000000..f1b8ea4f23d --- /dev/null +++ b/addons/website_blog/static/src/js/s_latest_posts_frontend.js @@ -0,0 +1,142 @@ +odoo.define('website_blog.s_latest_posts_frontend', function (require) { +'use strict'; + +var core = require('web.core'); +var sAnimation = require('website.content.snippets.animation'); + +var _t = core._t; + +sAnimation.registry.js_get_posts = sAnimation.Class.extend({ + selector : '.js_get_posts', + + /** + * @override + */ + start: function () { + var self = this; + var limit = self.$target.data('postsLimit') || 3; + var blogID = self.$target.data('filterByBlogId'); + var template = self.$target.data('template') || 'website_blog.s_latest_posts_list_template'; + var loading = self.$target.data('loading'); + + this.$target.empty(); // Compatibility with db that saved content inside by mistake + this.$target.attr('contenteditable', 'False'); // Prevent user edition + + var domain = [['website_published', '=', true]]; + if (blogID) { + domain.push(['blog_id', '=', parseInt(blogID)]); + } + + var def = $.Deferred(); + this._rpc({ + route: '/blog/render_latest_posts', + params: { + template: template, + domain: domain, + limit: limit, + }, + }).then(function (posts) { + var $posts = $(posts).filter('.s_latest_posts_post'); + if (!$posts.length) { + self.$target.append($('
', {class: 'col-md-6 offset-md-3'}) + .append($('
', { + class: 'alert alert-warning alert-dismissible text-center', + text: _t("No blog post was found. Make sure your posts are published."), + }))); + return; + } + + if (loading && loading === true) { + // Perform an intro animation + self._showLoading($posts); + } else { + self.$target.html($posts); + } + }, function (e) { + if (self.editableMode) { + self.$target.append($('

', { + class: 'text-danger', + text: _t("An error occured with this latest posts block. If the problem persists, please consider deleting it and adding a new one"), + })); + } + }).always(def.resolve.bind(def)); + return $.when(this._super.apply(this, arguments), def); + }, + /** + * @override + */ + destroy: function () { + this.$target.empty(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _showLoading: function ($posts) { + var self = this; + + _.each($posts, function (post) { + var $post = $(post); + var $loadingContainer = $post.find('.loading_container'); + var $thumb = $post.find('.thumb .o_blog_cover_image'); + var $progress = $('

', { + class: 'progress js-loading', + }).append($('
', { + class: 'progress-bar', + role: 'progressbar', + 'aria-valuenow': '0', + 'aria-valuemin': '0', + 'aria-valuemax': '100', + css: { + width: 0, + }, + })); + + // If can't find loading container or thumb inside the post, then they are the post itself + if (!$loadingContainer.length) { + $loadingContainer = $post; + } + if (!$thumb.length) { + $thumb = $post; + } + + $post.addClass('js-loading'); + $progress.appendTo($loadingContainer); + $post.appendTo(self.$target); + + var m = $thumb.css('background-image').match(/url\(["']?(.+)["']?\)/); + var bg = m ? m[1] : 'none'; + var loaded = false; + + var $bar = $progress.find('.progress-bar'); + $bar.css('width', '50%').attr('aria-valuenow', '50'); + + var $dummyImg = $(''); + + // Show the post after 5sec in any case + var timer = setTimeout(function () { + $dummyImg.remove(); + $post.removeClass('js-loading'); + $progress.hide(); + }, 5000); + + $dummyImg.load(function () { + $bar.css('width', '100%').attr('aria-valuenow', '100'); + setTimeout(function () { + $post.removeClass('js-loading'); + $progress.fadeOut(500); + }, 500); + $dummyImg.remove(); + clearTimeout(timer); + }); + + $dummyImg.attr('src', bg); + }); + }, +}); +}); diff --git a/addons/website_blog/static/src/scss/primary_variables.scss b/addons/website_blog/static/src/scss/primary_variables.scss new file mode 100644 index 00000000000..f5cda7678dc --- /dev/null +++ b/addons/website_blog/static/src/scss/primary_variables.scss @@ -0,0 +1,5 @@ + +$s-latest-posts-big-picture-figure-bg: null; + +@mixin s-latest-posts-js-get-posts-hook {}; +@mixin s-latest-posts-figure-hook {}; diff --git a/addons/website_blog/static/src/scss/website_blog.scss b/addons/website_blog/static/src/scss/website_blog.scss index cff15ed82e5..f16afd40bf2 100644 --- a/addons/website_blog/static/src/scss/website_blog.scss +++ b/addons/website_blog/static/src/scss/website_blog.scss @@ -232,3 +232,122 @@ padding: 2px 15px; } } + +//------------------------------------------------------------------------------ +// Latest posts snippets +//------------------------------------------------------------------------------ + +.s_latest_posts .media > a { + width: 100%; + height: 200px; + + @include media-breakpoint-up(md) { + width: 100px; + height: 100px; + } + + > .o_blog_cover_container { + position: relative; + width: 100%; + height: 100%; + padding: 0; + + > .o_blog_cover_component { + @include o-position-absolute(0, 0, 0, 0); + } + > .o_blog_cover_image { + background-size: cover; + background-position: center; + } + } +} + +$s-latest-posts-big-picture-figure-bg: $white !default; + +.s_latest_posts_big_picture { + .s_latest_posts_post { + position: relative; + height: 250px; + background: $s-latest-posts-big-picture-figure-bg; + text-align: center; + cursor: pointer; + transition: all 300ms; + + &.js-loading { + background: gray('200') !important; + } + + .thumb { + position: relative; + display: block; + width: 100%; + height: 100%; + padding: 0; + opacity: 0.8; + + > .o_blog_cover_component { + @include o-position-absolute(0, 0, 0, 0); + } + > .o_blog_cover_image { + background-size: cover; + background-position: center; + } + } + > div, > div > a { + @include o-position-absolute(0, 0, 0, 0); + padding: 2em; + color: $white; + text-transform: uppercase; + font-size: 1.25em; + backface-visibility: hidden; + &::after, &::before { + pointer-events: none; + } + + > a { + z-index: 1000; + text-indent: 200%; + white-space: nowrap; + font-size: 0; + opacity: 0; + } + h2 { + margin: 0; + font-size: 1.5em; + font-weight: 300; + span { + font-weight: 800; + } + } + p { + margin: 1em 0 0; + letter-spacing: 1px; + font-size: 68.5%; + } + } + @include s-latest-posts-figure-hook; + } + + @include s-latest-posts-js-get-posts-hook; +} + +.s_latest_posts, .s_latest_posts_big_picture { + + .js_get_posts { + position: relative; + min-height: 100px; + } + + .progress.js-loading { + @include o-position-absolute(0, 0, 0, 0); + border-radius: 0; + + .progress-bar { + @include o-position-absolute(45%, 10%, auto, 10%); + height: 10px; + margin: 0; + background: gray('600'); + box-shadow: none; + } + } +} diff --git a/addons/website_blog/views/snippets.xml b/addons/website_blog/views/snippets.xml index 6366b9c5458..9bb7a22136b 100644 --- a/addons/website_blog/views/snippets.xml +++ b/addons/website_blog/views/snippets.xml @@ -1,6 +1,11 @@ + + + diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml index 44d1d4a9ffa..1853eaefaa8 100644 --- a/addons/website_blog/views/website_blog_templates.xml +++ b/addons/website_blog/views/website_blog_templates.xml @@ -1,9 +1,16 @@ + + @@ -14,6 +21,7 @@