From a48bc56d4476d9d62f8845a3be9e3640ad365aea Mon Sep 17 00:00:00 2001 From: "stephen@cubedesigners.com" Date: Thu, 5 Mar 2020 20:00:28 +0000 Subject: [PATCH] WIP #3413 @5 --- .../controllers/NewsController.php | 17 +- framework/application/models/News.php | 68 ++++ framework/application/views/helpers/News.php | 2 +- .../views/scripts/common/body.phtml | 9 + .../views/scripts/news/index.phtml | 38 +- js/news-article.js | 17 + js/slick-lightbox.js | 335 ++++++++++++++++++ less/common.less | 3 +- less/news-article.less | 17 + less/slick-lightbox.less | 1 + less/utilities.less | 73 ++++ 11 files changed, 555 insertions(+), 25 deletions(-) create mode 100644 framework/application/views/scripts/common/body.phtml create mode 100644 js/news-article.js create mode 100644 js/slick-lightbox.js create mode 100644 less/news-article.less create mode 100644 less/slick-lightbox.less create mode 100644 less/utilities.less diff --git a/framework/application/controllers/NewsController.php b/framework/application/controllers/NewsController.php index 2db0df1..e3fba61 100644 --- a/framework/application/controllers/NewsController.php +++ b/framework/application/controllers/NewsController.php @@ -4,25 +4,26 @@ class NewsController extends CubeIT_Controller_PageController { public function indexAction() { + $this->view->headScript()->addSlickCarousel(); + $this->view->headScript()->addScriptAndStyle('slick-lightbox'); $this->view->headScript()->addScriptAndStyle('news-article'); $parent = $this->view->currentPage->getParent(); $parent_data = $this->getBootstrap()->getCMSDatasOfNavigationPage($parent); $this->view->datas = $parent_data; - $db = Zend_Db_Table::getDefaultAdapter(); - $select = $db->select()->from(['n' => 'news']) - ->where('n.id = ?', $this->getRequest()->getParam('news_id')); - $query = $select->query(); + $model = CCGM_Model_News::factory()->where('id = ?', $this->getRequest()->getParam('news_id')); + $article = $model->find(); - //echo 'QUERY: '. (string) $select; - - if (!$query->rowCount()) { + if (count($article) < 1) { $this->_404(); return; } - $this->view->news = $query->fetch(); + // Get the first and only array item + $article = reset($article); + + $this->view->news = $article; } } diff --git a/framework/application/models/News.php b/framework/application/models/News.php index 8f0ecff..71c016a 100644 --- a/framework/application/models/News.php +++ b/framework/application/models/News.php @@ -24,4 +24,72 @@ class CCGM_Model_News extends CubeIT_Model_Data_Table { $table->addColumn('pdf', 'text'); } + // Get the excerpt, which is just the first line of the content + public function getExcerpt() { + return explode(PHP_EOL, $this->content)[0]; + } + + // Process YouTube URLs entered and get poster images + public function getVideoDetails() { + $video_list = trim($this->videos); + $videos = []; + + if (empty($video_list)) return false; + + $video_URLs = explode(PHP_EOL, $video_list); + + foreach ($video_URLs as $video_URL) { + $video_ID = $this->getVideoID($video_URL); + if (!$video_ID) continue; + + $video_image = $this->youtube_image($video_ID); + if (!$video_image) continue; + + $videos[] = [ + 'url' => $video_URL, + 'image' => $video_image, + 'embed' => "https://www.youtube.com/embed/$video_ID", + ]; + } + + return $videos; + } + + public function getVideoID($URL) { + + $pattern = '/^.*(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:(?:watch)?\?(?:.*&)?vi?=|(?:embed|v|vi|user)\/))([^\?&\"\'>]+)/'; + preg_match($pattern, $URL, $matches); + + if ($matches) { + return trim($matches[1]); + } + + return false; + } + + // We can't guarantee that the video's maxresdefault.jpg will exist (it's not created for non-HD uploads) + // So this function will find the highest res image available... + // Ref: https://stackoverflow.com/a/20655623 + function youtube_image($id) { + $resolution = [ + 'maxresdefault', + 'hqdefault', // Might have black bars + 'mqdefault', // No black bars on this size normally + 'sddefault', + 'default' + ]; + + $url = false; + + for ($x = 0; $x < sizeof($resolution); $x++) { + $url = 'https://img.youtube.com/vi/' . $id . '/' . $resolution[$x] . '.jpg'; + + // Make sure we get a 200 OK HTTP response + if (strpos(get_headers($url)[0], '200 OK') !== false) { + break; + } + } + return $url; + } + } diff --git a/framework/application/views/helpers/News.php b/framework/application/views/helpers/News.php index ff98d98..7f341db 100644 --- a/framework/application/views/helpers/News.php +++ b/framework/application/views/helpers/News.php @@ -24,7 +24,7 @@ class CCGM_View_Helper_News extends CubeIT_View_Helper_Abstract { //$c = $this->dateTime($n->getDate(), CubeIT_Date::DAY . '/' . CubeIT_Date::MONTH . '/' . CubeIT_Date::YEAR); $c = $this->htmlElement($n->getTitle(), 'h2'); $c .= '
'; - $c .= $this->markupDotclear($n->getContent()); + $c .= $this->markupDotclear($n->getExcerpt()); $c .= $this->linkCMS($n->getLink()); $c .= '
'; $c .= $this->link('Lire la suite', $URL, ['class' => 'read-more']); diff --git a/framework/application/views/scripts/common/body.phtml b/framework/application/views/scripts/common/body.phtml new file mode 100644 index 0000000..60fcfd9 --- /dev/null +++ b/framework/application/views/scripts/common/body.phtml @@ -0,0 +1,9 @@ +bodyClass .'">' . "\n"; +echo $this->bannerCookies(); +echo $this->render('admin/mockup.phtml'); +echo $this->render('admin/nav.phtml'); +echo $this->render('common/content.phtml'); +echo $this->render('ajax/loader.phtml'); +echo "\n" . '' . "\n"; diff --git a/framework/application/views/scripts/news/index.phtml b/framework/application/views/scripts/news/index.phtml index 704cac5..6613214 100644 --- a/framework/application/views/scripts/news/index.phtml +++ b/framework/application/views/scripts/news/index.phtml @@ -1,29 +1,37 @@ bodyClass = 'page-news-article'; $this->showtopimage = false; $this->showsidebar = false; -$news = CubeIT_Util_CMS::unserialize($this->news); +$news = $this->news; ?> -

title ?>

+

Actualités

+ +

getTitle() ?>

- image as $image) { - echo $this->imageProcess($image, '', 1000, null); - } - ?> +
- markupDotclear($news->content); ?> + getImage()) as $image): ?> + imageProcess($image, '', 980, null, [], 'C', 'C', 'M', false, 'auto', null, 92, 'files', true); ?> + + + + - linkCMS($news->link); ?> + getVideoDetails() as $video): ?> + + + + -
+
-
-
-
+ markupDotclear($news->getContent()); ?> + + linkCMS($news->getLink()); ?> + + diff --git a/js/news-article.js b/js/news-article.js new file mode 100644 index 0000000..8a626ce --- /dev/null +++ b/js/news-article.js @@ -0,0 +1,17 @@ +TO_LOAD_ONCE[TO_LOAD_ONCE.length] = 'load_news_article();'; + +function load_news_article() { + + var $slider = $('.news-slider'); + + // Base slider in page + $slider.slick({ + slidesToShow: 1 + }); + + // Slider for lightbox + $slider.each(function () { + $(this).slickLightbox(); + }); + +} diff --git a/js/slick-lightbox.js b/js/slick-lightbox.js new file mode 100644 index 0000000..499ee17 --- /dev/null +++ b/js/slick-lightbox.js @@ -0,0 +1,335 @@ +// https://github.com/jongacnik/slick-lightbox/blob/master/dist/slick-lightbox.js + +'use strict'; + +/* + * SlickLightbox documentation # + +Documentation generated by [CoffeeDoc](http://github.com/omarkhan/coffeedoc) + */ +(function($) { + var SlickLightbox, defaults; + SlickLightbox = (function() { + + /* + The one and only class used. + */ + function SlickLightbox(element, options) { + var that; + this.options = options; + + /* Binds the plugin. */ + this.element = $(element); + this.didInit = false; + that = this; + this.element.on('click.slickLightbox', this.options.itemSelector, function(e) { + e.preventDefault(); + return that.init(that.element.find(that.options.itemSelector).index($(this))); + }); + } + + SlickLightbox.prototype.init = function(index) { + + /* Creates the lightbox, opens it, binds events and calls `slick`. Accepts `index` of the element, that triggered it (so that we know, on which slide to start slick). */ + this.didInit = true; + this.detectIE(); + this.createModal(index); + this.bindEvents(); + this.initSlick(); + return this.open(); + }; + + SlickLightbox.prototype.createModalItems = function(index) { + + /* Creates individual slides to be used with slick. If `options.images` array is specified, it uses it's contents, otherwise loops through elements' `options.itemSelector`. */ + var a, createItem, links; + if (this.options.images) { + links = $.map(this.options.images, function(img) { + return "
"; + }); + } else { + createItem = (function(_this) { + return function(el) { + var caption, src; + caption = _this.getElementCaption(el); + src = _this.getElementSrc(el); + if (_this.detectImage(src)) { + return "
" + caption + "
"; + } else { + return "
" + caption + "
"; + } + }; + })(this); + a = this.element.find(this.options.itemSelector); + if (index === 0 || index === -1) { + links = $.map(a, createItem); + } else { + links = $.map(a.slice(index), createItem); + $.each(a.slice(0, index), function(i, el) { + return links.push(createItem(el)); + }); + } + } + return links; + }; + + SlickLightbox.prototype.createModal = function(index) { + + /* Creates a `slick`-friendly modal. Rearranges the items so that the `index`-th item is placed first. */ + var html, links; + links = this.createModalItems(index); + html = "
\n
\n
" + (links.join('')) + "
\n \n
\n
"; + this.modalElement = $(html); + return $('body').append(this.modalElement); + }; + + SlickLightbox.prototype.initSlick = function(index) { + + /* Runs slick by default, using `options.slick` if provided. If `options.slick` is a function, it gets fired instead of us initializing slick. */ + if (this.options.slick != null) { + if (typeof this.options.slick === 'function') { + + /* Function expected to return slick instance. */ + this.slick = this.options.slick(this.modalElement); + } else { + this.slick = this.modalElement.find('.slick-lightbox-slick').slick(this.options.slick); + } + } else { + this.slick = this.modalElement.find('.slick-lightbox-slick').slick(); + } + return this.modalElement.trigger('init.slickLightbox'); + }; + + SlickLightbox.prototype.open = function() { + + /* Opens the lightbox. */ + this.element.trigger('show.slickLightbox'); + setTimeout(((function(_this) { + return function() { + return _this.element.trigger('shown.slickLightbox'); + }; + })(this)), this.getTransitionDuration()); + return this.modalElement.removeClass('slick-hide-init'); + }; + + SlickLightbox.prototype.close = function() { + + /* Closes the lightbox and destroys it, maintaining the original element bindings. */ + this.element.trigger('hide.slickLightbox'); + setTimeout(((function(_this) { + return function() { + return _this.element.trigger('hidden.slickLightbox'); + }; + })(this)), this.getTransitionDuration()); + this.modalElement.addClass('slick-hide'); + return this.destroy(); + }; + + SlickLightbox.prototype.bindEvents = function() { + + /* Binds global events. */ + var resizeSlides; + resizeSlides = (function(_this) { + return function() { + var h; + h = _this.modalElement.find('.slick-lightbox-inner').height(); + _this.modalElement.find('.slick-lightbox-slick-item').height(h); + return _this.modalElement.find('.slick-lightbox-slick-img').css('max-height', Math.round(0.9 * h)); + }; + })(this); + $(window).on('orientationchange.slickLightbox resize.slickLightbox', resizeSlides); + this.modalElement.on('init.slickLightbox', resizeSlides); + this.modalElement.on('destroy.slickLightbox', (function(_this) { + return function() { + return _this.destroy(); + }; + })(this)); + this.element.on('destroy.slickLightbox', (function(_this) { + return function() { + return _this.destroy(true); + }; + })(this)); + this.modalElement.on('click.slickLightbox touchstart.slickLightbox', '.slick-lightbox-close', (function(_this) { + return function(e) { + e.preventDefault(); + return _this.close(); + }; + })(this)); + if (this.options.closeOnEscape || this.options.navigateByKeyboard) { + $(document).on('keydown.slickLightbox', (function(_this) { + return function(e) { + var code; + code = e.keyCode ? e.keyCode : e.which; + if (_this.options.navigateByKeyboard) { + if (code === 37) { + _this.slideSlick('left'); + } else if (code === 39) { + _this.slideSlick('right'); + } + } + if (_this.options.closeOnEscape) { + if (code === 27) { + return _this.close(); + } + } + }; + })(this)); + } + if (this.options.closeOnBackdropClick) { + this.modalElement.on('click.slickLightbox touchstart.slickLightbox', '.slick-lightbox-slick-img', (function(_this) { + return function(e) { + return e.stopPropagation(); + }; + })(this)); + this.modalElement.on('click.slickLightbox touchstart.slickLightbox', '.slick-lightbox-slick-iframe-wrap', (function(_this) { + return function(e) { + return e.stopPropagation(); + }; + })(this)); + return this.modalElement.on('click.slickLightbox touchstart.slickLightbox', '.slick-lightbox-slick-item', (function(_this) { + return function(e) { + e.preventDefault(); + return _this.close(); + }; + })(this)); + } + }; + + SlickLightbox.prototype.slideSlick = function(direction) { + + /* Moves the slick prev or next. */ + if (direction === 'left') { + return this.slick.slick('slickPrev'); + } else { + return this.slick.slick('slickNext'); + } + }; + + SlickLightbox.prototype.detectIE = function() { + + /* Detects usage of IE8 and lower. */ + var ieversion; + this.isIE = false; + if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { + ieversion = new Number(RegExp.$1); + if (ieversion < 9) { + return this.isIE = true; + } + } + }; + + SlickLightbox.prototype.getElementCaption = function(el) { + + /* Returns caption for each slide based on the type of `options.caption`. */ + var c; + if (!this.options.caption) { + return ''; + } + c = (function() { + switch (typeof this.options.caption) { + case 'function': + return this.options.caption(el); + case 'string': + return $(el).data(this.options.caption); + } + }).call(this); + return "" + c + ""; + }; + + SlickLightbox.prototype.getElementSrc = function(el) { + + /* Returns src for each slide image based on the type of `options.src`. */ + switch (typeof this.options.src) { + case 'function': + return this.options.src(el); + case 'string': + return $(el).attr(this.options.src); + default: + return el.href; + } + }; + + SlickLightbox.prototype.detectImage = function(url) { + + /* Returns true if finds image file extension */ + return url.match(/\.(jpeg|jpg|gif|png)$/) !== null; + }; + + SlickLightbox.prototype.unbindEvents = function() { + + /* Unbinds global events. */ + $(window).off('.slickLightbox'); + $(document).off('.slickLightbox'); + return this.modalElement.off('.slickLightbox'); + }; + + SlickLightbox.prototype.destroy = function(unbindAnchors) { + if (unbindAnchors == null) { + unbindAnchors = false; + } + + /* Destroys the lightbox and unbinds global events. If `true` is passed as an argument, unbinds the original element as well. */ + if (this.didInit) { + this.unbindEvents(); + setTimeout(((function(_this) { + return function() { + return _this.modalElement.remove(); + }; + })(this)), this.options.destroyTimeout); + } + if (unbindAnchors) { + this.element.off('.slickLightbox'); + return this.element.off('.slickLightbox', this.options.itemSelector); + } + }; + + SlickLightbox.prototype.destroyPrevious = function() { + + /* Destroys lightboxes currently in DOM. */ + return $('body').children('.slick-lightbox').trigger('destroy.slickLightbox'); + }; + + SlickLightbox.prototype.getTransitionDuration = function() { + + /* Detects the transition duration to know when to remove stuff from DOM etc. */ + var duration; + if (this.transitionDuration) { + return this.transitionDuration; + } + duration = this.modalElement.css('transition-duration'); + if (typeof duration === 'undefined') { + return this.transitionDuration = 500; + } else { + return this.transitionDuration = duration.indexOf('ms') > -1 ? parseFloat(duration) : parseFloat(duration) * 1000; + } + }; + + return SlickLightbox; + + })(); + defaults = { + background: 'rgba(0,0,0,.8)', + closeOnEscape: true, + closeOnBackdropClick: true, + destroyTimeout: 500, + itemSelector: 'a', + navigateByKeyboard: true, + src: false, + caption: false, + captionPosition: 'dynamic', + images: false, + slick: {} + }; + $.fn.slickLightbox = function(options) { + + /* Fires the plugin. */ + options = $.extend({}, defaults, options); + new SlickLightbox(this, options); + return this; + }; + return $.fn.unslickLightbox = function() { + + /* Removes everything. */ + return $(this).trigger('destroy.slickLightbox'); + }; +})(jQuery); diff --git a/less/common.less b/less/common.less index dbe8150..1470861 100644 --- a/less/common.less +++ b/less/common.less @@ -1,4 +1,5 @@ @import "constants.less"; +@import "utilities.less"; * { max-height: 1000000px; @@ -12,7 +13,7 @@ html { body { margin: 0; color: #5e5e5e; - font: 300 16px/20px @Ubuntu; + font: 300 16px/1.25 @Ubuntu; background-color: #fff; min-width: 990px; -webkit-text-size-adjust: 100%; diff --git a/less/news-article.less b/less/news-article.less new file mode 100644 index 0000000..ba8274a --- /dev/null +++ b/less/news-article.less @@ -0,0 +1,17 @@ +.page-news-article .breadcrumbs { + margin: 2rem 0; + padding-bottom: 0; +} + +.news-slider { + &-image { + max-width: 490px; + display: block; + margin: 0 auto; + } + + .slick-track { + display: flex; + align-items: center; + } +} diff --git a/less/slick-lightbox.less b/less/slick-lightbox.less new file mode 100644 index 0000000..2893c15 --- /dev/null +++ b/less/slick-lightbox.less @@ -0,0 +1 @@ +.slick-lightbox{position:fixed;top:0;left:0;z-index:9999;width:100%;height:100%;background:#000;-webkit-transition:opacity .5s ease;transition:opacity .5s ease}.slick-lightbox.slick-hide{opacity:0}.slick-lightbox.slick-hide-init{position:absolute;top:-9999px;opacity:0}.slick-lightbox.slick-lightbox-ie.slick-hide,.slick-lightbox.slick-lightbox-ie.slick-hide-init{-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0)}.slick-lightbox .slick-lightbox-inner{position:fixed;top:0;left:0;width:100%;height:100%}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item{text-align:center;overflow:hidden}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item:before{content:'';display:inline-block;height:100%;vertical-align:middle;margin-right:-0.25em}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner{display:inline-block;vertical-align:middle;max-width:90%;max-height:90%}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner.iframe{width:70%;height:90%}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner .slick-lightbox-slick-img{margin:0 auto;display:block;max-width:90%;max-height:90%}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner .slick-lightbox-slick-iframe-wrap{position:relative;top:50%;transform:translateY(-50%)}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner .slick-lightbox-slick-iframe-wrap:before{content:'';display:block;padding-top:56.25%}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner .slick-lightbox-slick-iframe-wrap .slick-lightbox-slick-caption{position:relative;top:30px}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner .slick-lightbox-slick-iframe-wrap .slick-lightbox-slick-iframe{position:absolute;top:0;left:0;width:100%;height:100%;z-index:0}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick .slick-lightbox-slick-item .slick-lightbox-slick-item-inner .slick-lightbox-slick-caption{margin-top:10px;color:#fff}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick.slick-caption-bottom .slick-lightbox-slick-item .slick-lightbox-slick-caption{position:absolute;bottom:0;left:0;text-align:center;width:100%;margin-bottom:20px}.slick-lightbox .slick-lightbox-inner .slick-lightbox-slick.slick-caption-dynamic .slick-lightbox-slick-item .slick-lightbox-slick-caption{display:block;text-align:center}.slick-lightbox .slick-lightbox-inner .slick-lightbox-close{position:absolute;top:15px;right:15px;display:block;height:20px;width:20px;line-height:0;font-size:0;cursor:pointer;background:transparent;color:transparent;padding:0;border:none}.slick-lightbox .slick-lightbox-inner .slick-lightbox-close:focus{outline:none}.slick-lightbox .slick-lightbox-inner .slick-lightbox-close:before{font-family:"slick";font-size:20px;line-height:1;color:#fff;opacity:.85;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:'×'}.slick-lightbox .slick-loading .slick-list{background-color:transparent}.slick-lightbox .slick-prev{left:15px}.slick-lightbox .slick-next{right:15px} diff --git a/less/utilities.less b/less/utilities.less new file mode 100644 index 0000000..dfe57d1 --- /dev/null +++ b/less/utilities.less @@ -0,0 +1,73 @@ +// Colours +.text-orange { color: #f5810f; } +.text-pink { color: #e30186; } + +// Font sizes +.text-xs { font-size: .75rem; } +.text-sm { font-size: .875rem; } +.text-base { font-size: 1rem; } +.text-lg { font-size: 1.125rem; } +.text-xl { font-size: 1.25rem; } +.text-2xl { font-size: 1.5rem; } +.text-3xl { font-size: 1.875rem; } +.text-4xl { font-size: 2.25rem; } +.text-5xl { font-size: 3.25rem; } +.text-6xl { font-size: 4rem; } + +// Font weights +.font-light { font-weight: 300; } +.font-medium { font-weight: 500; } + +// Line heights +.leading-none { line-height: 1; } +.leading-tight { line-height: 1.25; } +.leading-snug { line-height: 1.375; } +.leading-normal { line-height: 1.5; } +.leading-relaxed { line-height: 1.625; } +.leading-loose { line-height: 2; } + +// Margins +.m-0 { margin: 0; } +.m-1 { margin: 0.25rem; } +.m-2 { margin: 0.5rem; } +.m-3 { margin: 0.75rem; } +.m-4 { margin: 1rem; } +.m-5 { margin: 1.25rem; } +.m-6 { margin: 1.5rem; } +.m-8 { margin: 2rem; } +.m-10 { margin: 2.5rem; } +.m-12 { margin: 3rem; } + +.mt-0 { margin-top: 0; } +.mt-1 { margin-top: 0.25rem; } +.mt-2 { margin-top: 0.5rem; } +.mt-3 { margin-top: 0.75rem; } +.mt-4 { margin-top: 1rem; } +.mt-5 { margin-top: 1.25rem; } +.mt-6 { margin-top: 1.5rem; } +.mt-8 { margin-top: 2rem; } +.mt-10 { margin-top: 2.5rem; } +.mt-12 { margin-top: 3rem; } + +.mb-0 { margin-bottom: 0; } +.mb-1 { margin-bottom: 0.25rem; } +.mb-2 { margin-bottom: 0.5rem; } +.mb-3 { margin-bottom: 0.75rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-5 { margin-bottom: 1.25rem; } +.mb-6 { margin-bottom: 1.5rem; } +.mb-8 { margin-bottom: 2rem; } +.mb-10 { margin-bottom: 2.5rem; } +.mb-12 { margin-bottom: 3rem; } + +// Padding +.p-0 { padding: 0; } +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-3 { padding: 0.75rem; } +.p-4 { padding: 1rem; } +.p-5 { padding: 1.25rem; } +.p-6 { padding: 1.5rem; } +.p-8 { padding: 2rem; } +.p-10 { padding: 2.5rem; } +.p-12 { padding: 3rem; } -- 2.39.5