From cca3533d35c23848edff7edcbfe0327fd9d283a7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 30 Dec 2015 19:57:17 +0000 Subject: [PATCH] Improved error messages for image uploads and formatted much js --- resources/assets/js/controllers.js | 238 +++++++++++----------- resources/assets/js/directives.js | 48 +++-- resources/assets/js/global.js | 12 +- resources/assets/js/pages/page-form.js | 75 ++++--- resources/assets/sass/_image-manager.scss | 190 ++++++++++++----- 5 files changed, 324 insertions(+), 239 deletions(-) diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 5d2d4bbca..5cf71579a 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -1,133 +1,133 @@ "use strict"; -module.exports = function(ngApp) { +module.exports = function (ngApp) { - ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout','imageManagerService', - function($scope, $attrs, $http, $timeout, imageManagerService) { - $scope.images = []; - $scope.imageType = $attrs.imageType; - $scope.selectedImage = false; - $scope.dependantPages = false; - $scope.showing = false; - $scope.hasMore = false; - $scope.imageUpdateSuccess = false; - $scope.imageDeleteSuccess = false; - var page = 0; - var previousClickTime = 0; - var dataLoaded = false; - var callback = false; - - $scope.getUploadUrl = function() { - return '/images/' + $scope.imageType + '/upload'; - }; - - $scope.uploadSuccess = function(file, data) { - $scope.$apply(() => { - $scope.images.unshift(data); - }); - }; - - function callbackAndHide(returnData) { - if (callback) callback(returnData); + ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', + function ($scope, $attrs, $http, $timeout, imageManagerService) { + $scope.images = []; + $scope.imageType = $attrs.imageType; + $scope.selectedImage = false; + $scope.dependantPages = false; $scope.showing = false; - } + $scope.hasMore = false; + $scope.imageUpdateSuccess = false; + $scope.imageDeleteSuccess = false; + var page = 0; + var previousClickTime = 0; + var dataLoaded = false; + var callback = false; - $scope.imageSelect = function (image) { - var dblClickTime = 300; - var currentTime = Date.now(); - var timeDiff = currentTime - previousClickTime; + $scope.getUploadUrl = function () { + return '/images/' + $scope.imageType + '/upload'; + }; - if (timeDiff < dblClickTime) { - // If double click - callbackAndHide(image); - } else { - // If single - $scope.selectedImage = image; - $scope.dependantPages = false; - } - previousClickTime = currentTime; - }; - - $scope.selectButtonClick = function() { - callbackAndHide($scope.selectedImage); - }; - - function show(doneCallback) { - callback = doneCallback; - $scope.showing = true; - // Get initial images if they have not yet been loaded in. - if (!dataLoaded) { - fetchData(); - dataLoaded = true; - } - } - - imageManagerService.show = show; - imageManagerService.showExternal = function(doneCallback) { - $scope.$apply(() => { - show(doneCallback); - }); - }; - window.ImageManager = imageManagerService; - - $scope.hide = function() { - $scope.showing = false; - }; - - function fetchData() { - var url = '/images/' + $scope.imageType + '/all/' + page; - $http.get(url).then((response) => { - $scope.images = $scope.images.concat(response.data.images); - $scope.hasMore = response.data.hasMore; - page++; - }); - } - - $scope.saveImageDetails = function(event) { - event.preventDefault(); - var url = '/images/update/' + $scope.selectedImage.id; - $http.put(url, this.selectedImage).then((response) => { - $scope.imageUpdateSuccess = true; - $timeout(() => { - $scope.imageUpdateSuccess = false; - }, 3000); - }, (response) => { - var errors = response.data; - var message = ''; - Object.keys(errors).forEach((key) => { - message += errors[key].join('\n'); + $scope.uploadSuccess = function (file, data) { + $scope.$apply(() => { + $scope.images.unshift(data); }); - $scope.imageUpdateFailure = message; - $timeout(() => { - $scope.imageUpdateFailure = false; - }, 5000); - }); - }; + }; - $scope.deleteImage = function(event) { - event.preventDefault(); - var force = $scope.dependantPages !== false; - var url = '/images/' + $scope.selectedImage.id; - if (force) url += '?force=true'; - $http.delete(url).then((response) => { - $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); - $scope.selectedImage = false; - $scope.imageDeleteSuccess = true; - $timeout(() => { - $scope.imageDeleteSuccess = false; - }, 3000); - }, (response) => { - // Pages failure - if (response.status === 400) { - $scope.dependantPages = response.data; + function callbackAndHide(returnData) { + if (callback) callback(returnData); + $scope.showing = false; + } + + $scope.imageSelect = function (image) { + var dblClickTime = 300; + var currentTime = Date.now(); + var timeDiff = currentTime - previousClickTime; + + if (timeDiff < dblClickTime) { + // If double click + callbackAndHide(image); + } else { + // If single + $scope.selectedImage = image; + $scope.dependantPages = false; } - }); - }; + previousClickTime = currentTime; + }; - }]); + $scope.selectButtonClick = function () { + callbackAndHide($scope.selectedImage); + }; + + function show(doneCallback) { + callback = doneCallback; + $scope.showing = true; + // Get initial images if they have not yet been loaded in. + if (!dataLoaded) { + fetchData(); + dataLoaded = true; + } + } + + imageManagerService.show = show; + imageManagerService.showExternal = function (doneCallback) { + $scope.$apply(() => { + show(doneCallback); + }); + }; + window.ImageManager = imageManagerService; + + $scope.hide = function () { + $scope.showing = false; + }; + + function fetchData() { + var url = '/images/' + $scope.imageType + '/all/' + page; + $http.get(url).then((response) => { + $scope.images = $scope.images.concat(response.data.images); + $scope.hasMore = response.data.hasMore; + page++; + }); + } + + $scope.saveImageDetails = function (event) { + event.preventDefault(); + var url = '/images/update/' + $scope.selectedImage.id; + $http.put(url, this.selectedImage).then((response) => { + $scope.imageUpdateSuccess = true; + $timeout(() => { + $scope.imageUpdateSuccess = false; + }, 3000); + }, (response) => { + var errors = response.data; + var message = ''; + Object.keys(errors).forEach((key) => { + message += errors[key].join('\n'); + }); + $scope.imageUpdateFailure = message; + $timeout(() => { + $scope.imageUpdateFailure = false; + }, 5000); + }); + }; + + $scope.deleteImage = function (event) { + event.preventDefault(); + var force = $scope.dependantPages !== false; + var url = '/images/' + $scope.selectedImage.id; + if (force) url += '?force=true'; + $http.delete(url).then((response) => { + $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); + $scope.selectedImage = false; + $scope.imageDeleteSuccess = true; + $timeout(() => { + $scope.imageDeleteSuccess = false; + }, 3000); + }, (response) => { + // Pages failure + if (response.status === 400) { + $scope.dependantPages = response.data; + } + }); + }; + + }]); - ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', function($scope, $http, $attrs) { + ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', function ($scope, $http, $attrs) { $scope.searching = false; $scope.searchTerm = ''; $scope.searchResults = ''; @@ -151,7 +151,7 @@ module.exports = function(ngApp) { } }; - $scope.clearSearch = function() { + $scope.clearSearch = function () { $scope.searching = false; $scope.searchTerm = ''; }; diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 4e936388b..758f16cd2 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -5,25 +5,25 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html'); var imagePickerTemplate = require('./components/image-picker.html'); var dropZoneTemplate = require('./components/drop-zone.html'); -module.exports = function(ngApp) { +module.exports = function (ngApp) { /** * Toggle Switches * Has basic on/off functionality. * Use string values of 'true' & 'false' to dictate the current state. */ - ngApp.directive('toggleSwitch', function() { + ngApp.directive('toggleSwitch', function () { return { restrict: 'E', template: toggleSwitchTemplate, scope: true, - link: function(scope, element, attrs) { + link: function (scope, element, attrs) { scope.name = attrs.name; scope.value = attrs.value; scope.isActive = scope.value == true && scope.value != 'false'; scope.value = (scope.value == true && scope.value != 'false') ? 'true' : 'false'; - scope.switch = function() { + scope.switch = function () { scope.isActive = !scope.isActive; scope.value = scope.isActive ? 'true' : 'false'; } @@ -37,7 +37,7 @@ module.exports = function(ngApp) { * Image Picker * Is a simple front-end interface that connects to an ImageManager if present. */ - ngApp.directive('imagePicker', ['$http', 'imageManagerService', function($http, imageManagerService) { + ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) { return { restrict: 'E', template: imagePickerTemplate, @@ -52,7 +52,7 @@ module.exports = function(ngApp) { defaultImage: '@', imageClass: '@' }, - link: function(scope, element, attrs) { + link: function (scope, element, attrs) { var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false'; scope.image = scope.currentImage; scope.value = scope.currentImage || ''; @@ -62,22 +62,22 @@ module.exports = function(ngApp) { scope.value = usingIds ? imageModel.id : imageUrl; } - scope.reset = function() { + scope.reset = function () { setImage({id: 0}, scope.defaultImage); }; - scope.remove = function() { + scope.remove = function () { scope.image = 'none'; scope.value = 'none'; }; - scope.showImageManager = function() { + scope.showImageManager = function () { imageManagerService.show((image) => { scope.updateImageFromModel(image); }); }; - scope.updateImageFromModel = function(model) { + scope.updateImageFromModel = function (model) { var isResized = scope.resizeWidth && scope.resizeHeight; if (!isResized) { @@ -102,7 +102,7 @@ module.exports = function(ngApp) { * DropZone * Used for uploading images */ - ngApp.directive('dropZone', [function() { + ngApp.directive('dropZone', [function () { return { restrict: 'E', template: dropZoneTemplate, @@ -111,26 +111,32 @@ module.exports = function(ngApp) { eventSuccess: '=', eventError: '=' }, - link: function(scope, element, attrs) { + link: function (scope, element, attrs) { var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { url: scope.uploadUrl, - init: function() { + init: function () { var dz = this; - dz.on('sending', function(file, xhr, data) { + dz.on('sending', function (file, xhr, data) { var token = window.document.querySelector('meta[name=token]').getAttribute('content'); data.append('_token', token); }); if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess); - dz.on('success', function(file, data) { + dz.on('success', function (file, data) { $(file.previewElement).fadeOut(400, function () { dz.removeFile(file); }); }); if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError); dz.on('error', function (file, errorMessage, xhr) { - if (errorMessage.file) { - $(file.previewElement).find('[data-dz-errormessage]').text(errorMessage.file[0]); + console.log(errorMessage); + console.log(xhr); + function setMessage(message) { + $(file.previewElement).find('[data-dz-errormessage]').text(message); } + + if (xhr.status === 413) setMessage('The server does not allow uploads of this size. Please try a smaller file.'); + if (errorMessage.file) setMessage(errorMessage.file[0]); + }); } }); @@ -139,14 +145,14 @@ module.exports = function(ngApp) { }]); - ngApp.directive('dropdown', [function() { + ngApp.directive('dropdown', [function () { return { restrict: 'A', - link: function(scope, element, attrs) { + link: function (scope, element, attrs) { var menu = element.find('ul'); - element.find('[dropdown-toggle]').on('click', function() { + element.find('[dropdown-toggle]').on('click', function () { menu.show().addClass('anim menuIn'); - element.mouseleave(function() { + element.mouseleave(function () { menu.hide(); menu.removeClass('anim menuIn'); }); diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index e740654fd..2cd45e5bc 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -18,8 +18,8 @@ var controllers = require('./controllers')(ngApp); //Global jQuery Config & Extensions // Smooth scrolling -jQuery.fn.smoothScrollTo = function() { - if(this.length === 0) return; +jQuery.fn.smoothScrollTo = function () { + if (this.length === 0) return; $('body').animate({ scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin }, 800); // Adjust to change animations speed (ms) @@ -27,8 +27,8 @@ jQuery.fn.smoothScrollTo = function() { }; // Making contains text expression not worry about casing -$.expr[":"].contains = $.expr.createPseudo(function(arg) { - return function( elem ) { +$.expr[":"].contains = $.expr.createPseudo(function (arg) { + return function (elem) { return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; }; }); @@ -42,7 +42,7 @@ $(function () { }); // Chapter page list toggles - $('.chapter-toggle').click(function(e) { + $('.chapter-toggle').click(function (e) { e.preventDefault(); $(this).toggleClass('open'); $(this).closest('.chapter').find('.inset-list').slideToggle(180); @@ -56,7 +56,7 @@ function elemExists(selector) { } // TinyMCE editor -if(elemExists('#html-editor')) { +if (elemExists('#html-editor')) { var tinyMceOptions = require('./pages/page-form'); tinymce.init(tinyMceOptions); } \ No newline at end of file diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index cadccb59c..b5773ec7d 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -1,4 +1,3 @@ - module.exports = { selector: '#html-editor', content_css: [ @@ -27,13 +26,13 @@ module.exports = { {title: "Code Block", icon: "code", format: "pre"}, {title: "Inline Code", icon: "code", inline: "code"} ], - formats : { - alignleft : {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes : 'align-left'}, - aligncenter : {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes : 'align-center'}, - alignright : {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes : 'align-right'}, + formats: { + alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'}, + aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'}, + alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, }, - file_browser_callback: function(field_name, url, type, win) { - ImageManager.show(function(image) { + file_browser_callback: function (field_name, url, type, win) { + ImageManager.show(function (image) { win.document.getElementById(field_name).value = image.url; if ("createEvent" in document) { var evt = document.createEvent("HTMLEvents"); @@ -44,63 +43,63 @@ module.exports = { } }); }, - paste_preprocess: function(plugin, args) { + paste_preprocess: function (plugin, args) { var content = args.content; - if(content.indexOf(''; - html += ''+image.name+''; + onclick: function () { + window.ImageManager.showExternal(function (image) { + var html = ''; + html += '' + image.name + ''; html += ''; editor.execCommand('mceInsertContent', false, html); }); @@ -108,10 +107,10 @@ module.exports = { }); // Paste image-uploads - editor.on('paste', function(e) { - if(e.clipboardData) { + editor.on('paste', function (e) { + if (e.clipboardData) { var items = e.clipboardData.items; - if (items){ + if (items) { for (var i = 0; i < items.length; i++) { if (items[i].type.indexOf("image") !== -1) { @@ -128,14 +127,14 @@ module.exports = { } var id = "image-" + Math.random().toString(16).slice(2); - editor.execCommand('mceInsertContent', false, ''); + editor.execCommand('mceInsertContent', false, ''); var remoteFilename = "image-" + Date.now() + "." + ext; formData.append('file', file, remoteFilename); formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content')); xhr.open('POST', '/upload/image'); - xhr.onload = function() { + xhr.onload = function () { if (xhr.status === 200 || xhr.status === 201) { var result = JSON.parse(xhr.responseText); editor.dom.setAttrib(id, 'src', result.url); diff --git a/resources/assets/sass/_image-manager.scss b/resources/assets/sass/_image-manager.scss index 3b3ce7d9c..babbad0c1 100644 --- a/resources/assets/sass/_image-manager.scss +++ b/resources/assets/sass/_image-manager.scss @@ -32,6 +32,7 @@ font-weight: 300; } } + #image-manager .dropzone-container { position: relative; border: 3px dashed #DDD; @@ -52,7 +53,7 @@ width: (100%/6); height: auto; border: 1px solid #FFF; - transition: all cubic-bezier(.4,0,1,1) 160ms; + transition: all cubic-bezier(.4, 0, 1, 1) 160ms; &.selected { transform: scale3d(0.92, 0.92, 0.92); } @@ -77,6 +78,7 @@ padding: 0 $-l; border-left: 1px solid #DDD; } + .image-manager-close { position: absolute; top: 0; @@ -84,6 +86,7 @@ margin: 0; border-radius: 0; } + .image-manager-list { overflow-y: scroll; flex: 1; @@ -97,9 +100,6 @@ flex: 1; } - - - // Dropzone /* * The MIT License @@ -114,69 +114,104 @@ padding: $-xl $-m; transition: all ease-in-out 120ms; } + .dz-drag-hover .dz-message { background-color: rgb(16, 126, 210); color: #EEE; } + @keyframes passing-through { 0% { opacity: 0; - transform: translateY(40px); } + transform: translateY(40px); + } 30%, 70% { opacity: 1; - transform: translateY(0px); } + transform: translateY(0px); + } 100% { opacity: 0; - transform: translateY(-40px); } } + transform: translateY(-40px); + } +} @keyframes slide-in { 0% { opacity: 0; - transform: translateY(40px); } + transform: translateY(40px); + } 30% { opacity: 1; - transform: translateY(0px); } } + transform: translateY(0px); + } +} + @keyframes pulse { 0% { - transform: scale(1); } + transform: scale(1); + } 10% { - transform: scale(1.1); } + transform: scale(1.1); + } 20% { - transform: scale(1); } } -.dropzone, .dropzone * { - box-sizing: border-box; } + transform: scale(1); + } +} +.dropzone, .dropzone * { + box-sizing: border-box; +} .dz-preview { position: relative; display: inline-block; vertical-align: top; margin: 12px; - min-height: 80px; } + min-height: 80px; +} + .dz-preview:hover { - z-index: 1000; } + z-index: 1000; +} + .dz-preview:hover .dz-details { - opacity: 1; } + opacity: 1; +} + .dz-preview.dz-file-preview .dz-image { border-radius: 4px; background: #999; - background: linear-gradient(to bottom, #eee, #ddd); } + background: linear-gradient(to bottom, #eee, #ddd); +} + .dz-preview.dz-file-preview .dz-details { - opacity: 1; } + opacity: 1; +} + .dz-preview.dz-image-preview { - background: white; } + background: white; +} + .dz-preview.dz-image-preview .dz-details { - transition: opacity 0.2s linear; } + transition: opacity 0.2s linear; +} + .dz-preview .dz-remove { font-size: 14px; text-align: center; display: block; cursor: pointer; - border: none; } + border: none; +} + .dz-preview .dz-remove:hover { - text-decoration: underline; } + text-decoration: underline; +} + .dz-preview:hover .dz-details { - opacity: 1; } + opacity: 1; +} + .dz-preview .dz-details { z-index: 20; position: absolute; @@ -189,26 +224,42 @@ padding: 6px 3px; text-align: center; color: rgba(0, 0, 0, 0.9); - line-height: 150%; } + line-height: 150%; +} + .dz-preview .dz-details .dz-size { margin-bottom: 0.5em; - font-size: 12px; } + font-size: 12px; +} + .dz-preview .dz-details .dz-filename { - white-space: nowrap; } + white-space: nowrap; +} + .dz-preview .dz-details .dz-filename:hover span { border: 1px solid rgba(200, 200, 200, 0.8); - background-color: rgba(255, 255, 255, 0.8); } + background-color: rgba(255, 255, 255, 0.8); +} + .dz-preview .dz-details .dz-filename:not(:hover) { overflow: hidden; - text-overflow: ellipsis; } + text-overflow: ellipsis; +} + .dz-preview .dz-details .dz-filename:not(:hover) span { - border: 1px solid transparent; } + border: 1px solid transparent; +} + .dz-preview .dz-details .dz-filename span, .dz-preview .dz-details .dz-size span { background-color: rgba(255, 255, 255, 0.4); padding: 0 0.4em; - border-radius: 3px; } + border-radius: 3px; +} + .dz-preview:hover .dz-image img { - filter: blur(8px); } + filter: blur(8px); +} + .dz-preview .dz-image { border-radius: 4px; overflow: hidden; @@ -216,14 +267,22 @@ height: 80px; position: relative; display: block; - z-index: 10; } + z-index: 10; +} + .dz-preview .dz-image img { - display: block; } + display: block; +} + .dz-preview.dz-success .dz-success-mark { - animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); } + animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); +} + .dz-preview.dz-error .dz-error-mark { opacity: 1; - animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); } + animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); +} + .dz-preview .dz-success-mark, .dz-preview .dz-error-mark { pointer-events: none; opacity: 0; @@ -233,19 +292,29 @@ top: 50%; left: 50%; margin-left: -27px; - margin-top: -27px; } + margin-top: -27px; +} + .dz-preview .dz-success-mark svg, .dz-preview .dz-error-mark svg { display: block; width: 54px; - height: 54px; } + height: 54px; +} + .dz-preview.dz-processing .dz-progress { opacity: 1; - transition: all 0.2s linear; } + transition: all 0.2s linear; +} + .dz-preview.dz-complete .dz-progress { opacity: 0; - transition: opacity 0.4s ease-in; } + transition: opacity 0.4s ease-in; +} + .dz-preview:not(.dz-processing) .dz-progress { - animation: pulse 6s ease infinite; } + animation: pulse 6s ease infinite; +} + .dz-preview .dz-progress { opacity: 1; z-index: 1000; @@ -260,7 +329,9 @@ background: rgba(255, 255, 255, 0.9); transform: scale(1); border-radius: 8px; - overflow: hidden; } + overflow: hidden; +} + .dz-preview .dz-progress .dz-upload { background: #333; background: linear-gradient(to bottom, #666, #444); @@ -269,12 +340,18 @@ left: 0; bottom: 0; width: 0; - transition: width 300ms ease-in-out; } + transition: width 300ms ease-in-out; +} + .dz-preview.dz-error .dz-error-message { - display: block; } + display: block; +} + .dz-preview.dz-error:hover .dz-error-message { opacity: 1; - pointer-events: auto; } + pointer-events: auto; +} + .dz-preview .dz-error-message { pointer-events: none; z-index: 1000; @@ -283,15 +360,17 @@ display: none; opacity: 0; transition: opacity 0.3s ease; - border-radius: 8px; - font-size: 13px; - top: 130px; - left: -10px; - width: 140px; - background: #be2626; - background: linear-gradient(to bottom, #be2626, #a92222); - padding: 0.5em 1.2em; - color: white; } + border-radius: 4px; + font-size: 11.5px; + line-height: 1.2; + top: 88px; + left: -26px; + width: 148px; + background: $negative; + padding: $-xs; + color: white; +} + .dz-preview .dz-error-message:after { content: ''; position: absolute; @@ -301,4 +380,5 @@ height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; - border-bottom: 6px solid #be2626; } + border-bottom: 6px solid $negative; +}