]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6972 improve loading UX of administration pages
authorStas Vilchik <vilchiks@gmail.com>
Mon, 16 Nov 2015 14:46:23 +0000 (15:46 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 16 Nov 2015 15:43:49 +0000 (16:43 +0100)
23 files changed:
server/sonar-web/src/main/js/api/components.js
server/sonar-web/src/main/js/apps/global-permissions/main.js
server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js
server/sonar-web/src/main/js/apps/groups/header-view.js
server/sonar-web/src/main/js/apps/groups/list-view.js
server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs
server/sonar-web/src/main/js/apps/permission-templates/header.js
server/sonar-web/src/main/js/apps/permission-templates/main.js
server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js
server/sonar-web/src/main/js/apps/project-permissions/main.js
server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js
server/sonar-web/src/main/js/apps/project-permissions/permissions.js
server/sonar-web/src/main/js/apps/projects/create-view.js
server/sonar-web/src/main/js/apps/projects/main.js
server/sonar-web/src/main/js/apps/projects/projects.js
server/sonar-web/src/main/js/apps/projects/search.js
server/sonar-web/src/main/js/apps/users/header-view.js
server/sonar-web/src/main/js/apps/users/list-view.js
server/sonar-web/src/main/js/apps/users/templates/users-header.hbs
server/sonar-web/src/main/js/components/shared/list-footer.js
server/sonar-web/src/main/js/helpers/request.js
server/sonar-web/src/main/less/components/page.less
server/sonar-web/src/main/less/init/misc.less

index a4eb949a2d7fa43a184f239b814e300ba17ed980..e4f60f8110b5cbfb87f8c0eaca18117aa30d5b46 100644 (file)
@@ -1,30 +1,29 @@
-import { getJSON } from '../helpers/request.js';
-import $ from 'jquery';
+import { getJSON, postJSON, post } from '../helpers/request.js';
+
 
 export function getComponents (data) {
   let url = baseUrl + '/api/components/search';
-  return $.get(url, data);
+  return getJSON(url, data);
 }
 
 export function getProvisioned (data) {
   let url = baseUrl + '/api/projects/provisioned';
-  return $.get(url, data);
+  return getJSON(url, data);
 }
 
 export function getGhosts (data) {
   let url = baseUrl + '/api/projects/ghosts';
-  return $.get(url, data);
+  return getJSON(url, data);
 }
 
 export function deleteComponents (data) {
   let url = baseUrl + '/api/projects/bulk_delete';
-  return $.post(url, data);
+  return post(url, data);
 }
 
-export function createProject (options) {
-  options.url = baseUrl + '/api/projects/create';
-  options.type = 'POST';
-  return $.ajax(options);
+export function createProject (data) {
+  let url = baseUrl + '/api/projects/create';
+  return postJSON(url, data);
 }
 
 export function getChildren (componentKey, metrics = []) {
index 11f6b032ebf44eb7b246c669738143885418b973..99efd69cbc26d3afbab8bde543fe3d81b8bd2a2e 100644 (file)
@@ -4,7 +4,7 @@ import PermissionsList from './permissions-list';
 
 export default React.createClass({
   getInitialState() {
-    return { permissions: [] };
+    return { ready: false, permissions: [] };
   },
 
   componentDidMount() {
@@ -14,18 +14,26 @@ export default React.createClass({
   requestPermissions() {
     const url = `${window.baseUrl}/api/permissions/search_global_permissions`;
     $.get(url).done(r => {
-      this.setState({ permissions: r.permissions });
+      this.setState({ ready: true, permissions: r.permissions });
     });
   },
 
+  renderSpinner () {
+    if (this.state.ready) {
+      return null;
+    }
+    return <i className="spinner"/>;
+  },
+
   render() {
     return (
         <div className="page">
           <header id="global-permissions-header" className="page-header">
             <h1 className="page-title">{window.t('global_permissions.page')}</h1>
+            {this.renderSpinner()}
             <p className="page-description">{window.t('global_permissions.page.description')}</p>
           </header>
-          <PermissionsList permissions={this.state.permissions}/>
+          <PermissionsList ready={this.state.ready} permissions={this.state.permissions}/>
         </div>
     );
   }
index e019fbcbfec1e3383af99c945937a4d8f122e3c6..c48769daab6d6d2b83604d769bd16c242cd57494 100644 (file)
@@ -1,6 +1,9 @@
+import classNames from 'classnames';
 import React from 'react';
+
 import Permission from './permission';
 
+
 export default React.createClass({
   propTypes:{
     permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
@@ -13,6 +16,7 @@ export default React.createClass({
   },
 
   render() {
-    return <ul id="global-permissions-list">{this.renderPermissions()}</ul>;
+    let className = classNames({ 'new-loading': !this.props.ready });
+    return <ul id="global-permissions-list" className={className}>{this.renderPermissions()}</ul>;
   }
 });
index e4a118f2822ed82d0a6e7745f5f3cbcc1a2f20af..3ac152c4e986aced51f02214e780d46ba6566019 100644 (file)
@@ -5,10 +5,23 @@ import Template from './templates/groups-header.hbs';
 export default Marionette.ItemView.extend({
   template: Template,
 
+  collectionEvents: {
+    'request': 'showSpinner',
+    'sync': 'hideSpinner'
+  },
+
   events: {
     'click #groups-create': 'onCreateClick'
   },
 
+  showSpinner: function () {
+    this.$('.spinner').removeClass('hidden');
+  },
+
+  hideSpinner: function () {
+    this.$('.spinner').addClass('hidden');
+  },
+
   onCreateClick: function (e) {
     e.preventDefault();
     this.createGroup();
index 699e9c76a855a1cb90b6bbd27ca0c2e9d8442d9d..22f699697e9466d153e1928bb8dc9dd553b92f70 100644 (file)
@@ -3,7 +3,20 @@ import ListItemView from './list-item-view';
 
 export default Marionette.CollectionView.extend({
   tagName: 'ul',
-  childView: ListItemView
+  childView: ListItemView,
+
+  collectionEvents: {
+    'request': 'showLoading',
+    'sync': 'hideLoading'
+  },
+
+  showLoading: function () {
+    this.$el.addClass('new-loading');
+  },
+
+  hideLoading: function () {
+    this.$el.removeClass('new-loading');
+  }
 });
 
 
index 19ba74febf86e112d6b4748d67107d50aae9e80e..94cf4a1ec34a6f9239cc045d5ec71076e82a4b4c 100644 (file)
@@ -1,5 +1,6 @@
 <header class="page-header">
   <h1 class="page-title">{{t 'user_groups.page'}}</h1>
+  <i class="spinner hidden"></i>
   <div class="page-actions">
     <div class="button-group">
       <button id="groups-create">Create Group</button>
index 0325d4bf6cb76d35c249fd78dd86dba29ef4cd62..eb367d830bfdf2a33d562bbd6733e52954174b92 100644 (file)
@@ -9,10 +9,18 @@ export default React.createClass({
     }).render();
   },
 
+  renderSpinner () {
+    if (this.props.ready) {
+      return null;
+    }
+    return <i className="spinner"/>;
+  },
+
   render() {
     return (
         <header id="project-permissions-header" className="page-header">
           <h1 className="page-title">{window.t('permission_templates.page')}</h1>
+          {this.renderSpinner()}
           <div className="page-actions">
             <button onClick={this.onCreate}>Create</button>
           </div>
index 1a0abfc8ead2e747c12b8ef8df0b5e20a0b9fc41..5032bef55475171b0654ce1792907f100f54bdd0 100644 (file)
@@ -12,7 +12,7 @@ export default React.createClass({
   },
 
   getInitialState() {
-    return { permissions: [], permissionTemplates: [] };
+    return { ready: false, permissions: [], permissionTemplates: [] };
   },
 
   componentDidMount() {
@@ -53,6 +53,7 @@ export default React.createClass({
       let permissionTemplates = this.mergePermissionsToTemplates(r.permissionTemplates, permissions);
       let permissionTemplatesWithDefaults = this.mergeDefaultsToTemplates(permissionTemplates, r.defaultTemplates);
       this.setState({
+        ready: true,
         permissionTemplates: permissionTemplatesWithDefaults,
         permissions: permissions
       });
@@ -62,10 +63,10 @@ export default React.createClass({
   render() {
     return (
         <div className="page">
-          <Header
-              refresh={this.requestPermissions}/>
+          <Header ready={this.state.ready} refresh={this.requestPermissions}/>
 
           <PermissionTemplates
+              ready={this.state.ready}
               permissionTemplates={this.state.permissionTemplates}
               permissions={this.state.permissions}
               topQualifiers={this.props.topQualifiers}
index a86379e256d05ef653e998ef2a68b7cf35465101..030fec04c2fad1d8e7f9fde21786c18df90e2523 100644 (file)
@@ -1,7 +1,10 @@
+import classNames from 'classnames';
 import React from 'react';
+
 import PermissionsHeader from './permissions-header';
 import PermissionTemplate from './permission-template';
 
+
 export default React.createClass({
   propTypes:{
     permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
@@ -18,8 +21,9 @@ export default React.createClass({
           topQualifiers={this.props.topQualifiers}
           refresh={this.props.refresh}/>;
     });
+    let className = classNames('data zebra', { 'new-loading': !this.props.ready });
     return (
-        <table id="permission-templates" className="data zebra">
+        <table id="permission-templates" className={className}>
           <PermissionsHeader permissions={this.props.permissions}/>
           <tbody>{permissionTemplates}</tbody>
         </table>
index 2c248f81344958bea277de218358f3580634ebb7..6dbc6a4c837d567909726c2e229ca947e80675e5 100644 (file)
@@ -14,7 +14,7 @@ export default React.createClass({
   },
 
   getInitialState() {
-    return { permissions: [], projects: [], total: 0 };
+    return { ready: false, permissions: [], projects: [], total: 0 };
   },
 
   componentDidMount() {
@@ -42,18 +42,21 @@ export default React.createClass({
     if (this.props.componentId) {
       data = { projectId: this.props.componentId };
     }
-    $.get(url, data).done(r => {
-      let permissions = this.sortPermissions(r.permissions);
-      let projects = this.mergePermissionsToProjects(r.projects, permissions);
-      if (page > 1) {
-        projects = [].concat(this.state.projects, projects);
-      }
-      this.setState({
-        projects: projects,
-        permissions: permissions,
-        total: r.paging.total,
-        page: r.paging.pageIndex,
-        query: query
+    this.setState({ ready: false }, () => {
+      $.get(url, data).done(r => {
+        let permissions = this.sortPermissions(r.permissions);
+        let projects = this.mergePermissionsToProjects(r.projects, permissions);
+        if (page > 1) {
+          projects = [].concat(this.state.projects, projects);
+        }
+        this.setState({
+          ready: true,
+          projects: projects,
+          permissions: permissions,
+          total: r.paging.total,
+          page: r.paging.pageIndex,
+          query: query
+        });
       });
     });
   },
@@ -88,11 +91,19 @@ export default React.createClass({
     );
   },
 
+  renderSpinner () {
+    if (this.state.ready) {
+      return null;
+    }
+    return <i className="spinner"/>;
+  },
+
   render() {
     return (
         <div className="page">
           <header id="project-permissions-header" className="page-header">
             <h1 className="page-title">{window.t('roles.page')}</h1>
+            {this.renderSpinner()}
             <div className="page-actions">
               {this.renderBulkApplyButton()}
             </div>
@@ -103,12 +114,14 @@ export default React.createClass({
               search={this.search}/>
 
           <Permissions
+              ready={this.state.ready}
               projects={this.state.projects}
               permissions={this.state.permissions}
               permissionTemplates={this.props.permissionTemplates}
               refresh={this.refresh}/>
 
           <PermissionsFooter {...this.props}
+              ready={this.state.ready}
               count={this.state.projects.length}
               total={this.state.total}
               loadMore={this.loadMore}/>
index cab1354e3ff4862b1961c6b917dae5da08d5e0fb..ad03541cbd6a2693bf003b8dbe5e973334082eb5 100644 (file)
@@ -1,5 +1,7 @@
+import classNames from 'classnames';
 import React from 'react';
 
+
 export default React.createClass({
   propTypes:{
     count: React.PropTypes.number.isRequired,
@@ -13,8 +15,9 @@ export default React.createClass({
     }
     let hasMore = this.props.total > this.props.count;
     let loadMoreLink = <a onClick={this.props.loadMore} className="spacer-left" href="#">show more</a>;
+    let className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready });
     return (
-        <footer className="spacer-top note text-center">
+        <footer className={className}>
           {this.props.count}/{this.props.total} shown
           {hasMore ? loadMoreLink : null}
         </footer>
index 26da7da40d67043392465bee6a8c151caf745a3a..c3ae66271a4692e88d0c6e4e8ecd9adfb7515548 100644 (file)
@@ -1,7 +1,10 @@
+import classNames from 'classnames';
 import React from 'react';
+
 import PermissionsHeader from './permissions-header';
 import Project from './project';
 
+
 export default React.createClass({
   propTypes:{
     projects: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
@@ -18,8 +21,9 @@ export default React.createClass({
           permissionTemplates={this.props.permissionTemplates}
           refresh={this.props.refresh}/>;
     });
+    let className = classNames('data zebra', { 'new-loading': !this.props.ready });
     return (
-        <table id="projects" className="data zebra">
+        <table id="projects" className={className}>
           <PermissionsHeader permissions={this.props.permissions}/>
           <tbody>{projects}</tbody>
         </table>
index 89425a212f82e60256cd00418a983da34631eb43..ce7b7d1f9f209854bbf34335fe102e673ffa2a35 100644 (file)
@@ -1,7 +1,8 @@
 import ModalForm from '../../components/common/modal-form';
-import {createProject} from '../../api/components';
+import { createProject } from '../../api/components';
 import Template from './templates/projects-create-form.hbs';
 
+
 export default ModalForm.extend({
   template: Template,
 
@@ -27,20 +28,18 @@ export default ModalForm.extend({
       key: this.$('#create-project-key').val()
     };
     this.disableForm();
-    return createProject({
-      data,
-      statusCode: {
-        // do not show global error
-        400: null
-      }
-    }).done(() => {
-      if (this.options.refresh) {
-        this.options.refresh();
-      }
-      this.destroy();
-    }).fail((jqXHR) => {
-      this.enableForm();
-      this.showErrors([{ msg: jqXHR.responseJSON.err_msg }]);
-    });
+    return createProject(data)
+        .then(() => {
+          if (this.options.refresh) {
+            this.options.refresh();
+          }
+          this.destroy();
+        })
+        .catch(error => {
+          this.enableForm();
+          if (error.response.status === 400) {
+            error.response.json().then(obj => this.showErrors([{ msg: obj.err_msg }]));
+          }
+        });
   }
 });
index 5db96f6ede9f766e217d99cc07872d184b21f2e1..051de7c515a37fd864f9c73c3bad03d9375ca922 100644 (file)
@@ -15,6 +15,7 @@ export default React.createClass({
 
   getInitialState() {
     return {
+      ready: false,
       projects: [],
       total: 0,
       page: 1,
@@ -62,48 +63,49 @@ export default React.createClass({
 
   requestGhosts() {
     let data = this.getFilters();
-    getGhosts(data).done(r => {
+    getGhosts(data).then(r => {
       let projects = r.projects.map(project => {
         return _.extend(project, { id: project.uuid, qualifier: 'TRK' });
       });
       if (this.state.page > 1) {
         projects = [].concat(this.state.projects, projects);
       }
-      this.setState({ projects: projects, total: r.total });
+      this.setState({ ready: true, projects: projects, total: r.total });
     });
   },
 
   requestProvisioned() {
     let data = this.getFilters();
-    getProvisioned(data).done(r => {
+    getProvisioned(data).then(r => {
       let projects = r.projects.map(project => {
         return _.extend(project, { id: project.uuid, qualifier: 'TRK' });
       });
       if (this.state.page > 1) {
         projects = [].concat(this.state.projects, projects);
       }
-      this.setState({ projects: projects, total: r.total });
+      this.setState({ ready: true, projects: projects, total: r.total });
     });
   },
 
   requestAllProjects() {
     let data = this.getFilters();
     data.qualifiers = this.state.qualifiers;
-    getComponents(data).done(r => {
+    getComponents(data).then(r => {
       let projects = r.components;
       if (this.state.page > 1) {
         projects = [].concat(this.state.projects, projects);
       }
-      this.setState({ projects: projects, total: r.paging.total });
+      this.setState({ ready: true, projects: projects, total: r.paging.total });
     });
   },
 
   loadMore() {
-    this.setState({ page: this.state.page + 1 }, this.requestProjects);
+    this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects);
   },
 
   onSearch(query) {
     this.setState({
+      ready: false,
       page: 1,
       query,
       selection: []
@@ -112,6 +114,7 @@ export default React.createClass({
 
   onTypeChanged(newType) {
     this.setState({
+      ready: false,
       page: 1,
       query: '',
       type: newType,
@@ -122,6 +125,7 @@ export default React.createClass({
 
   onQualifierChanged(newQualifier) {
     this.setState({
+      ready: false,
       page: 1,
       query: '',
       type: TYPE.ALL,
@@ -153,7 +157,7 @@ export default React.createClass({
 
   deleteProjects() {
     let ids = this.state.selection.join(',');
-    deleteComponents({ ids }).done(() => {
+    deleteComponents({ ids }).then(() => {
       this.setState({ page: 1, selection: [] }, this.requestProjects);
     });
   },
@@ -174,6 +178,7 @@ export default React.createClass({
               deleteProjects={this.deleteProjects}/>
 
           <Projects
+              ready={this.state.ready}
               projects={this.state.projects}
               refresh={this.requestProjects}
               selection={this.state.selection}
@@ -181,6 +186,7 @@ export default React.createClass({
               onProjectDeselected={this.onProjectDeselected}/>
 
           <ListFooter
+              ready={this.state.ready}
               count={this.state.projects.length}
               total={this.state.total}
               loadMore={this.loadMore}/>
index 1f3babcd067cf5427edf0d1406a77357649dbaf6..27432016944bcda4f1bb063611275a27b234e64c 100644 (file)
@@ -1,3 +1,4 @@
+import classNames from 'classnames';
 import React from 'react';
 import { getComponentUrl } from '../../helpers/urls';
 import Checkbox from '../../components/shared/checkbox';
@@ -43,8 +44,9 @@ export default React.createClass({
   },
 
   render() {
+    let className = classNames('data', 'zebra', { 'new-loading': !this.props.ready });
     return (
-        <table className="data zebra">
+        <table className={className}>
           <tbody>{this.props.projects.map(this.renderProject)}</tbody>
         </table>
     );
index a5cebad2a63604c0d977ae89eb3e8fd3650203c1..02b7229421a70f55ed6463e9cc3ca5eb889846d3 100644 (file)
@@ -47,6 +47,10 @@ export default React.createClass({
     return <Checkbox onCheck={this.onCheck} initiallyChecked={isChecked} thirdState={thirdState}/>;
   },
 
+  renderSpinner() {
+    return <i className="spinner"/>;
+  },
+
   onCheck(checked) {
     if (checked) {
       this.props.onAllSelected();
@@ -56,7 +60,7 @@ export default React.createClass({
   },
 
   renderGhostsDescription () {
-    if (this.props.type !== TYPE.GHOSTS) {
+    if (this.props.type !== TYPE.GHOSTS || !this.props.ready) {
       return null;
     }
     return <div className="spacer-top alert alert-info">{window.t('bulk_deletion.ghosts.description')}</div>;
@@ -89,7 +93,7 @@ export default React.createClass({
             <tbody>
             <tr>
               <td className="thin text-middle">
-                {this.renderCheckbox()}
+                {this.props.ready ? this.renderCheckbox() : this.renderSpinner()}
               </td>
               {this.renderQualifierFilter()}
               <td className="thin nowrap text-middle">
index 66e5df75b1a8455e397799a86dab5a416029e3d0..85140d2ef7a7255481e87baaf40a135f6c57061b 100644 (file)
@@ -5,10 +5,23 @@ import Template from './templates/users-header.hbs';
 export default Marionette.ItemView.extend({
   template: Template,
 
+  collectionEvents: {
+    'request': 'showSpinner',
+    'sync': 'hideSpinner'
+  },
+
   events: {
     'click #users-create': 'onCreateClick'
   },
 
+  showSpinner: function () {
+    this.$('.spinner').removeClass('hidden');
+  },
+
+  hideSpinner: function () {
+    this.$('.spinner').addClass('hidden');
+  },
+
   onCreateClick: function (e) {
     e.preventDefault();
     this.createUser();
index 699e9c76a855a1cb90b6bbd27ca0c2e9d8442d9d..22f699697e9466d153e1928bb8dc9dd553b92f70 100644 (file)
@@ -3,7 +3,20 @@ import ListItemView from './list-item-view';
 
 export default Marionette.CollectionView.extend({
   tagName: 'ul',
-  childView: ListItemView
+  childView: ListItemView,
+
+  collectionEvents: {
+    'request': 'showLoading',
+    'sync': 'hideLoading'
+  },
+
+  showLoading: function () {
+    this.$el.addClass('new-loading');
+  },
+
+  hideLoading: function () {
+    this.$el.removeClass('new-loading');
+  }
 });
 
 
index e35600392884ba02897732e80129c65247e45350..66dff8a39b62dd00b9e67ebf4c5caef3f5b559ab 100644 (file)
@@ -1,5 +1,6 @@
 <header class="page-header">
   <h1 class="page-title">{{t 'users.page'}}</h1>
+  <i class="spinner hidden"></i>
   <div class="page-actions">
     <div class="button-group">
       <button id="users-create">Create User</button>
index 31ba9e1f0d622416fec433a73cff500f4cddf65e..68922f39b2d573fc4b93d293fcfc2adc0a55eadc 100644 (file)
@@ -1,5 +1,7 @@
+import classNames from 'classnames';
 import React from 'react';
 
+
 export default React.createClass({
   propTypes: {
     count: React.PropTypes.number.isRequired,
@@ -11,18 +13,25 @@ export default React.createClass({
     return typeof this.props.loadMore === 'function';
   },
 
-  loadMoreProxy(e) {
+  handleLoadMore(e) {
     e.preventDefault();
     if (this.canLoadMore()) {
       this.props.loadMore();
     }
   },
 
+  renderLoading() {
+    return <footer className="spacer-top note text-center">
+      {window.t('loading')}
+    </footer>;
+  },
+
   render() {
     let hasMore = this.props.total > this.props.count,
-        loadMoreLink = <a onClick={this.loadMoreProxy} className="spacer-left" href="#">show more</a>;
+        loadMoreLink = <a onClick={this.handleLoadMore} className="spacer-left" href="#">show more</a>;
+    let className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready });
     return (
-        <footer className="spacer-top note text-center">
+        <footer className={className}>
           {this.props.count}/{this.props.total} shown
           {this.canLoadMore() && hasMore ? loadMoreLink : null}
         </footer>
index 86e9b8242e3273bd067c9a9992e88d8a2a3aa425..8cc20149f8b1e245f9fa86442d907887741d5973 100644 (file)
@@ -147,3 +147,13 @@ export function post (url, data) {
       .submit()
       .then(checkStatus);
 }
+
+
+/**
+ * Delay promise for testing purposes
+ * @param response
+ * @returns {Promise}
+ */
+export function delay (response) {
+  return new Promise(resolve => setTimeout(() => resolve(response), 3000));
+}
index bd752a1e9121a4c8f46ff70c635b3121348857bf..77d560efb237933cce35fbd61eb089bee37e9d54 100644 (file)
@@ -49,6 +49,12 @@ body {
 .page-header {
   .clearfix;
   margin-bottom: 10px;
+
+  .spinner {
+    position: relative;
+    top: 3px;
+    margin-left: 8px;
+  }
 }
 
 .page-title {
index 803bc92954cc7dd7ab8a4da1fbe9b4c88e8a0c9a..28ea70fb7a9a5abbb9c41b3a88dadfa668c7e9fc 100644 (file)
@@ -104,6 +104,11 @@ td.big-spacer-top    { padding-top: 16px; }
   justify-content: space-between !important;
 }
 
+.new-loading {
+  opacity: 0.5;
+  transition: opacity 0.5s ease;
+}
+
 
 // Background Color