diff --git a/.env b/.env index f8f280fbc..2488f7cd6 100644 --- a/.env +++ b/.env @@ -110,3 +110,5 @@ SENTRY_DSN= # postgresql+advisory://db_user:db_password@localhost/db_name LOCK_DSN=flock ###< symfony/lock ### + +MAIL_SENDER='info@netgen.io' diff --git a/Makefile b/Makefile index be3417ae8..41bcbfd86 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ APP_ENV = dev -PHP_VERSION = php8.1 +PHP_VERSION = php8.2 PHP_RUN = /usr/bin/env $(PHP_VERSION) COMPOSER_PATH = /usr/local/bin/composer2 ifeq ("$(wildcard $(COMPOSER_PATH))","") diff --git a/assets/js/backoffice/components.js b/assets/js/backoffice/components.js new file mode 100644 index 000000000..7fee568cb --- /dev/null +++ b/assets/js/backoffice/components.js @@ -0,0 +1,58 @@ +// This file contains only import, configuration and initialization of components +import addDocumentEventListeners from '../utilities/components/add-document-event-listeners'; + +import ModalConfirmControl from '../components/backoffice/ModalConfirmControl.component'; +import NgConversationsMarkThreadAsRead from '../components/backoffice/NgConversationsMarkThreadAsRead.component'; +import Sidebar from '../components/backoffice/Sidebar.component'; +import EnableFilterButtons from '../components/backoffice/EnableFilterButtons.component'; +import HideOverflowingFilters from '../components/backoffice/HideOverflowingFilters.component'; +import DateRangeInput from '../components/backoffice/DateRangeInput.component'; + +const componentConfiguration = [ + { + Component: NgConversationsMarkThreadAsRead, + selector: '#ngconversations-app-root', + options: { + menuItemSelector: '#menu_item_conversations', + menuItemCountClass: 'menu-ntf-count', + menuItemIconSelector: '.icon-conversations', + markAsReadEvent: 'ngconversations-change-thread-read-state', + }, + }, + { + Component: Sidebar, + selector: '.app-container', + options: { + hasSubmenu: '.has-submenu', + submenu: '.menu_level_1', + child: 'div:first-child', + currentItems: + '.app-sidebar .sidebar-wrapper .current, .app-sidebar .sidebar-wrapper .current_ancestor', + }, + }, + { + Component: ModalConfirmControl, + selector: '.js-modal-confirm', + }, + { + Component: EnableFilterButtons, + selector: '.js-filter-form', + options: { + hasActiveFilterClass: 'has-active-filter', + hasNewInputClass: 'has-new-input', + }, + }, + { + Component: HideOverflowingFilters, + selector: '.js-filter-wrapper', + options: { + filterWrapSelector: '.js-filters', + }, + }, + { + Component: DateRangeInput, + selector: '.js-date-range-input', + }, +]; + +addDocumentEventListeners(componentConfiguration); diff --git a/assets/js/backoffice/date.js b/assets/js/backoffice/date.js new file mode 100644 index 000000000..45aebec09 --- /dev/null +++ b/assets/js/backoffice/date.js @@ -0,0 +1,34 @@ +import flatpickr from 'flatpickr'; + +const date = document.querySelector('.js-date-filter'); + +if (date) { + flatpickr(date, { + mode: 'range', + minDate: '2022-01-01', + dateFormat: 'Y-m-d', + wrap: true, + + onChange(selectedDates) { + // eslint-disable-next-line no-empty + if (!selectedDates || selectedDates.length < 1) return; + + const dateFrom = new Date(selectedDates[0]); + dateFrom.setHours(0, 0, 0, 0); + + let dateTo = selectedDates[0]; + if (selectedDates.length > 1) { + // eslint-disable-next-line prefer-destructuring + dateTo = selectedDates[1]; + } + dateTo = new Date(dateTo); + dateTo.setHours(23, 59, 59, 0); + + const formattedDateFrom = dateFrom.toISOString().split('.')[0]; + const formattedDateTo = dateTo.toISOString().split('.')[0]; + + document.getElementById('dateFromHidden').value = formattedDateFrom; + document.getElementById('dateToHidden').value = formattedDateTo; + }, + }); +} diff --git a/assets/js/backoffice/index.js b/assets/js/backoffice/index.js new file mode 100644 index 000000000..e664d107a --- /dev/null +++ b/assets/js/backoffice/index.js @@ -0,0 +1,4 @@ +import '../../sass/backoffice/style.scss'; +import './components'; +import 'bootstrap'; +import './date'; diff --git a/assets/js/components-noncritical.js b/assets/js/components-noncritical.js index 821a6093d..36406064b 100644 --- a/assets/js/components-noncritical.js +++ b/assets/js/components-noncritical.js @@ -13,9 +13,11 @@ import PageHeader from './components/PageHeader.component'; import LoginFormFragment from './components/LoginFormFragment.component'; import ResponsiveVideo from './components/ResponsiveVideo.component'; import SwiperBase from './components/SwiperBase.component'; +import SubscriptionWidget from './components/SubscriptionWidget.component'; import SwiperThumb from './components/SwiperThumb.component'; import VideoPoster from './components/VideoPoster.component'; import SkipToMainContent from './components/SkipToMainContent.component'; +import BookmarkWidget from './components/BookmarkWidget.component'; // Configuration const componentConfiguration = [ @@ -214,6 +216,27 @@ const componentConfiguration = [ Component: SkipToMainContent, selector: '#skip-to-main-content', }, + { + Component: BookmarkWidget, + selector: '.js-bookmark-widget', + options: { + bookmarkIcon: '.fa-bookmark', + bookmarkTooltip: '.bookmark-tooltip', + }, + }, + { + Component: SubscriptionWidget, + selector: '#subscription-modal-body', + options: { + modalBody: '.modal-body', + subscriptionForm: '.ntf-subscribe-widget', + subscriptionTypeWrapper: '#subscription_widget_type', + subscriptionTypeRadios: 'input[type=radio]', + digestType: '#subscription_widget_type_1', + digestOptions: '.digest-options', + buttonSubscribe: 'button[type=submit]', + }, + }, ]; addDocumentEventListeners(componentConfiguration); diff --git a/assets/js/components/BookmarkWidget.component.js b/assets/js/components/BookmarkWidget.component.js new file mode 100644 index 000000000..8ee0ac345 --- /dev/null +++ b/assets/js/components/BookmarkWidget.component.js @@ -0,0 +1,40 @@ +export default class BookmarkWidget { + constructor(element, options) { + this.element = element; + this.bookmarkIcon = this.element.querySelector(options.bookmarkIcon); + this.bookmarkTooltip = this.element.querySelector(options.bookmarkTooltip); + this.createBookmarkEndpoint = element.dataset.createBookmarkEndpoint; + this.deleteBookmarkEndpoint = element.dataset.deleteBookmarkEndpoint; + this.isBookmarked = element.dataset.isBookmarked === 'true'; + + this.onInit(); + } + + onInit() { + console.log('bookmark component'); + + this.element.addEventListener('click', this.updateBookmarkData.bind(this)); + } + + updateBookmarkData() { + this.bookmarkTooltip.classList.remove('tooltip-animated'); + + if (this.isBookmarked) { + this.bookmarkTooltip.innerHTML = 'Bookmark removed'; + } else { + this.bookmarkTooltip.innerHTML = 'Bookmarked'; + } + + return fetch(this.isBookmarked ? this.deleteBookmarkEndpoint : this.createBookmarkEndpoint, { + method: 'POST', + }).then((response) => { + ['far', 'fas'].forEach((faTypeClass) => this.bookmarkIcon.classList.toggle(faTypeClass)); + + this.bookmarkTooltip.classList.add('tooltip-animated'); + + this.isBookmarked = !this.isBookmarked; + + return response; + }); + } +} diff --git a/assets/js/components/PageHeader.component.js b/assets/js/components/PageHeader.component.js index f274f5e4e..e541fff1f 100644 --- a/assets/js/components/PageHeader.component.js +++ b/assets/js/components/PageHeader.component.js @@ -100,7 +100,6 @@ export default class PageHeader { if (this.mainNav === null) { return; } - this.level1Menus = this.mainNav.querySelectorAll(this.options.menuLevel1); if (this.level1Menus.length === 0) { return; @@ -116,9 +115,19 @@ export default class PageHeader { this.submenuTriggerElements.push(submenuTriggerContent); }); - this.submenuTriggerElements.forEach((submenuTrigger) => { - submenuTrigger.addEventListener('click', () => { - this.toggleMobileSubmenu(submenuTrigger); + window.addEventListener('click', (e) => { + this.submenuTriggerElements.forEach((submenuTrigger) => { + const submenuTriggered = submenuTrigger.contains(e.target); + + if (e.target.closest('.prevent-user-menu-cta')) { + e.preventDefault(); + } + + if (!submenuTriggered || submenuTrigger.classList.contains('submenu-active')) { + submenuTrigger.classList.remove('submenu-active'); + } else { + submenuTrigger.classList.add('submenu-active'); + } }); }); } diff --git a/assets/js/components/SubscriptionWidget.component.js b/assets/js/components/SubscriptionWidget.component.js new file mode 100644 index 000000000..94dd66462 --- /dev/null +++ b/assets/js/components/SubscriptionWidget.component.js @@ -0,0 +1,74 @@ +export default class SubscriptionWidget { + constructor(element, options) { + this.el = element; + this.options = options; + this.modalBody = this.el.querySelector(options.modalBody); + this.subscriptionForm = this.el.querySelector(options.subscriptionForm); + this.digestType = this.el.querySelector(options.digestType); + this.digestOptions = this.el.querySelector(options.digestOptions); + this.buttonSubscribe = this.el.querySelector(options.buttonSubscribe); + + this.onInit(); + } + + onInit() { + if (this.digestType && this.digestType.checked) { + this.digestOptions.classList.remove('d-none'); + SubscriptionWidget.activateSubscribeButton(this.buttonSubscribe); + } + + this.el.addEventListener('click', (e) => { + const buttonSubscribe = e.currentTarget.querySelector(this.options.buttonSubscribe); + + if (e.target.matches(this.options.subscriptionTypeRadios)) { + this.changeType(e); + SubscriptionWidget.activateSubscribeButton(buttonSubscribe); + } + }); + + this.el.addEventListener('submit', (e) => { + if (e.target.matches(this.options.subscriptionForm)) { + const modalBody = e.currentTarget.querySelector(this.options.modalBody); + + e.preventDefault(); + this.formSubmit(modalBody, e.target); + } + }); + } + + changeType(e) { + const digestOptions = e.currentTarget.querySelector(this.options.digestOptions); + const digestType = e.currentTarget.querySelector(this.options.digestType); + + if (e.target === digestType || digestType.checked) { + digestOptions.classList.remove('d-none'); + } else { + digestOptions.classList.add('d-none'); + } + } + + static activateSubscribeButton(button) { + if (button) { + button.removeAttribute('disabled'); + button.classList.replace('btn-tertiary', 'btn-primary'); + } + } + + formSubmit(modalBody, subscriptionForm) { + const xhr = new XMLHttpRequest(); + xhr.open('POST', this.subscriptionForm.action); + xhr.onload = function (event) { + modalBody.innerHTML = event.target.response; + const errorMessage = modalBody.querySelector('.errors'); + const subscribeButton = modalBody.querySelector('button[type=submit]'); + + if (errorMessage && errorMessage.closest('.digest-options')) { + errorMessage.closest('.digest-options').classList.remove('d-none'); + } + + SubscriptionWidget.activateSubscribeButton(subscribeButton); + }; + const formData = new FormData(subscriptionForm); + xhr.send(formData); + } +} diff --git a/assets/js/components/backoffice/DateRangeInput.component.js b/assets/js/components/backoffice/DateRangeInput.component.js new file mode 100644 index 000000000..1150ba422 --- /dev/null +++ b/assets/js/components/backoffice/DateRangeInput.component.js @@ -0,0 +1,41 @@ +import flatpickr from 'flatpickr'; + +export default class Date { + constructor(element) { + this.element = element; + + this.init(); + } + + init() { + flatpickr(this.element, { + mode: 'range', + minDate: '2022-01-01', + dateFormat: 'Y-m-d', + wrap: true, + + onChange(selectedDates) { + if (!selectedDates || selectedDates.length < 1) { + return; + } + + const dateFrom = selectedDates[0]; + dateFrom.setHours(0, 0, 0, 0); + + let dateTo = selectedDates[0]; + if (selectedDates.length > 1) { + // eslint-disable-next-line prefer-destructuring + dateTo = selectedDates[1]; + } + + dateTo.setHours(23, 59, 59, 0); + + const formattedDateFrom = dateFrom.toISOString().split('.')[0]; + const formattedDateTo = dateTo.toISOString().split('.')[0]; + + document.getElementById('dateFromHidden').value = formattedDateFrom; + document.getElementById('dateToHidden').value = formattedDateTo; + }, + }); + } +} diff --git a/assets/js/components/backoffice/EnableFilterButtons.component.js b/assets/js/components/backoffice/EnableFilterButtons.component.js new file mode 100644 index 000000000..dc8afd846 --- /dev/null +++ b/assets/js/components/backoffice/EnableFilterButtons.component.js @@ -0,0 +1,30 @@ +export default class EnableFilterButtons { + constructor(element, options) { + this.form = element; + this.options = options; + this.onInit(); + } + + onInit() { + this.searchParams = new URLSearchParams(window.location.search); + this.inputs = this.form.querySelectorAll('input, select'); + + if ([...this.searchParams.keys()].some((query) => query !== 'page')) { + this.form.classList.add(this.options.hasActiveFilterClass); + } + + this.form.addEventListener('input', this.checkInputs.bind(this)); + + this.checkInputs(); + } + + checkInputs() { + const action = [...this.inputs].some( + ({ value, name }) => value && value !== this.searchParams.get(name) + ) + ? 'add' + : 'remove'; + + this.form.classList[action](this.options.hasNewInputClass); + } +} diff --git a/assets/js/components/backoffice/HideOverflowingFilters.component.js b/assets/js/components/backoffice/HideOverflowingFilters.component.js new file mode 100644 index 000000000..daa602c02 --- /dev/null +++ b/assets/js/components/backoffice/HideOverflowingFilters.component.js @@ -0,0 +1,47 @@ +const SEARCH_ELEMENT_MARGIN_RIGHT = 16; + +export default class HideOverflowingFilters { + constructor(element, options) { + this.filterWrapper = element; + this.options = options; + this.onInit(); + } + + onInit() { + this.filtersModal = this.filterWrapper.parentElement.querySelector('.dropdown-menu'); + this.filters = this.filterWrapper.querySelector(this.options.filterWrapSelector); + this.movedElements = []; + this.mostRightElement = [...this.filterWrapper.querySelector(':scope > div').children] + .reverse() + .find((element) => window.getComputedStyle(element).display !== 'none'); + this.handleWindowResize(); + + window.addEventListener('resize', this.handleWindowResize.bind(this)); + } + + handleWindowResize() { + const wrapperRect = this.filterWrapper.getBoundingClientRect(); + const mostRightElementRect = this.mostRightElement.getBoundingClientRect(); + + if (mostRightElementRect.right > wrapperRect.right || window.innerWidth < 576) { + // Move the last child of filter-wrapper to filters-modal and store window width at which it should be restored + const lastChild = this.filters.lastElementChild; + if (lastChild && this.filtersModal) { + this.movedElements.unshift({ + element: lastChild, + width: wrapperRect.right + SEARCH_ELEMENT_MARGIN_RIGHT, + }); + this.filtersModal.prepend(lastChild); + this.filterWrapper.classList.add('more-filters'); + } + + requestAnimationFrame(this.handleWindowResize.bind(this)); + } else if (this.movedElements.length && window.innerWidth > this.movedElements[0].width) { + this.filters.append(this.movedElements[0].element); + this.movedElements.shift(); + if (!this.movedElements.length) { + this.filterWrapper.classList.remove('more-filters'); + } + } + } +} diff --git a/assets/js/components/backoffice/ModalConfirmControl.component.js b/assets/js/components/backoffice/ModalConfirmControl.component.js new file mode 100644 index 000000000..59169c0e8 --- /dev/null +++ b/assets/js/components/backoffice/ModalConfirmControl.component.js @@ -0,0 +1,26 @@ +export default class ModalConfirmControl { + constructor(element) { + this.element = element; + this.modalElement = document.querySelector(element.dataset.modalElementSelector); + this.getFormUrl = element.dataset.getFormUrl; + + this.init(); + } + + init() { + const that = this; + + this.element.addEventListener('click', () => { + that.modalElement.innerHTML = + ''; + + that.fetchFormHTML().then((data) => { + that.modalElement.innerHTML = data; + }); + }); + } + + fetchFormHTML() { + return fetch(this.getFormUrl, { method: 'GET' }).then((response) => response.text()); + } +} diff --git a/assets/js/components/backoffice/NgConversationsMarkThreadAsRead.component.js b/assets/js/components/backoffice/NgConversationsMarkThreadAsRead.component.js new file mode 100644 index 000000000..9d9d71f49 --- /dev/null +++ b/assets/js/components/backoffice/NgConversationsMarkThreadAsRead.component.js @@ -0,0 +1,32 @@ +export default class NgConversationsMarkThreadAsRead { + constructor(element, options) { + this.element = element; + this.options = options; + + this.init(); + } + + init() { + const menuItem = document.querySelector(this.options.menuItemSelector); + + this.element.addEventListener(this.options.markAsReadEvent, ({ detail }) => { + let countElement = menuItem.querySelector(`.${this.options.menuItemCountClass}`); + + let count = Number(countElement?.innerText) || 0; + count += detail.isRead ? -1 : 1; + + if (count > 0) { + if (countElement === null) { + countElement = document.createElement('span'); + countElement.classList.add(this.options.menuItemCountClass); + const menuItemIcon = menuItem.querySelector(this.options.menuItemIconSelector); + menuItemIcon.parentNode.insertBefore(countElement, menuItemIcon); + } + + countElement.innerText = count; + } else { + countElement?.remove(); + } + }); + } +} diff --git a/assets/js/components/backoffice/Sidebar.component.js b/assets/js/components/backoffice/Sidebar.component.js new file mode 100644 index 000000000..4363aa250 --- /dev/null +++ b/assets/js/components/backoffice/Sidebar.component.js @@ -0,0 +1,71 @@ +export default class Sidebar { + constructor(element, options) { + this.element = element; + this.options = options; + + this.init(); + } + + init() { + const sidebarToggle = document.getElementById('toggleSidebar'); + if (sidebarToggle) { + sidebarToggle.onclick = () => { + this.element.classList.toggle('closed-sidebar'); + }; + } + + // Sidebar menu dropdowns + const menuItems = document.querySelectorAll(this.options.hasSubmenu); + menuItems.forEach((menuItem) => { + const submenu = menuItem.querySelector(this.options.submenu); + const child = menuItem.querySelector(this.options.child); + + child.addEventListener('click', (event) => { + event.preventDefault(); + submenu.classList.toggle('active'); + menuItem.classList.toggle('active'); + }); + }); + + const mobileNavToggle = document.getElementById('mobileNavToggle'); + + // Mobile menu toggle + mobileNavToggle.onclick = () => { + this.element.classList.toggle('closed-sidebar'); + }; + + // Sidebar account mini menu toggle and closing on outside click + const myAccountToggle = document.getElementById('myAccountToggle'); + const myAccountMenu = document.getElementById('myAccountMenu'); + + if (myAccountToggle && myAccountMenu) { + myAccountToggle.addEventListener('click', (e) => { + e.stopPropagation(); + myAccountMenu.classList.toggle('user-settings-menu--visible'); + }); + + document.addEventListener('click', (event) => { + const isClickInsideMenu = myAccountMenu.contains(event.target); + const isClickOnToggle = event.target === myAccountToggle; + + if (!isClickInsideMenu && !isClickOnToggle) { + myAccountMenu.classList.remove('user-settings-menu--visible'); + } + }); + } + + // Scroll sidebar to the selected item after the reload + const currentItems = document.querySelectorAll(this.options.currentItems); + + if (currentItems.length > 0) { + const currentItem = currentItems[0]; + currentItem.scrollIntoView(); + } + + // Detect Safari for specific CSS + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + document.querySelector('body').classList.add('sf-helper'); + } + + } +} diff --git a/assets/sass/_page_floating_widgets.scss b/assets/sass/_page_floating_widgets.scss new file mode 100644 index 000000000..e841795e1 --- /dev/null +++ b/assets/sass/_page_floating_widgets.scss @@ -0,0 +1,110 @@ +.page-floating-widgets { + position: fixed; + bottom: 40%; + right: 0; + display: flex; + flex-direction: column; + gap: 0.25rem; + color: $secondary_3; + z-index: 10; + + .subscription-widget { + background-color: $secondary_0; + writing-mode: vertical-rl; + transform: rotate(180deg); + width: 100%; + display: flex; + align-items: center; + } + + .subscription-icon { + transform: rotate(180deg); + + &::before { + content: url(../sass/assets/icons/icon_notification.svg); + } + } + + .subscriptions-widget-toggle { + display: flex; + justify-content: center; + gap: 8px; + padding: 1rem 0; + } + + .bookmark-widget { + position: relative; + + .bookmark-tooltip { + position: absolute; + right: calc(100% + 0.5625rem); + background-color: $secondary_1; + border-radius: 0.25rem; + padding: 0.125rem 0.75rem; + opacity: 0; + pointer-events: none; + visibility: hidden; + font-size: 0.875rem; + + &.tooltip-animated { + animation: tooltipAppear 3s ease forwards; + } + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 100%; + transform: translateY(-50%); + border: 6px solid transparent; + border-left: 6px solid $secondary_1; + } + } + } +} + +@keyframes tooltipAppear { + 0% { + opacity: 0; + pointer-events: none; + visibility: hidden; + } + 25% { + opacity: 1; + pointer-events: all; + visibility: visible; + } + 75% { + opacity: 1; + pointer-events: all; + visibility: visible; + } + 100% { + opacity: 0; + pointer-events: none; + visibility: hidden; + } +} + +#subscriptionModal { + .modal-dialog { + max-width: 30rem; + } + + .modal-content { + border-radius: 0.25rem; + } + + .modal-header { + padding: 1.5rem 1.5rem 1rem; + } + + .modal-body { + padding: 1rem 1.5rem 1.5rem; + } + + .modal-footer { + border-top: none; + padding: 0 1.5rem 1.5rem; + } +} diff --git a/assets/sass/_subscription_widget.scss b/assets/sass/_subscription_widget.scss new file mode 100644 index 000000000..01e686286 --- /dev/null +++ b/assets/sass/_subscription_widget.scss @@ -0,0 +1,114 @@ +#subscription-modal-body { + + .modal-header { + align-items: flex-start; + + .modal-title { + margin-right: 1rem; + @include text-1_125; + font-weight: 700; + } + + .btn-close { + margin: 0; + } + } + + .modal-body { + p { + margin: 0; + } + + a { + font-weight: 600; + color: $secondary_4; + border-bottom: 1px solid $secondary_4; + } + } + + .ntf-subscribe-widget { + #subscription_widget_type { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1rem; + + & > div { + display: flex; + align-items: center; + } + } + + .type-options { + label { + display: none; + } + + input { + height: 1rem; + width: 1rem; + accent-color: $secondary_4; + + & + label { + padding-left: 0.5rem; + display: inline-block; + } + } + } + + .digest-options { + display: flex; + flex-direction: column; + gap: 1rem; + margin: 0 0 1rem 1.5rem; + + label { + display: none; + } + + .form-select { + @include text-1_125; + font-weight: 600; + } + } + + button[type=submit] { + width: 100%; + } + + small { + display: inline-block; + text-align: center; + color: rgba(37, 45, 64, 0.66); + padding: 0 3rem; + margin-top: 0.5rem; + @include text-0_875; + + @include media-breakpoint-down(sm) { + padding: 0; + } + } + } + + .subscription-success { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + + .success-icon { + width: 3rem; + height: 3rem; + display: flex; + justify-content: center; + align-items: center; + background-color: $success-green; + border-radius: 50%; + margin-bottom: 1.5rem; + + &::before { + content: url(../sass/assets/icons/icon_check.svg); + } + } + } +} diff --git a/assets/sass/_user_impersonation.scss b/assets/sass/_user_impersonation.scss new file mode 100644 index 000000000..c52d3cee4 --- /dev/null +++ b/assets/sass/_user_impersonation.scss @@ -0,0 +1,44 @@ +.user-impersonation { + background-color: $primary_0; + padding: 0.5rem 1rem; + height: $impersonation-bar-height; + display: flex; + align-items: center; + justify-content: space-between; + + .user-impersonation-label { + display: flex; + align-items: center; + margin-bottom: 0; + gap: 0.25rem; + @include text-0_875; + + &::before { + content: url(../../assets/sass/assets/icons/icon_info.svg); + line-height: 0; + } + + .impersonating-label { + @include media-breakpoint-down(sm) { + text-transform: capitalize; + } + } + + .user-name { + font-weight: 600; + } + } + + .btn-end-process { + @include text-0_75; + font-weight: 600; + gap: 0.25rem; + + &.btn:not(.btn.btn-link) { + @include media-breakpoint-down(sm) { + width: fit-content; + } + } + } +} + diff --git a/assets/sass/_variables.scss b/assets/sass/_variables.scss index 62d02eb5c..64a53d7a5 100644 --- a/assets/sass/_variables.scss +++ b/assets/sass/_variables.scss @@ -3,10 +3,19 @@ $header-height: 5.9375rem; $header-height-sm: 4rem; $gutter: .9375rem; $gap: 2 * $gutter; +$impersonation-bar-height: 3rem; // colours $primary: #FED82F; +$primary_0: #FFCD00; +$primary_1: #346094; +$primary_2: #BBC6D6; $secondary: #F8F9FC; +$secondary_0: #00396F; +$secondary_1: #252D40; +$secondary_2: #F3F3F3; +$secondary_3: #F9F1DE; +$secondary_4: #A31F34; $white: hsl(0, 0, 100); $black: hsl(0, 0, 0); $gray-87: hsl(0, 0, 13); @@ -17,6 +26,9 @@ $gray-19: hsl(0, 0, 81); $gray-12: hsl(0, 0, 88); $gray-7: hsl(0, 0, 93); $outline-color: #4D90FE; +$success-green: #4BC17C; +$darkgray: rgba(37, 45, 64, 0.66); + $body-bg: $white; $footer-bg: $black; @@ -27,4 +39,5 @@ $filtered-colours: ( secondary: brightness(0) saturate(100%) invert(100%) sepia(26%) saturate(567%) hue-rotate(178deg) brightness(100%) contrast(98%), white: brightness(0) saturate(100%) invert(92%) sepia(93%) saturate(32%) hue-rotate(251deg) brightness(107%) contrast(100%), black: brightness(0) saturate(100%) invert(0%) sepia(9%) saturate(7500%) hue-rotate(154deg) brightness(91%) contrast(108%), + success-green: brightness(0) saturate(100%) invert(31%) sepia(10%) saturate(2823%) hue-rotate(92deg) brightness(95%) contrast(83%), ); diff --git a/assets/sass/assets/icons/icon_alert.svg b/assets/sass/assets/icons/icon_alert.svg new file mode 100644 index 000000000..4cbf2ab54 --- /dev/null +++ b/assets/sass/assets/icons/icon_alert.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_arrow_down.svg b/assets/sass/assets/icons/icon_arrow_down.svg new file mode 100644 index 000000000..2410d9579 --- /dev/null +++ b/assets/sass/assets/icons/icon_arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_arrow_left.svg b/assets/sass/assets/icons/icon_arrow_left.svg new file mode 100644 index 000000000..3a8739fe7 --- /dev/null +++ b/assets/sass/assets/icons/icon_arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_arrow_right.svg b/assets/sass/assets/icons/icon_arrow_right.svg new file mode 100644 index 000000000..6e11db2fc --- /dev/null +++ b/assets/sass/assets/icons/icon_arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_arrow_right_small.svg b/assets/sass/assets/icons/icon_arrow_right_small.svg new file mode 100644 index 000000000..59a559b72 --- /dev/null +++ b/assets/sass/assets/icons/icon_arrow_right_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_arrow_white.svg b/assets/sass/assets/icons/icon_arrow_white.svg new file mode 100644 index 000000000..e437100a0 --- /dev/null +++ b/assets/sass/assets/icons/icon_arrow_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_bookmarks.svg b/assets/sass/assets/icons/icon_bookmarks.svg new file mode 100644 index 000000000..eee27c72b --- /dev/null +++ b/assets/sass/assets/icons/icon_bookmarks.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_caret_right.svg b/assets/sass/assets/icons/icon_caret_right.svg new file mode 100644 index 000000000..52ce92c88 --- /dev/null +++ b/assets/sass/assets/icons/icon_caret_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_chat.svg b/assets/sass/assets/icons/icon_chat.svg new file mode 100644 index 000000000..a12760825 --- /dev/null +++ b/assets/sass/assets/icons/icon_chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_check.svg b/assets/sass/assets/icons/icon_check.svg new file mode 100644 index 000000000..cc4de9b02 --- /dev/null +++ b/assets/sass/assets/icons/icon_check.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_check_white.svg b/assets/sass/assets/icons/icon_check_white.svg new file mode 100644 index 000000000..7fb8d94f8 --- /dev/null +++ b/assets/sass/assets/icons/icon_check_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_chevron.svg b/assets/sass/assets/icons/icon_chevron.svg new file mode 100644 index 000000000..a478f2b1f --- /dev/null +++ b/assets/sass/assets/icons/icon_chevron.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_close.svg b/assets/sass/assets/icons/icon_close.svg new file mode 100644 index 000000000..548a0db66 --- /dev/null +++ b/assets/sass/assets/icons/icon_close.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_close_small.svg b/assets/sass/assets/icons/icon_close_small.svg new file mode 100644 index 000000000..97c0cc0b8 --- /dev/null +++ b/assets/sass/assets/icons/icon_close_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_conversations.svg b/assets/sass/assets/icons/icon_conversations.svg new file mode 100644 index 000000000..f62977051 --- /dev/null +++ b/assets/sass/assets/icons/icon_conversations.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_copy.svg b/assets/sass/assets/icons/icon_copy.svg new file mode 100644 index 000000000..b18e32252 --- /dev/null +++ b/assets/sass/assets/icons/icon_copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sass/assets/icons/icon_dashboard.svg b/assets/sass/assets/icons/icon_dashboard.svg new file mode 100644 index 000000000..0bd80331f --- /dev/null +++ b/assets/sass/assets/icons/icon_dashboard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/sass/assets/icons/icon_date.svg b/assets/sass/assets/icons/icon_date.svg new file mode 100644 index 000000000..ebaf2b3a6 --- /dev/null +++ b/assets/sass/assets/icons/icon_date.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_delete.svg b/assets/sass/assets/icons/icon_delete.svg new file mode 100644 index 000000000..13dbc20e1 --- /dev/null +++ b/assets/sass/assets/icons/icon_delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_docs.svg b/assets/sass/assets/icons/icon_docs.svg new file mode 100644 index 000000000..d224169e4 --- /dev/null +++ b/assets/sass/assets/icons/icon_docs.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_docx.svg b/assets/sass/assets/icons/icon_docx.svg new file mode 100644 index 000000000..222260bb0 --- /dev/null +++ b/assets/sass/assets/icons/icon_docx.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/sass/assets/icons/icon_dots.svg b/assets/sass/assets/icons/icon_dots.svg new file mode 100644 index 000000000..16a11bf08 --- /dev/null +++ b/assets/sass/assets/icons/icon_dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_download.svg b/assets/sass/assets/icons/icon_download.svg new file mode 100644 index 000000000..8cb9434a9 --- /dev/null +++ b/assets/sass/assets/icons/icon_download.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_edit.svg b/assets/sass/assets/icons/icon_edit.svg new file mode 100644 index 000000000..735b20865 --- /dev/null +++ b/assets/sass/assets/icons/icon_edit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_exteral_dark.svg b/assets/sass/assets/icons/icon_exteral_dark.svg new file mode 100644 index 000000000..9a84b51d4 --- /dev/null +++ b/assets/sass/assets/icons/icon_exteral_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_external.svg b/assets/sass/assets/icons/icon_external.svg new file mode 100644 index 000000000..8bbb3b65b --- /dev/null +++ b/assets/sass/assets/icons/icon_external.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_filter.svg b/assets/sass/assets/icons/icon_filter.svg new file mode 100644 index 000000000..f50f53560 --- /dev/null +++ b/assets/sass/assets/icons/icon_filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_filter_red.svg b/assets/sass/assets/icons/icon_filter_red.svg new file mode 100644 index 000000000..15b4e6170 --- /dev/null +++ b/assets/sass/assets/icons/icon_filter_red.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_filter_small.svg b/assets/sass/assets/icons/icon_filter_small.svg new file mode 100644 index 000000000..648f2b9ab --- /dev/null +++ b/assets/sass/assets/icons/icon_filter_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_info.svg b/assets/sass/assets/icons/icon_info.svg new file mode 100644 index 000000000..efe1785b9 --- /dev/null +++ b/assets/sass/assets/icons/icon_info.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_list.svg b/assets/sass/assets/icons/icon_list.svg new file mode 100644 index 000000000..51f9da9c0 --- /dev/null +++ b/assets/sass/assets/icons/icon_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_lock.svg b/assets/sass/assets/icons/icon_lock.svg new file mode 100644 index 000000000..8e5783a11 --- /dev/null +++ b/assets/sass/assets/icons/icon_lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_logout.svg b/assets/sass/assets/icons/icon_logout.svg new file mode 100644 index 000000000..7afbd2206 --- /dev/null +++ b/assets/sass/assets/icons/icon_logout.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/sass/assets/icons/icon_next.svg b/assets/sass/assets/icons/icon_next.svg new file mode 100644 index 000000000..6e11db2fc --- /dev/null +++ b/assets/sass/assets/icons/icon_next.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_note_info.svg b/assets/sass/assets/icons/icon_note_info.svg new file mode 100644 index 000000000..396ba39ad --- /dev/null +++ b/assets/sass/assets/icons/icon_note_info.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_notification.svg b/assets/sass/assets/icons/icon_notification.svg new file mode 100644 index 000000000..0aed5c81e --- /dev/null +++ b/assets/sass/assets/icons/icon_notification.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_notifications_dark.svg b/assets/sass/assets/icons/icon_notifications_dark.svg new file mode 100644 index 000000000..843d134c3 --- /dev/null +++ b/assets/sass/assets/icons/icon_notifications_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_overview.svg b/assets/sass/assets/icons/icon_overview.svg new file mode 100644 index 000000000..2dea5a0bb --- /dev/null +++ b/assets/sass/assets/icons/icon_overview.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_pdf.svg b/assets/sass/assets/icons/icon_pdf.svg new file mode 100644 index 000000000..cd9d9b65e --- /dev/null +++ b/assets/sass/assets/icons/icon_pdf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/sass/assets/icons/icon_plus.svg b/assets/sass/assets/icons/icon_plus.svg new file mode 100644 index 000000000..1a86f5ec4 --- /dev/null +++ b/assets/sass/assets/icons/icon_plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_print.svg b/assets/sass/assets/icons/icon_print.svg new file mode 100644 index 000000000..8805c5b2d --- /dev/null +++ b/assets/sass/assets/icons/icon_print.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_save.svg b/assets/sass/assets/icons/icon_save.svg new file mode 100644 index 000000000..9dc33fa71 --- /dev/null +++ b/assets/sass/assets/icons/icon_save.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_search.svg b/assets/sass/assets/icons/icon_search.svg new file mode 100644 index 000000000..b8d18d421 --- /dev/null +++ b/assets/sass/assets/icons/icon_search.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_star.svg b/assets/sass/assets/icons/icon_star.svg new file mode 100644 index 000000000..c211b223a --- /dev/null +++ b/assets/sass/assets/icons/icon_star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_star_filled.svg b/assets/sass/assets/icons/icon_star_filled.svg new file mode 100644 index 000000000..5ba2d330d --- /dev/null +++ b/assets/sass/assets/icons/icon_star_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_trash.svg b/assets/sass/assets/icons/icon_trash.svg new file mode 100644 index 000000000..5028b4b57 --- /dev/null +++ b/assets/sass/assets/icons/icon_trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sass/assets/icons/icon_user.svg b/assets/sass/assets/icons/icon_user.svg new file mode 100644 index 000000000..54aba15d1 --- /dev/null +++ b/assets/sass/assets/icons/icon_user.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/sass/assets/icons/icon_user_cancel.svg b/assets/sass/assets/icons/icon_user_cancel.svg new file mode 100644 index 000000000..f28f02723 --- /dev/null +++ b/assets/sass/assets/icons/icon_user_cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sass/assets/icons/icon_user_check.svg b/assets/sass/assets/icons/icon_user_check.svg new file mode 100644 index 000000000..adca2547f --- /dev/null +++ b/assets/sass/assets/icons/icon_user_check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sass/assets/icons/icon_user_group.svg b/assets/sass/assets/icons/icon_user_group.svg new file mode 100644 index 000000000..db0ccd01d --- /dev/null +++ b/assets/sass/assets/icons/icon_user_group.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_view.svg b/assets/sass/assets/icons/icon_view.svg new file mode 100644 index 000000000..65b03b469 --- /dev/null +++ b/assets/sass/assets/icons/icon_view.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/icon_warning.svg b/assets/sass/assets/icons/icon_warning.svg new file mode 100644 index 000000000..a7f9a2014 --- /dev/null +++ b/assets/sass/assets/icons/icon_warning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/sass/assets/icons/x-twitter.svg b/assets/sass/assets/icons/x-twitter.svg new file mode 100644 index 000000000..f5feed78f --- /dev/null +++ b/assets/sass/assets/icons/x-twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sass/backoffice/_buttons.scss b/assets/sass/backoffice/_buttons.scss new file mode 100644 index 000000000..885be0529 --- /dev/null +++ b/assets/sass/backoffice/_buttons.scss @@ -0,0 +1,280 @@ +%btn-icon { + content: ''; + width: 1rem; + height: 1rem; + display: inline-block; + margin: 0; + + &.btn-icon-plus { + @extend %icon-plus; + } + + &.btn-icon-arrow-left { + width: 1.5rem; + height: 1.5rem; + @extend %icon-arrow-white; + transform: rotate(180deg); + } + + &.btn-icon-arrow-right { + width: 1.5rem; + height: 1.5rem; + @extend %icon-arrow-white; + } + + &.btn-icon-download { + @extend %icon-download; + } + + &.btn-icon-edit { + width: 1.5rem; + height: 1.5rem; + @extend %icon-edit; + } + + &.btn-icon-check { + @extend %icon-check; + } + + &.btn-icon-close { + @extend %icon-close; + } + + &.btn-icon-save { + width: 1.5rem; + height: 1.5rem; + @extend %icon-save; + } + + &.btn-icon-conversations { + width: 1.5rem; + height: 1.5rem; + @extend %icon-conversations; + } + + &.btn-icon-user-group { + width: 1.5rem; + height: 1.5rem; + @extend %icon-user-group; + } + +} + +.btn { + font-size: 0.75rem; + text-transform: uppercase; + height: 40px; + padding: 0.35rem 1.75rem 0.35rem; + display: inline-flex; + width: fit-content; + justify-content: center; + align-items: center; + + &.btn-primary { + background-color: $secondary_4; + border: 2px solid $secondary_4; + color: $secondary_3; + + &:hover { + background-color: darken($secondary_4, 6%); + border-color: darken($secondary_4, 6%); + color: $secondary_3; + } + + &.hidden { + visibility: hidden; + } + } + + &.btn-secondary { + background: $white; + border: 2px solid $border; + color: $secondary_1; + + &:hover { + background-color: $light-bg; + border-color: $border; + color: $secondary_1; + } + } + + &.btn-ghost { + background: transparent; + border: 2px solid transparent; + color: $secondary_4; + + &:hover { + background-color: rgba($secondary_1, 0.04); + border-color: rgba($secondary_1, 0.04); + color: $secondary_4; + } + } + + &.btn-accepted { + color: $olive; + background-color: $accept; + + &.hidden { + visibility: hidden; + } + } + + &.btn-link { + border-radius: 0; + padding: 0; + text-transform: unset; + font-size: 0.875rem; + border-bottom: 2px solid $border; + color: $secondary_4; + white-space: nowrap; + height: auto; + + &.btn-icon-left, + &.btn-icon-right { + padding: 0; + } + + &:hover { + color: $secondary_4; + border-color: $secondary_4; + background: transparent; + } + + &.btn-link-plain { + border-bottom: none; + + &.icon-view { + &::after { + content: ''; + width: 1.5rem; + height: 1.5rem; + display: inline-block; + @extend %icon-view; + } + } + } + + &.btn-link-list { + border-bottom: none; + font-weight: 400; + text-decoration: underline; + } + + &.btn-margin { + margin: 0 0.5rem; + } + } + + &.btn-dropdown { + text-transform: unset; + font-size: 0.875rem; + color: $secondary_4; + white-space: nowrap; + padding: 0.35rem 1rem 0.35rem 1.5rem; + + &:hover { + background-color: rgba($secondary_1, 0.04); + } + } + + &.btn-confirm { + color: $olive; + background-color: $accept; + height: 2rem; + + &.btn-icon-left, + &.btn-icon-right { + padding: 0.5rem 0.75rem; + } + } + + &.btn-decline { + padding: 0.5rem 0.75rem; + color: $olive-dark; + background-color: $decline; + + &::before { + content: ''; + width: 1rem; + height: 1rem; + @extend %icon-x; + margin-right: 0.25rem; + } + } + + &.btn-icon-left, + &.btn-icon-right { + display: inline-flex; + gap: 8px; + + &.btn-primary { + &::after, + &::before { + @include icon-color(secondary_3); + } + } + + &.btn-secondary { + &::after, + &::before { + @include icon-color(secondary_1); + } + } + + &.btn-ghost { + &::after, + &::before { + @include icon-color(secondary_4); + } + } + + &.btn-confirm { + &::after, + &::before { + @include icon-color(success-green); + } + } + + &.btn-decline { + &::after, + &::before { + @include icon-color(secondary_4); + } + } + } + + &.btn-icon-left { + padding-left: 1.5rem; + padding-right: 2rem; + + &::before { + @extend %btn-icon; + } + + &::after { + content: unset; + } + } + + &.btn-icon-right { + padding-left: 2rem; + padding-right: 1.5rem; + + &::before { + content: unset; + } + + &::after { + @extend %btn-icon; + } + } + + &.btn-icon { + border: none; + background: none; + + &::after { + @extend %btn-icon; + } + } +} diff --git a/assets/sass/backoffice/_dashboard.scss b/assets/sass/backoffice/_dashboard.scss new file mode 100644 index 000000000..3ae77615c --- /dev/null +++ b/assets/sass/backoffice/_dashboard.scss @@ -0,0 +1,145 @@ +.startpage-wrapper { + padding: 1rem; + + h5 { + font-size: 1.125rem; + margin: 1.5rem 0 1rem; + font-weight: 600; + } +} +.startpage-shortcuts { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + + @include media-breakpoint-down(lg) { + display: block; + } + + &-item { + padding: 1.5rem; + background-color: $light-bg; + border-radius: 8px; + + @include media-breakpoint-down(lg) { + margin-bottom: 8px; + } + + h6 { + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 8px; + } + + p { + font-size: 0.875rem; + margin-bottom: 2.5rem; + color: rgba($secondary_1, .66); + } + + ul { + list-style: none; + padding: 0; + + li { + a { + color: $secondary_4; + text-decoration: underline; + font-size: 0.875rem; + } + } + } + + &.startpage-shortcuts-item-full-width { + grid-column: 1 / 4; + } + + .startpage-shortcuts-lists { + display: flex; + gap: 3.5rem; + > ul { + flex: 0 0 calc(33.333333% - 2.375rem); + } + } + } +} + +.welcome-message, +.user-info-message { + background-color: rgba($message, .2); + padding: 1rem 1rem 1.5rem 1rem; + border-radius: 8px; + margin-bottom: 1.5rem; + position: relative; + + @include media-breakpoint-down(md) { + min-width: calc(100vw - 110px); + } + + i::before { + content: ""; + width: 1rem; + height: 16px; + background-repeat: no-repeat; + position: absolute; + top: 1.125rem; + left: 1.125rem; + border-radius: 50%; + } + + i.icon-info::before { + @extend %icon-info; + } + + a.btn-close { + position: absolute; + right: 0; + top: 0.125rem; + outline: none; + border: none; + font-size: 0.625rem; + color: $black; + opacity: 1; + + @include media-breakpoint-down(xs) { + &.btn:not(.btn.btn-link) { + width: auto; + } + } + } + + &-content { + padding: 0 2rem; + + p { + font-size: 0.875rem; + margin: 0.625rem 0 0; + } + } +} + +.user-info-message { + &-content { + a, span { + font-weight: 600; + } + + a { + color: $secondary_4; + } + } +} + +.startpage-set-interests { + display: block; + color: $secondary-4; + text-decoration: underline; + font-size: 1rem; + margin-bottom: 1rem; + + &:hover { + color: $secondary-4; + opacity: .9; + text-decoration: underline; + } +} \ No newline at end of file diff --git a/assets/sass/backoffice/_filter_form.scss b/assets/sass/backoffice/_filter_form.scss new file mode 100644 index 000000000..e68debf10 --- /dev/null +++ b/assets/sass/backoffice/_filter_form.scss @@ -0,0 +1,320 @@ +.filter-form { + .ntf-filter-search { + padding: 1rem; + border-bottom: 4px solid $secondary_2; + + @include media-breakpoint-down(sm) { + display: block; + } + + .ntf-filter-search-wrap { + display: flex; + gap: 16px; + + @include media-breakpoint-down(sm) { + flex-wrap: wrap; + column-gap: 8px; + } + + div .input-group [type='search'] { + @include media-breakpoint-up(md) { + max-width: 224px; + } + } + } + + &.more-filters .dropdown-menu { + select.form-select, + .date-wrapper, + .filter-by-date, + input { + width: 100%; + } + } + + &.ntf-filter-search-wrap-grid { + .ss-main.form-select { + background-image: none; + + .ss-single { + color: $placeholder; + } + } + + div > select.form-select { + width: 235px; + } + } + + .search-wrap { + align-self: flex-end; + + @include media-breakpoint-down(sm) { + flex: 1; + } + } + + .dropdown { + align-self: flex-end; + + .dropdown-menu { + border: 2px solid $secondary_2; + border-radius: 4px; + padding: 1rem; + box-shadow: 0px 4px 12px 0px rgba(37, 45, 64, 0.1); + display: none; + flex-direction: column; + gap: 16px; + top: 0.25rem; + + &.show { + display: flex; + } + } + } + + .ntf-filter-wrap { + display: flex; + align-items: flex-end; + gap: 16px; + margin-right: -1rem; + + @include media-breakpoint-down(md) { + > div { + width: 100%; + } + } + + select { + color: $placeholder; + @extend %icon-chevron; + background-size: 1.5rem; + background-position: 98%; + position: relative; + background-color: $light-bg; + } + } + + .date-wrapper { + margin-right: auto; + } + + label { + font-size: 0.875rem; + display: block; + } + + .form-hidden { + display: none; + } + + .form-select { + height: 40px; + font-size: 0.875rem; + width: 220px; + border-radius: 4px; + padding: 0 0 0 0.75rem; + } + + input { + height: 40px; + border-radius: 4px; + background-color: $light-bg; + border: 2px solid $border; + font-size: 0.875rem; + + @include media-breakpoint-down(md) { + width: 100%; + } + } + + select { + background-color: $light-bg; + } + + input[type='search'] { + border-radius: 100px; + padding-left: 3rem; + min-width: 200px; + + @include media-breakpoint-up(md) { + max-width: 204px; + } + } + + .input-group { + @include media-breakpoint-up(md) { + flex-wrap: nowrap; + } + } + + .btn-apply-filters { + white-space: nowrap; + align-self: flex-end; + width: auto; + color: $secondary_4; + border-color: $secondary_4; + + @include media-breakpoint-down(md) { + padding: 0.5rem 1.5rem 0.5rem 1.5rem; + } + + &:active { + background: $red; + border-color: $red; + color: $white; + } + } + + .btn-reset-filters { + height: fit-content; + display: none; + align-self: flex-end; + margin: 0.5938rem 0; + } + + .more-filters-btn { + align-items: center; + border-color: transparent; + display: none; + align-self: flex-end; + + @include media-breakpoint-down(md) { + padding: 0.125rem 0.5rem 0.125rem 1rem; + } + + &::after { + content: unset; + } + + span { + display: inline-block; + line-height: 1.4; + } + + i { + display: inline-flex; + + &::after { + content: ''; + width: 1rem; + height: 1rem; + } + + &.icon-filter::after { + @extend %icon-filter; + } + } + + &.show { + background-color: rgba($secondary_1, 0.04); + } + } + + &.more-filters .more-filters-btn { + display: flex; + } + + .input-group.input-search { + position: relative; + &::before { + content: ''; + @extend %icon-search; + width: 1.5rem; + height: 1.5rem; + position: absolute; + margin: 0; + left: 1rem; + top: 0.5rem; + z-index: 10; + } + + .btn { + border: 2px solid $border; + } + } + + .input-group:not(.has-validation) + > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) { + @include media-breakpoint-down(md) { + border-radius: 100px; + margin-bottom: 0.5rem; + } + } + + .input-group + > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not( + .invalid-tooltip + ):not(.invalid-feedback) { + &.btn-apply-filters { + border-radius: 100px; + + @include media-breakpoint-down(md) { + width: 100%; + } + } + } + } + + &.has-active-filter .ntf-filter-search { + .btn-reset-filters { + display: block; + } + } + + &:not(.has-new-input) .ntf-filter-search { + .btn-apply-filters { + color: $gray; + border-color: $gray; + pointer-events: none; + } + } + + .filter-by-date { + position: relative; + + @include media-breakpoint-down(md) { + width: 100%; + margin-right: 0; + } + + .input-button { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + position: absolute; + bottom: 0; + border-radius: 4px; + cursor: pointer; + + @include media-breakpoint-down(md) { + top: 2rem; + } + + &-calendar { + right: 0; + + &::before { + content: ''; + display: block; + width: 1.5rem; + height: 1.5rem; + @extend %icon-date; + } + } + } + + input { + min-width: 220px; + padding-left: 1rem; + } + } +} + +.modal-dialog .modal-content { + .ntf-filter-search .btn-apply-filters { + pointer-events: initial!important; + } +} \ No newline at end of file diff --git a/assets/sass/backoffice/_global.scss b/assets/sass/backoffice/_global.scss new file mode 100644 index 000000000..ed497c11d --- /dev/null +++ b/assets/sass/backoffice/_global.scss @@ -0,0 +1,720 @@ +.hide-sm { + @media (max-width: 767px) { + display: none !important; + } +} + +.hide-lg { + @media (min-width: 768px) { + display: none !important; + } +} + +.cell-input-wrap { + display: flex; + align-items: center; + + input[type='checkbox'] { + margin-right: 0.5rem; + } + + a { + font-weight: 600; + } +} + +.flash-notices { + position: fixed; + right: 0; + top: 4rem; + font-size: 0.875rem; + z-index: 9999; + animation: 3s hideMessage 5s forwards; + + > div { + display: flex; + align-items: center; + padding: 0.5rem 1rem; + margin-bottom: 0.25rem; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } +} + +.flash-success, +.flash-notice, +.flash-error { + padding: 1rem; + color: $secondary_4; + font-size: 1rem; + padding-bottom: 0; + font-weight: 600; + + &::before { + content: ''; + margin-right: 0.5rem; + border-radius: 50%; + padding: 0.25rem; + width: 1.5rem; + height: 1.5rem; + } +} + +.flash-success { + background: $flash-ntf; + color: $flash-ntf-text; + + &::before { + @extend %icon-check-white; + background-color: $success-green; + } +} + +.flash-notice { + background: $orange; + + &::before { + content: '!'; + line-height: 1; + } +} + +.flash-error { + background: $red; + color: $white; + + &::before { + content: '!'; + line-height: 1; + font-weight: 600; + margin-right: 0; + } +} + +@keyframes hideMessage { + from { + opacity: 1; + z-index: 9999; + } + + to { + opacity: 0; + z-index: -1; + } +} + +form { + label { + font-size: 0.875rem; + margin-right: 1rem; + } + + .row-input { + &.error-input { + ul { + list-style: none; + margin-top: 0.5rem; + margin-bottom: 0; + padding-left: 0; + + li { + color: $red; + font-size: 0.75rem; + display: flex; + align-items: center; + + &::before { + content: ''; + @extend %icon-alert; + margin-right: 0.25rem; + width: 1rem; + height: 1rem; + display: inline-block; + } + } + } + } + } + + .ss-main .ss-values .ss-value { + background-color: rgba($secondary_1, 0.12); + font-weight: 900; + padding: 0.0625rem; + + .ss-value-text { + color: $secondary_1; + } + + .ss-value-delete { + border-left: none; + svg { + transform: scale(1.25); + margin-top: 0.0625rem; + + path { + stroke: $secondary_1; + } + } + } + } +} + +div.ss-content { + font-size: 0.875rem; + border: 2px solid $border; + border-radius: 4px; + background-color: $light-bg; + + &.ss-open-below { + border-top: none; + border-color: $secondary_1; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + &.ss-open-above { + border-bottom: none; + border-color: $secondary_1; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + .ss-list { + .ss-option:not(.ss-hide) { + line-height: 1.4; + margin: 0 0.5rem 0.25rem; + padding: 0.375rem 0.25rem; + display: block; + &:hover { + background-color: $secondary_2; + color: inherit; + } + + .ss-search-highlight { + padding: 0; + background-color: $secondary_4; + color: $secondary_3; + } + } + + .ss-search, + .ss-searching { + padding: 0.25rem 0.875rem 0.5rem; + } + } + + .ss-search { + input { + background-color: transparent; + border: none; + + &:focus { + box-shadow: none; + } + + &::-webkit-search-cancel-button { + width: 0.75rem; + height: 0.75rem; + } + } + + .ss-addable { + border: none; + margin-right: 0.5rem; + } + } +} + +div.ss-main { + padding-left: 0.8125rem; + border-radius: 4px !important; + + .ss-arrow { + width: 1rem; + height: 1rem; + margin-right: 1rem; + + path { + stroke-width: 14px; + } + } + + &[aria-expanded='true'] { + border-color: $secondary_1; + } + + &.ss-open-below { + border-bottom: none; + border-radius: 4px 4px 0 0 !important; + } + + &.ss-open-above { + border-top: none; + border-radius: 0 0 4px 4px !important; + } +} + +div.ss-main, +input[type='text'], +input[type='email'], +input[type='password'], +input[type='email'], +input[type='file'], +select, +textarea, +textarea.form-control { + font-size: 0.875rem; + font-weight: 600; + height: 2.5rem; + border: 2px solid $border; + border-radius: 4px; + width: 330px; + background-color: $light-bg; + transition: border-color 0.3s ease, background-color 0.3s ease; + + @include media-breakpoint-down(md) { + width: 100%; + max-width: 330px; + } +} + +textarea, +textarea.form-control { + height: unset; +} + +input[type='text'], +input[type='password'], +input[type='email'], +select, +select.form-select, +textarea, +textarea.form-control { + padding: 0 0.75rem 0 1rem; + + &::placeholder { + font-weight: 400; + color: $border-hover; + } + + &:hover:not(:disabled):not(:focus):not([readonly='readonly']) { + border-color: $border-hover; + } + + &:focus, + &:focus-visible { + outline: none; + box-shadow: none; + + &:not(:disabled):not([readonly='readonly']) { + border-color: $secondary_1; + font-weight: 400; + letter-spacing: 0.07px; + } + } + + &:disabled, + &[readonly='readonly']:not([data-provide='datetimepicker']):not(.flatpickr-input) { + border-color: $light-bg; + background-color: $light-bg; + color: $border-hover; + } +} + +textarea, +textarea.form-control { + padding: 0.625rem 0.75rem 0.625rem 1rem; + height: unset; +} + +select { + @extend %icon-chevron; + background-position: right 12px center; + appearance: none; + padding-right: 2.75rem; + + &, + &.form-select { + font-size: 0.875rem; + font-weight: 600; + } +} + +input[type='checkbox'] { + &, + &.form-check-input { + position: relative; + height: 1.25rem; + width: 1.25rem; + cursor: pointer; + appearance: none; + background-color: $white; + margin: 0; + + border-radius: 4px; + border: 2px solid $border; + transition: background-color 0.2s ease, border-color 0.2s ease; + + &:not(:checked):not(:indeterminate):hover { + background-color: $light-bg; + } + + &:checked:not(:indeterminate) { + background-color: $secondary_4; + border-color: $secondary_4; + background-image: none; + + &::after { + content: ''; + @extend %icon-check; + position: absolute; + inset: 0; + } + } + + &:indeterminate { + background-color: $secondary_4; + border-color: $secondary_4; + background-image: none; + + &::after { + content: '—'; + color: $white; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + position: absolute; + inset: 0; + } + } + } +} + +input[type='file'] { + font-weight: 400; + + &::file-selector-button { + background-color: $white; + color: $secondary_1; + border: none; + border-radius: 4px; + border: 2px solid $border; + padding: 0.125rem 0.375rem; + margin: 0.25rem; + height: 1.75rem; + cursor: pointer; + transition: background-color 0.3s ease, border-color 0.3s ease; + font-weight: 600; + } + + &:hover { + &::file-selector-button { + background-color: rgba($secondary_1, 0.12); + border-color: $border-hover; + } + } +} + +input[type='radio'], +input[type='radio'].form-check-input { + border-width: 2px; + width: 1.25rem; + height: 1.25rem; + @extend .form-check-input; + + &:checked { + background-image: none; + position: relative; + background-color: transparent; + border-color: $secondary_4; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0.625rem; + height: 0.625rem; + border-radius: 500px; + background-color: $secondary_4; + } + } +} + +div.cke, +div.cke.cke_chrome { + border: none; + box-sizing: border-box; + width: calc(65% - 1rem); + border-radius: 4px; + border: 2px solid $border; + transition: border-color 0.3s ease; + + &:hover { + border-color: $border-hover; + } + + &.cke_focus { + border-color: $black; + } + + .cke_inner { + flex: 1; + + .cke_top { + border-bottom: none; + } + + .cke_bottom { + border-top: none; + } + + .cke_contents { + position: relative; + + &::after { + content: ''; + position: absolute; + inset: 0; + background-color: rgba(0, 0, 0, 0.01); + pointer-events: none; + } + } + } +} + +ol li a { + color: $secondary_4; +} + +.ntf-view-text a { + color: $secondary_4; + font-weight: 600; + border-bottom: 2px solid $border; +} + +.gl-star-rating--stars[class*=' s'] > span { + background-image: none !important; + + &::before { + content: ''; + @extend %icon-star; + position: absolute; + width: 1.5rem; + height: 1.5rem; + } + + &:hover { + &::before { + @include icon-color(secondary_4); + } + } +} + +.gl-star-rating--stars[class*=' s'] > span.gl-active { + background-image: none !important; + + &::before { + @extend %icon-star-filled; + } +} + +input[type='range'] { + width: 330px; + + @include media-breakpoint-down(md) { + width: 100%; + max-width: 330px; + } + + + output { + width: fit-content; + } +} + +.custom-image-upload { + .file-wrapper { + display: inline-block; + border: 2px solid $border; + border-radius: 4px; + padding: 0.25rem; + width: 330px; + max-width: 100%; + background-color: $light-bg; + height: 2.5rem; + display: inline-block; + + @include media-breakpoint-down(sm) { + width: 100%; + max-width: unset; + } + + &.hidden { + display: none; + } + + .file-container { + width: fit-content; + display: flex; + gap: 8px; + align-items: center; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 600; + background-color: rgba($secondary_1, 0.12); + + .file-name { + max-width: 274px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + @include media-breakpoint-down(sm) { + max-width: calc(100vw - 6.5rem); + } + } + + .remove-image-btn { + background-color: transparent; + border: none; + + &::after { + content: ''; + @extend %icon-close; + position: absolute; + inset: 0; + } + } + } + } + + .image-upload { + position: relative; + + &.hidden { + display: none; + } + } + + .btn-replace-image { + text-transform: uppercase; + height: fit-content; + align-self: center; + margin-left: 16px; + + &.hidden { + display: none; + } + } +} + +.alert.alert-margin { + margin: 1rem; +} + +.ajax-message-wrapper { + max-width: 40%; + margin: 7rem auto 2rem; + + i { + font-size: 4rem; + color: $success-green; + margin-bottom: 2rem; + } + + &-title { + font-size: 1.5rem; + margin-bottom: 1.25rem; + } + + h4 { + font-weight: 400; + font-size: 1.5rem; + } + + @include media-breakpoint-down(md) { + max-width: 100%; + + i { + font-size: 3rem; + } + + h4 { + font-size: 1.25rem; + } + } +} + +.sf-helper { + @include media-breakpoint-up(xl) { + .ntf-create-actions, + .create-actions, + .event-actions { + background-color: $white; + width: calc(100% - 248px) !important; + } + } +} + +.ntf-create { + input[type=checkbox] { + height: auto; + width: revert; + } +} + +.admin-form-action-buttons-wrapper { + display: none; + background-color: $light-bg; + width: 100%; + &.visible { + display: inline-flex; + } + + .btn-action { + background: none; + border: none; + display: flex; + align-items: center; + font-size: .875rem; + color: $secondary_4; + margin-right: 0.875rem; + + &.icon-user-cancel { + &::before { + content: ''; + @extend %icon-user-cancel; + width: 1.5rem; + height: 1.5rem; + display: block; + margin-right: 0.5rem; + } + } + + &.icon-user-check { + &::before { + content: ''; + @extend %icon-user-check; + width: 1.5rem; + height: 1.5rem; + display: block; + margin-right: 0.5rem; + } + } + + &.icon-delete { + &::before { + content: ''; + @extend %icon-trash; + width: 1.5rem; + height: 1.5rem; + display: block; + margin-right: 0.5rem; + } + } + } +} diff --git a/assets/sass/backoffice/_global_styles.scss b/assets/sass/backoffice/_global_styles.scss new file mode 100644 index 000000000..98656c404 --- /dev/null +++ b/assets/sass/backoffice/_global_styles.scss @@ -0,0 +1,179 @@ +// Section styles + +.backoffice-container { + flex: 1; + + &.page-actions-fixed { + margin-bottom: calc(4.5rem - 0rem); + } + + .impersonation-active & { + min-height: calc(100vh - 5rem - 3.25rem - 4.625rem); + } + + .backoffice-container-title { + color: $secondary_1; + padding: 1rem; + + font-size: 1.25rem; + } +} + +.backoffice-section { + padding: 1rem; + + + .backoffice-section, + .backoffice-container-title + &, + + * > .backoffice-section { + border-top: 2px solid $secondary_2; + } + + .backoffice-section-title { + color: $secondary_1; + + font-size: 1.125rem; + line-height: 1.2; + font-weight: 600; + + + .backoffice-section-description { + margin-top: 0.25rem; + font-size: 0.75rem; + line-height: 1.5; + color: rgba($secondary_1, 0.66); + } + + ~ .backoffice-section-content { + margin-top: 1rem; + } + } + + .backoffice-section-content:first-child { + .backoffice-table-view { + margin-top: -1rem; + } + } + + .backoffice-section-actions { + display: flex; + justify-content: space-between; + + .backoffice-section-actions-right { + margin-left: auto; + } + + .backoffice-section-actions-left { + + .backoffice-section-actions-right { + margin-left: unset; + } + } + + a, + button { + margin-top: 1rem; + } + } +} + +// Page actions styles (e.g. next page, discard, save) + +.backoffice-page-actions { + display: flex; + justify-content: space-between; + padding: 0 1rem; + position: relative; + background-color: $white; + z-index: 1; + + &::after { + content: ''; + position: absolute; + inset: 0px 0 calc(100% - 2px) 0; + background-color: $secondary_2; + } + + .backoffice-page-actions-right { + margin-left: auto; + } + + .backoffice-page-actions-left { + + .backoffice-page-actions-right { + margin-left: unset; + } + } + + .backoffice-page-actions-left, + .backoffice-page-actions-right { + display: flex; + gap: 1rem; + + button, + a { + margin: 1rem 0; + } + } + + &.page-actions-fixed { + position: fixed; + inset: auto 0 0 15.5rem; + } +} + +// Table styles + +.backoffice-table-view { + margin: 0 -1rem -1rem; + + .backoffice-table-head { + tr { + border-bottom: 2px solid $secondary_2; + + th { + font-size: 0.875rem; + line-height: 1.4; + font-weight: 400; + color: rgba($secondary_1, 0.54); + padding: 0.875rem 0; + + &:first-child { + padding-left: 1rem; + } + } + } + } + + .backoffice-table-body { + font-size: 0.875rem; + line-height: 1.4; + + tr { + + &:not(:last-child) { + border-bottom: 2px solid $secondary_2; + } + + td { + padding: 0.75rem 0; + + &:first-child { + padding-left: 1rem; + } + + &.backoffice-table-row-title { + font-weight: 600; + } + } + } + + .backoffice-table-row-highlight { + background-color: rgba($secondary_4, 0.05); + } + } + + + .backoffice-section-actions { + border-top: 2px solid $secondary_2; + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; + } +} diff --git a/assets/sass/backoffice/_header.scss b/assets/sass/backoffice/_header.scss new file mode 100644 index 000000000..e3cdf7929 --- /dev/null +++ b/assets/sass/backoffice/_header.scss @@ -0,0 +1,380 @@ +.app-header { + height: $chat-header-height; + display: flex; + align-items: center; + position: fixed; + border-bottom: 2px solid $secondary_2; + width: calc(100% - 248px); + right: 0; + top: 0; + z-index: 100; + background: $white; + transition: all .2s ease-in-out; + + @include media-breakpoint-down(md) { + height: 4.25rem; + width: 100%; + justify-content: space-between; + } + + &__content { + position: relative; + padding: 0 1rem; + width: 100%; + + @include media-breakpoint-down(md) { + width: auto; + } + + .header-action-button { + display: flex; + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + } + + h5 { + display: flex; + align-items: center; + span { + width: 3rem; + height: 3rem; + background: $secondary_2; + border-radius: 100px; + margin-right: 1rem; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + font-size: 1.1875rem; + } + } + + .header-icon { + margin-right: 1rem; + display: flex; + align-items: center; + justify-content: center; + background: $secondary_2; + border-radius: 50%; + + @media (max-width: 768px) { + width: 2.5rem; + height: 2.5rem; + margin-right: 0; + } + + &::before { + content: ""; + display: block; + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + &__content-wrap { + display: flex; + align-items: center; + position: relative; + color: $secondary_1; + } + + &__content-initials { + background: $secondary_2; + width: 3rem; + height: 3rem; + border-radius: 50%; + font-size: 0.75rem; + font-weight: 700; + display: flex; + justify-content: center; + align-items: center; + margin-right: 1rem; + + @include media-breakpoint-down(md) { + width: 2.5rem; + height: 2.5rem; + margin-right: 0; + } + } + + &__content-name { + font-size: 1.375rem; + font-weight: 600; + + @include media-breakpoint-down(md) { + display: none; + } + } + + &__content-location { + font-size: 0.75rem; + color: rgba($secondary_1, .66); + display: block; + margin-top: 0.25rem; + + @include media-breakpoint-down(md) { + display: none; + } + } + + &-logo { + @include media-breakpoint-up(md) { + display: none; + } + } + + .header-menu-icon-wrap { + display: flex; + align-items: center; + } + + .btn-primary { + font-size: 0.75rem; + text-transform: uppercase; + margin-left: auto; + } +} + +.info-header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + &-wrap { + display: flex; + align-items: center; + } + + &-name { + font-size: 1.375rem; + font-weight: 600; + + @media (max-width: 768px) { + display: none; + } + } + + &.info-header-modified { + + @include media-breakpoint-down(lg) { + h4 span { + display: none; + } + } + .btn-link-plain.country-profile-view { + + @include media-breakpoint-up(lg) { + position: absolute; + right: 1rem; + top: 4.6rem; + } + } + } + + h4 { + font-size: 1.375rem; + display: flex; + align-items: center; + + @include media-breakpoint-down(sm) { + font-size: 0.875rem; + } + } + + h4 i { + display: block; + width: 48px; + height: 48px; + border-radius: 50%; + background: $secondary-1; + margin-right: 16px; + + &::before { + content: ""; + display: block; + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } + + &.icon-arrow { + background: none; + cursor: pointer; + &::before { + @extend %icon-arrow; + } + } + } + + .content-download-btns { + padding: 1rem; + + .btn-icon-pdf { + margin-right: 0.5rem; + } + } +} + +.app-header-actions { + display: flex; + gap: 1.5rem; + margin-right: 1rem; + &.app-header-download-actions { + margin: 1rem; + } +} + +.app-header-tabs { + display: flex; + text-align: center; + + .app-tabs-mobile-wrapper & { + justify-content: center; + margin: 6.25rem auto 0; + border-bottom: 2px solid $border; + padding-bottom: 1.125rem; + + @include media-breakpoint-up(xl) { + display: none; + } + + @include media-breakpoint-down(md) { + margin: 5.25rem auto 0; + } + } + + .app-header__content-wrap & { + position: absolute; + left: 0; + right: 0; + margin: auto; + max-width: 300px; + + @include media-breakpoint-down(xl) { + display: none; + } + } + + a { + font-size: 0.875rem; + font-weight: 600; + padding: 0.625rem 2rem 0.375rem; + display: flex; + align-items: center; + position: relative; + + &::before { + content: ""; + display: inline-block; + margin-right: 0.5rem; + margin-bottom: 0.1875rem; + width: 1.5rem; + height: 1.5rem; + } + + &::after { + content: ""; + width: 100%; + height: 2px; + background: $secondary_1; + position: absolute; + left: 0; + bottom: -1.25rem; + } + + &.icon-overview { + &::before { + @extend %icon-overview; + } + } + + &.icon-docs { + &::before { + @extend %icon-docs; + } + } + + &:not(.tab-active) { + opacity: .66; + + &::after { + background: transparent; + } + } + } +} + +.closed-sidebar .app-header { + width: calc(100% - 75px); + + @include media-breakpoint-down(md) { + width: 100%; + } +} + +.mainnav-toggle { + display: none; + position: relative; + right: $gutter; + width: 3.75rem; + height: 3.75rem; + + @include media-breakpoint-down(md) { + display: block; + right: 0; + width: 3rem; + } + + .hamburger { + display: block; + width: 1.25rem; + height: 0.125rem; + position: absolute; + background: $black; + left: 50%; + top: 50%; + margin: -0.0625rem 0 0 -0.625rem; + transition: background 100ms ease-out; + + &::before, + &::after { + content: ''; + display: block; + width: 100%; + height: 100%; + position: absolute; + background: $black; + transition: all 400ms; + } + + &::before { + top: -0.375rem; + } + &::after { + bottom: -0.375rem; + } + } + + .closed-sidebar & { + .hamburger { + background: transparent; + transition: background 100ms ease-out; + &::before { + transform: translate(0, 0.375rem) rotate(135deg); + } + &::after { + transform: translate(0, -0.375rem) rotate(-135deg); + } + } + } +} diff --git a/assets/sass/backoffice/_icons.scss b/assets/sass/backoffice/_icons.scss new file mode 100644 index 000000000..9c3138f02 --- /dev/null +++ b/assets/sass/backoffice/_icons.scss @@ -0,0 +1,156 @@ +%icon-dashboard { + background: url("../assets/icons/icon_dashboard.svg") no-repeat center center; +} + +%icon-bookmarks { + background: url("../assets/icons/icon_bookmarks.svg") no-repeat center center; +} + +%icon-conversations { + background: url("../assets/icons/icon_conversations.svg") no-repeat center center; +} + +%icon-info { + background: url("../assets/icons/icon_info.svg") no-repeat center center; +} + +%icon-close { + background: url("../assets/icons/icon_close.svg") no-repeat center center; +} + +%icon-date { + background: url("../assets/icons/icon_date.svg") no-repeat center center; +} + +%icon-plus { + background: url("../assets/icons/icon_plus.svg") no-repeat center center; +} + +%icon-download { + background: url("../assets/icons/icon_download.svg") no-repeat center center; +} + +%icon-logout { + background: url("../assets/icons/icon_logout.svg") no-repeat center center; +} + +%icon-arrow { + background: url("../assets/icons/icon_arrow_left.svg") no-repeat center center; +} + +%icon-arrow-white { + background: url("../assets/icons/icon_arrow_white.svg") no-repeat center center; +} + +%icon-search { + background: url("../assets/icons/icon_search.svg") no-repeat center center; +} + +%icon-docs { + background: url("../assets/icons/icon_docs.svg") no-repeat center center; +} + +%icon-overview { + background: url("../assets/icons/icon_overview.svg") no-repeat center center; +} + +%icon-delete { + background: url("../assets/icons/icon_delete.svg") no-repeat center center; +} + +%icon-warning { + background: url("../assets/icons/icon_warning.svg") no-repeat center center; +} + +%icon-external { + background: url("../assets/icons/icon_external.svg") no-repeat center center; +} + +%icon-external-dark { + background: url("../assets/icons/icon_exteral_dark.svg") no-repeat center center; +} + +%icon-dots { + background: url("../assets/icons/icon_dots.svg") no-repeat center center; +} + +%icon-filter { + background: url("../assets/icons/icon_filter_red.svg") no-repeat center center; +} + +%icon-check { + background: url("../assets/icons/icon_check.svg") no-repeat center center; +} + +%icon-check-white { + background: url("../assets/icons/icon_check_white.svg") no-repeat center center; +} + +%icon-x { + background: url("../assets/icons/icon_close_small.svg") no-repeat center center; +} + +%icon-view { + background: url("../assets/icons/icon_view.svg") no-repeat center center; +} + +%icon-edit { + background: url("../assets/icons/icon_edit.svg") no-repeat center center; +} + +%icon-chevron { + background: url("../assets/icons/icon_chevron.svg") no-repeat center center; +} + +%icon-check { + background: url("../assets/icons/icon_check.svg") no-repeat center center; +} + +%icon-alert { + background: url("../assets/icons/icon_alert.svg") no-repeat center center; +} + +%icon-star { + background: url("../assets/icons/icon_star.svg") no-repeat center center; +} + +%icon-star-filled { + background: url("../assets/icons/icon_star_filled.svg") no-repeat center center; +} + +%icon-copy { + background: url("../assets/icons/icon_copy.svg") no-repeat center center; +} + +%icon-user-cancel { + background: url("../assets/icons/icon_user_cancel.svg") no-repeat center center; +} + +%icon-user-check { + background: url("../assets/icons/icon_user_check.svg") no-repeat center center; +} + +%icon-trash { + background: url("../assets/icons/icon_delete.svg") no-repeat center center; +} + +%icon-rename { + background: url("../assets/icons/icon_edit.svg") no-repeat center center; +} + +%icon-save { + background: url("../assets/icons/icon_save.svg") no-repeat center center; +} + +%icon-list { + background: url("../assets/icons/icon_list.svg") no-repeat center center; +} + +%icon-user-group { + background: url("../assets/icons/icon_user_group.svg") no-repeat center center; +} + +%icon-notifications { + background: url("../assets/icons/icon_notifications_dark.svg") no-repeat center center; +} + diff --git a/assets/sass/backoffice/_listing.scss b/assets/sass/backoffice/_listing.scss new file mode 100644 index 000000000..46272b766 --- /dev/null +++ b/assets/sass/backoffice/_listing.scss @@ -0,0 +1,285 @@ +.list-table { + thead { + tr { + border-bottom: 2px solid $secondary_2; + height: 3rem; + vertical-align: middle; + + th { + font-size: 0.875rem; + padding: 0 1rem; + color: rgba($secondary_1, .54); + font-weight: 400; + } + } + } +} + +.list-body { + vertical-align: middle; + + @include media-breakpoint-down(md) { + min-width: 900px; + } + + tr { + border-bottom: 2px solid $secondary_2; + height: 3rem; + + td { + font-size: 0.875rem; + padding: 0 1rem; + + &.list-title { + font-weight: 600; + } + + &.cell-wide { + padding: 1rem; + } + + &.cell-baseline { + vertical-align: top; + padding-top: 0.75rem; + } + } + + &.selected { + background: rgba(163, 31, 52, 0.1); + } + } + + &-title { + color: $secondary_1; + + @include media-breakpoint-down(sm) { + display: -webkit-box; + -webkit-line-clamp: 3; + overflow: hidden; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + } + } + + &-type { + span { + border-radius: 100px; + background: $secondary_2; + padding: 0.125rem 0.75rem; + color: inherit; + } + } + + .dropdown { + cursor: pointer; + position: relative; + width: fit-content; + + .dropdown-toggle { + &::after { + content: unset; + } + + i { + &::after { + content: ''; + display: block; + width: 1.5rem; + height: 1.5rem; + } + + &.icon-dots::after { + @extend %icon-dots; + } + } + } + + .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + flex-direction: column; + gap: 0.5rem; + background-color: $white; + border: 2px solid $secondary_2; + border-radius: 4px; + z-index: 1; + padding: 0.5rem; + box-shadow: 0px 4px 12px 0px rgba(37, 45, 64, 0.1); + + .dropdown-item { + display: flex; + align-items: center; + position: relative; + padding: 0; + + &:hover { + background-color: $darkgray; + color: $secondary_1; + } + + &.with-icon { + + a, button { + padding-right: 2.5rem; + } + + i { + position: absolute; + right: 0; + left: auto; + } + } + + &.with-icon-left { + + a, button { + padding-right: 0; + padding-left: 2.5rem; + } + + i { + right: auto; + left: 0; + } + } + } + + i { + left: 0; + display: flex; + padding: 0.5rem; + pointer-events: none; + + &::before { + content: ''; + width: 1.5rem; + height: 1.5rem; + display: inline-block; + } + } + + i.icon-docs::before { + @extend %icon-docs; + } + i.icon-delete::before { + @extend %icon-delete; + } + i.icon-view::before { + @extend %icon-view; + } + i.icon-plus::before { + @extend %icon-plus; + } + i.icon-edit::before { + @extend %icon-edit; + } + i.icon-external-dark::before { + @extend %icon-external-dark; + } + + a, + button { + display: block; + font-size: 0.875rem; + background: none; + white-space: nowrap; + border: none; + padding: 0.5rem 0; + width: 100%; + text-align: start; + } + } + } +} + +nav.page-navigation { + ul { + display: flex; + margin: 2rem 0; + justify-content: center; + + .page-item { + list-style: none; + a { + width: 2.5rem; + height: 2.5rem; + margin: 0.75rem; + display: flex; + justify-content: center; + align-items: center; + font-size: 0.875rem; + font-weight: 600; + border-radius: 2px; + } + + &.current { + .page-link { + width: 2.5rem; + height: 2.5rem; + margin: 0.75rem 0.75rem 0.75rem 0; + display: flex; + justify-content: center; + align-items: center; + font-size: 0.875rem; + font-weight: 600; + border-radius: 2px; + background-color: $secondary_2; + color: $border-hover; + } + } + + &.disabled { + opacity: .05; + } + } + + span.disabled, a[rel=next], a[rel=prev] { + text-indent: -9999px; + + &::after { + @extend %icon-arrow; + content: ""; + display: block; + width: 1.5rem; + height: 1.5rem; + } + } + + span.disabled, a[rel=next] { + &::after { + transform: rotate(180deg); + } + } + + span.disabled:first-child { + &::after { + transform: rotate(0); + } + } + } +} + +.nothing-to-show { + display: flex; + flex-direction: column; + align-items: center; + border: 2px dashed $border; + border-radius: .5rem; + padding: 1.5rem; + + &.nothing-to-show-with-margin { + margin: 1rem; + } + + .icon.icon-list { + @extend %icon-list; + width: 1.5rem; + height: 1.5rem; + } + + p { + font-size: .875rem; + margin-bottom: .5rem; + } +} \ No newline at end of file diff --git a/assets/sass/backoffice/_main.scss b/assets/sass/backoffice/_main.scss new file mode 100644 index 000000000..e2be1675f --- /dev/null +++ b/assets/sass/backoffice/_main.scss @@ -0,0 +1,121 @@ +html, body { + background-color: $white; +} + +.app-container .app-main__outer { + transition: all .2s ease-in-out; + + @include media-breakpoint-down(md) { + padding-top: 5rem; + } +} + +.impersonation-active { + .app-header { + top: 3.25rem; + + @media (max-width: 600px) { + top: 4.8125rem; + } + } + + .app-container { + margin-top: 3.25rem; + + @media (max-width: 600px) { + margin-top: 4.8125rem; + } + } + + .app-sidebar { + top: 3.25rem; + height: calc(100vh - 3.25rem); + + @media (max-width: 600px) { + top: 9rem; + height: calc(100vh - 9rem); + } + } +} + +.fixed-sidebar.app-container .app-main__outer { + z-index: 9; + padding-left: 15.5rem; + padding-top: 5rem; + + @include media-breakpoint-down(md) { + padding-left: 0; + padding-top: 4.25rem; + } +} + +.closed-sidebar.fixed-sidebar .app-main__outer { + padding-left: 4.6875rem; + + @include media-breakpoint-down(md) { + padding-left: 0; + } +} + +.chat-container { + height: calc(100vh - #{$chat-header-height}); +} + +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; + appearance: none; + width: 1rem; + height: 1rem; + margin-right: 1rem; + background-size: cover; + cursor: pointer; + @extend %icon-close; +} + +.impersonation-active-panel { + background: $primary_0; + width: 100%; + position: fixed; + top: 0; + left: 0; + z-index: 1000; + font-size: 0.875rem; + padding: 0.5rem 1rem; + + @media (max-width: 600px) { + flex-direction: column; + } + + p { + display: flex; + align-items: center; + + &::before { + content: ""; + width: 1rem; + height: 1rem; + display: block; + border-radius: 50%; + margin-right: 0.5rem; + @extend %icon-info; + } + } + + span { + font-weight: 600; + margin-left: 0.5rem; + } + + .btn-user-switch-back { + text-transform: uppercase; + color: $secondary_4; + border: 2px solid rgba($black, .12); + border-radius: 20px; + font-size: 0.75rem; + height: 2.25rem; + + @media (max-width: 600px) { + margin-top: 0.25rem; + } + } +} diff --git a/assets/sass/backoffice/_modal.scss b/assets/sass/backoffice/_modal.scss new file mode 100644 index 000000000..9abd8fa0b --- /dev/null +++ b/assets/sass/backoffice/_modal.scss @@ -0,0 +1,125 @@ +.modal { + &.show-spinner { + .spinner#spinner { + display: block; + } + + .modal-content > * { + display: none; + } + } + + @include media-breakpoint-up(sm) { + .modal-wide.modal-dialog { + --bs-modal-width: 630px; + } + } + + .modal-content { + border-radius: 4px; + padding: 32px; + min-height: 200px; + gap: 8px; + + p { + margin: 0; + } + .modal-header { + padding: 0; + border: none; + justify-content: center; + + .modal-title { + display: flex; + flex-direction: column; + gap: 8px; + + &.centered { + align-items: center; + text-align: center; + } + + i { + display: flex; + + &::before { + content: ''; + width: 56px; + height: 56px; + display: inline-block; + } + + &.icon-error::before { + @extend %icon-warning; + } + + &.icon-close::before { + transform: scale(1.25); + width: 12px; + height: 12px; + @extend %icon-close; + } + } + + .close-modal { + all: unset; + cursor: pointer; + position: absolute; + right: 20px; + top: 20px; + } + + p { + font-size: 22px; + font-weight: 600; + line-height: 1.2; + } + } + } + + .modal-body { + font-size: 14px; + padding: 0; + + &.centered { + text-align: center; + } + + .modal-form-input { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: flex-start; + + div.cke, + div.cke.cke_chrome { + width: 100%; + } + } + } + + .modal-footer { + padding: 0; + border-top: none; + margin-top: 32px; + + &.centered { + justify-content: center; + } + + > * { + margin: 0; + } + + .btn-primary { + &::after { + content: unset; + } + } + + .btn-secondary { + border: none; + } + } + } +} diff --git a/assets/sass/backoffice/_my_account.scss b/assets/sass/backoffice/_my_account.scss new file mode 100644 index 000000000..8e53b18e3 --- /dev/null +++ b/assets/sass/backoffice/_my_account.scss @@ -0,0 +1,255 @@ +.my-account { + form { + display: flex; + flex-direction: column; + gap: 1rem; + margin: 1.5rem 1rem; + + > div { + display: grid; + grid-template-columns: 35% 1fr; + column-gap: 1rem; + + row-gap: 0.5rem; + + @include media-breakpoint-down(sm) { + grid-template-columns: 1fr; + } + + label { + flex: 35%; + color: $secondary_1; + margin-right: 0; + font-size: 0.875rem; + line-height: 1.4; + } + + label[for='preferences_email_consent'] { + width: auto; + } + + > input, + > .ss-main, + > .cke, + .custom-image-upload, + select { + width: 100%; + + @include media-breakpoint-down(sm) { + max-width: unset; + } + } + + > input[type='checkbox'] { + min-width: unset; + max-width: 20px; + } + + .errors { + flex: 100%; + margin-left: calc(35% + 1rem); + + @include media-breakpoint-down(sm) { + margin-left: 0; + } + } + } + + .my-account-image-container { + display: flex; + gap: 1rem; + + @include media-breakpoint-down(sm) { + flex-direction: column; + + img { + align-self: flex-start; + } + } + + img { + border-radius: 4px; + max-width: 200px; + max-height: 200px; + + &[src=''] { + display: none; + } + + + .custom-image-upload { + display: flex; + flex-direction: column; + gap: 0.75rem; + + .file-wrapper { + width: unset; + } + + .btn-replace-image { + align-self: unset; + margin-left: 0.5rem; + } + } + } + } + + > .checkbox-row { + + @include media-breakpoint-down(sm) { + grid-template-columns: 1fr minmax(auto, 50%); + } + + label { + grid-row: 1; + grid-column: 1; + } + + input { + grid-row: 1; + grid-column: 2; + } + + .extra-label { + grid-row: 1; + grid-column: 2; + margin-left: 1.625rem; + font-size: 0.875rem; + font-weight: 600; + } + } + + &[name='change_password'] { + + @include media-breakpoint-down(lg) { + input:not([type='checkbox']) { + min-width: 100%; + } + } + + div:nth-child(1), + div:nth-child(2), + div:nth-child(3) { + position: relative; + margin-bottom: 1.25rem; + + @include media-breakpoint-down(lg) { + margin-bottom: 0.5rem; + } + + ul { + flex: 100%; + margin-left: calc(35% + 1rem); + position: absolute; + list-style: none; + top: 3rem; + padding-left: 0; + + @include media-breakpoint-down(lg) { + margin-left: 0; + margin-bottom: 0; + right: 0; + position: revert; + grid-row: 2; + grid-column: 2; + } + + @include media-breakpoint-down(sm) { + grid-column: 1; + grid-row: 3; + } + + li { + color: $red; + font-size: 0.75rem; + display: flex; + align-items: center; + + &::before { + content: ''; + @extend %icon-alert; + margin-right: 0.25rem; + width: 1rem; + height: 1rem; + display: inline-block; + } + } + } + } + + div:nth-child(3) { + margin-bottom: 0; + } + } + } + + .change-email-wrapper { + .email-domain { + display: inline-flex; + align-items: center; + font-size: 0.875rem; + font-weight: 600; + height: 2.5rem; + border: 2px solid $light-bg; + background-color: $light-bg; + color: $border-hover; + border-radius: 4px; + width: 330px; + padding: 0 0.75rem 0 1rem; + } + } + + .form-actions-password-requirements { + margin-bottom: 1rem; + width: calc(65% - 1rem); + margin-left: auto; + display: inline-block; + + p { + font-size: 0.875rem; + color: $gray-54; + margin-bottom: 0; + } + + @include media-breakpoint-down(lg) { + width: 100%; + } + } + + .form-actions { + display: flex; + gap: 1rem; + align-items: center; + width: 100%; + justify-content: space-between; + + @include media-breakpoint-down(lg) { + justify-content: center; + + button { + margin-bottom: 0.5rem; + } + } + } + + &.my-account-preferences { + div.checkbox-row { + display: flex; + align-items: center; + width: max-content; + min-width: 530px; + + span.extra-label { + margin-left: 0; + } + + @include media-breakpoint-down(sm) { + min-width: auto; + flex-direction: column; + } + } + + .form-actions { + justify-content: flex-start; + } + } +} diff --git a/assets/sass/backoffice/_print.scss b/assets/sass/backoffice/_print.scss new file mode 100644 index 000000000..85d3b809e --- /dev/null +++ b/assets/sass/backoffice/_print.scss @@ -0,0 +1,42 @@ +@media print { + html, + body { + font-size: 10px !important; + width: 210mm !important; + margin: 0px; + background-color: transparent; + } + + @page { + size: A4; + width: 210mm; + height: 297mm; + padding: 1cm; + margin: 25mm 25mm 25mm 25mm; + border: 1px #D3D3D3 solid; + border-radius: 0px; + background: white; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + } + .container { + max-width: 100%; + } + .impersonation-active-panel, + .app-sidebar, + .btn:not(.btn-download-link), + .header-icon.icon-arrow { + display: none !important; + } + .list-group-item { + border: none; + border-radius: 0; + } + .alert { + color: $black; + background-color: transparent; + border-color: $black; + } + .collapse:not(.show) { + display: block; + } +} diff --git a/assets/sass/backoffice/_sidebar.scss b/assets/sass/backoffice/_sidebar.scss new file mode 100644 index 000000000..f98124c47 --- /dev/null +++ b/assets/sass/backoffice/_sidebar.scss @@ -0,0 +1,590 @@ +.app-sidebar { + background-color: $white; + position: fixed; + height: 100vh; + width: 248px; + min-width: 248px; + display: flex; + z-index: 11; + flex: 0 0 248px; + top: 0; + transition: all 0.2s; + border-right: 2px solid $secondary_2; + overflow: hidden; + padding: 1.25rem 1.25rem 1.25rem 0; + + @include media-breakpoint-down(md) { + width: 280px; + top: 4.25rem; + filter: drop-shadow(8px 8px 32px rgba(0, 0, 0, 0.1)); + } +} + +.closed-sidebar .app-sidebar { + transition: all 0.2s ease-in-out; + width: 75px; + min-width: 75px; + flex: 0 0 75px; + z-index: 13; + + @include media-breakpoint-down(md) { + width: 280px; + } + + .sidebar-wrapper { + width: revert; + + @include media-breakpoint-down(md) { + width: 100%; + } + } +} + +@include media-breakpoint-down(md) { + .app-container:not(.closed-sidebar) { + .app-sidebar { + transform: translateX(-280px); + filter: none; + } + } +} + +.sidebar-wrapper { + width: 100%; + + &__logo { + + @include media-breakpoint-down(md) { + display: none; + } + + img { + width: 166px; + height: 40px; + object-fit: cover; + margin-left: 1.125rem; + } + } +} + +.sidebar-toggle { + position: absolute; + right: 0; + top: 63px; + background-color: $light-bg; + width: 1.75rem; + height: 2rem; + border-radius: 24px 0 0 24px; + padding-left: 0.75rem; + font-size: 0.875rem; + transition: all 0.2s ease-in-out; + z-index: 20; + border: none; + + @include media-breakpoint-down(md) { + display: none; + } + + i { + color: $secondary_1; + opacity: 0.4; + transition: all 0.2s ease-in-out; + + .closed-sidebar & { + transform: rotate(180deg); + } + } + + &:hover { + width: 2.125rem; + + i { + opacity: 1; + } + } +} + +.sidebar-menu { + margin-top: 3.75rem; + padding-left: 0.5rem; + max-height: calc(100% - 280px); + overflow-y: auto; + overflow-x: hidden; + color: $secondary_1; + + @include media-breakpoint-down(md) { + margin-top: 0; + max-height: calc(100% - 230px); + } + + ul { + list-style: none; + padding: 0; + + li { + font-size: 0.875rem; + margin: 0.5rem 0; + width: 200px; + position: relative; + border-radius: 4px; + + a { + transition: all 0.2s ease-in-out; + padding: 0.8rem 1rem 0.8rem 3rem; + z-index: 10; + position: relative; + display: block; + width: 100%; + + .closed-sidebar & { + opacity: 0; + + @include media-breakpoint-down(md) { + opacity: 1; + } + } + } + + &:hover { + background-color: $secondary_2; + } + + &.current { + background-color: $secondary_2; + } + + i { + + span.menu-ntf-count:not(:empty) { + position: absolute; + background: $primary_0; + min-width: 18px; + height: 18px; + border-radius: 50%; + font-size: 0.625rem; + font-weight: 600; + line-height: 1.6; + top: 0.5rem; + left: 0.625rem; + border: 1px solid rgba(37, 45, 64, 0.12); + padding-right: 0.0625rem; + text-align: center; + font-style: normal; + } + } + + i::before { + content: ''; + width: 1.5rem; + height: 1.5rem; + background-repeat: no-repeat; + position: absolute; + top: 0.6875rem; + left: 1rem; + } + + i.icon-dashboard::before { + @extend %icon-dashboard; + } + + i.icon-bookmarks::before { + @extend %icon-bookmarks; + } + + i.icon-conversations::before { + @extend %icon-conversations; + } + + i.icon-notifications::before { + @extend %icon-notifications; + } + + @include media-breakpoint-down(md) { + width: 100%; + } + } + + li.has-submenu { + > div { + padding: 0.625rem 0 0.625rem 2.875rem; + cursor: pointer; + + span:not(.menu-ntf-count) { + transition: all 0.2s ease-in-out; + pointer-events: none; + + .closed-sidebar & { + opacity: 0; + + @include media-breakpoint-down(md) { + opacity: 1; + } + } + } + + span.submenu-toggle-icn { + background: transparent; + display: block; + width: 1.875rem; + height: 1.875rem; + position: absolute; + right: 0; + top: 0.375rem; + z-index: 10; + pointer-events: all; + } + + + ul.menu_level_1 { + li > div > i { + &::before { + display: none; + } + } + } + } + + li { + width: 100%; + + > div { + padding-left: 3rem; + } + + > ul { + padding-left: 3rem; + } + } + > div > i::before { + content: ''; + width: 1.5rem; + height: 1.5rem; + background-repeat: no-repeat; + display: inline-block; + position: absolute; + top: 0.5625rem; + left: 1rem; + } + + ul.menu_level_1 { + padding: 0 0 0.3125rem 0; + background-color: $secondary_2; + width: 100%; + display: none; + + a { + width: 90%; + border-radius: 4px; + padding: 0.125rem 0.25rem; + margin: 0.125rem; + + &:hover { + background: $light-bg; + } + } + + &.submenu-open { + display: block; + } + + .closed-sidebar & { + opacity: 0; + + @include media-breakpoint-down(md) { + opacity: 1; + } + } + + &.active { + display: block; + } + } + + &::after { + font-family: 'Font Awesome 5 Free'; + content: '\f078'; + display: inline-block; + width: 1.5rem; + height: 1.5rem; + color: inherit; + position: absolute; + font-weight: 600; + right: 0.125rem; + top: 0.75rem; + text-align: center; + transition: all 0.2s ease-in-out; + transform-origin: 50% 40%; + cursor: pointer; + } + + &.active { + background-color: $secondary_2; + + &::after { + transform: rotateX(180deg); + } + + .closed-sidebar & { + background-color: transparent; + + @include media-breakpoint-down(md) { + background-color: $secondary_2; + } + } + } + } + + li:not(.has-submenu) { + > div > i:before { + top: 50%; + transform: translateY(-50%); + } + } + + li.current_ancestor.has-submenu { + background-color: $secondary_2; + > div, + div > span { + pointer-events: none; + } + + .menu_level_1 { + display: block; + + .current a { + background: $gray; + font-weight: 600; + } + } + } + } +} + +.sidebar-bottom { + position: absolute; + bottom: 0; + width: 100%; + border-top: 2px solid $secondary_2; + padding: 1.625rem 1.625rem 1.625rem 0.5rem; + + @include media-breakpoint-down(md) { + bottom: 4.25rem; + } + + .avatar { + height: 40px; + min-height: 40px; + width: 40px; + min-width: 40px; + border-radius: 50%; + object-fit: cover; + margin-right: 8px; + } + + ul { + list-style: none; + padding: 0; + + li { + font-size: 0.875rem; + margin: 0.5rem 0; + width: 200px; + position: relative; + border-radius: 4px; + + a { + transition: all 0.2s ease-in-out; + padding: 0.8rem 1rem 0.8rem 3.5rem; + z-index: 10; + position: relative; + display: block; + width: 100%; + + .closed-sidebar & { + opacity: 0; + + @include media-breakpoint-down(md) { + opacity: 1; + } + } + } + + &:hover { + background-color: $secondary_2; + } + + i { + font-style: normal; + + &:before { + content: ''; + width: 1.5rem; + height: 1.5rem; + background-repeat: no-repeat; + position: absolute; + top: 0.6875rem; + left: 1rem; + + } + } + } + } + + .user-settings { + display: flex; + align-items: center; + margin-left: 0.5rem; + user-select: none; + cursor: pointer; + + i span { + margin-right: 0.5rem; + background: $secondary_2; + border-radius: 100%; + width: 2.5rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.625rem; + font-weight: 800; + } + + a { + padding: 0; + } + } + + .user-settings-menu-wrap { + position: relative; + + .user-settings { + > div { + width: 100%; + transition: all 0.2s ease-in-out; + padding-right: 4px; + + @include media-breakpoint-up(md) { + .closed-sidebar & { + opacity: 0; + } + } + } + + &::after { + font-family: 'Font Awesome 5 Free'; + content: '\f078'; + display: inline-block; + width: 1rem; + height: 1rem; + color: inherit; + font-weight: 600; + transform: rotate(-90deg); + margin-right: 0.5rem; + } + } + } + + .user-settings-menu { + display: none; + position: absolute; + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.1); + border-radius: 2px; + bottom: 7.5rem; + z-index: 9999; + background-color: $white; + color: $secondary_1; + left: 0.25rem; + max-width: 240px; + + &__header { + padding: 2rem 1.5rem; + text-align: center; + img, .user-icon { + height: 3.5rem; + width: 3.5rem; + border-radius: 50%; + object-fit: cover; + margin: 0 auto 1.5rem; + } + + .user-icon { + background-color: $primary_2; + display: flex; + justify-content: center; + align-items: center; + + &::before { + content: url('../assets/icons/icon_user.svg'); + } + } + + h6 { + font-size: 0.875rem; + font-weight: 600; + } + } + + &__body { + border-top: 2px solid $secondary_2; + border-bottom: 2px solid $secondary_2; + + ul { + padding: 1rem 1.5rem; + margin-bottom: 0; + + li { + position: relative; + margin: 0.6rem 0; + a { + padding: 0; + + &::after { + font-family: 'Font Awesome 5 Free'; + content: '\f078'; + display: inline-block; + width: 1.5rem; + height: 1.5rem; + color: inherit; + position: absolute; + font-weight: 600; + right: 0; + top: -2px; + text-align: center; + cursor: pointer; + transform: rotate(-90deg); + } + } + + &:hover { + background: none; + + a { + text-decoration: underline; + } + } + } + } + } + + &__footer { + padding: 1rem 1.5rem; + a { + padding: 0; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.875rem; + + &::after { + content: ""; + @extend %icon-logout; + width: 1rem; + height: 1rem; + } + } + } + + &--visible { + display: block; + } + } +} diff --git a/assets/sass/backoffice/_spinner.scss b/assets/sass/backoffice/_spinner.scss new file mode 100644 index 000000000..326b3260a --- /dev/null +++ b/assets/sass/backoffice/_spinner.scss @@ -0,0 +1,24 @@ +.spinner { + display: none; + &.show { + display: block; + } + + position: absolute; + margin: auto; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + border: 3px solid $secondary_2; + border-top: 3px solid $secondary_0; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/assets/sass/backoffice/_top_navigation.scss b/assets/sass/backoffice/_top_navigation.scss new file mode 100644 index 000000000..d78328d06 --- /dev/null +++ b/assets/sass/backoffice/_top_navigation.scss @@ -0,0 +1,59 @@ +.top-navigation { + height: 60px; + border-bottom: 2px solid $secondary_2; + + ul { + list-style-type: none; + padding: 0; + display: flex; + } + + &-btn { + height: 60px; + + @media (max-width: 768px) { + flex: 1; + } + + & > div { + height: 100%; + } + + a { + display: flex; + align-items: center; + color: rgba(37, 45, 64, 0.38); + font-size: 0.875rem; + font-weight: 600; + line-height: 140%; + padding: 0rem 1rem; + height: 100%; + border-bottom: 2px solid transparent; + + cursor: pointer; + + @media (max-width: 768px) { + text-align: center; + padding: 0 0.5rem; + justify-content: center; + } + + @media (max-width: 360px) { + padding: 0 0.125rem; + } + + &:hover { + background-color: $secondary_4; + border-color: $secondary_4; + color: $darkgray; + } + } + + &--active { + a { + color: $secondary_4; + border-color: $secondary_4; + } + } + } +} diff --git a/assets/sass/backoffice/_user_impersonation.scss b/assets/sass/backoffice/_user_impersonation.scss new file mode 100644 index 000000000..f2d3cdc54 --- /dev/null +++ b/assets/sass/backoffice/_user_impersonation.scss @@ -0,0 +1,95 @@ +.ng-user-impersonation-app { + .icon { + margin-right: 0.75rem; + width: 2rem; + height: 2rem; + + &.icon-small{ + width: 1rem; + height: 1rem; + } + } + + .btn-primary { + svg { + fill: $white; + } + } +} + +.ng-user-impersonation-content { + .container { + max-width: 100%; + } + + .users-filter { + h3 { + font-size: 1.25rem; + margin-bottom: 24px; + } + } + + form { + margin: 16px 0; + + > div.row { + flex-direction: column; + font-size: 0.875rem; + + label { + padding-bottom: 8px; + padding-top: 0; + width: auto; + } + + input.form-control, + select.form-select { + padding-top: 0.4rem; + padding-bottom: 0.4rem; + border-radius: 4px; + } + } + + div.col-xs-12 { + display: flex; + + a { + margin-right: 16px; + } + + @include media-breakpoint-down(md) { + flex-direction: column; + + a { + margin-bottom: 16px; + margin-right: 0; + } + } + } + } + + .users-list { + font-size: 0.875rem; + margin-top: 32px; + + .section-title { + .title { + font-size: 20px; + margin-bottom: 20px; + color: $secondary_4; + } + } + + .table { + tr { + td { + vertical-align: middle; + } + + ul { + margin-bottom: 0; + } + } + } + } +} diff --git a/assets/sass/backoffice/_user_profile.scss b/assets/sass/backoffice/_user_profile.scss new file mode 100644 index 000000000..66008f1f1 --- /dev/null +++ b/assets/sass/backoffice/_user_profile.scss @@ -0,0 +1,35 @@ +.user-profile { + padding: 1rem; + .user-profile-person-image { + width: 8.375rem; + height: 8.375rem; + margin-bottom: 1.25rem; + overflow: hidden; + position: relative; + img { + position: absolute; + object-fit: cover; + object-position: center; + width: 100%; + height: 100%; + } + } + .user-profile-person-details { + h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + margin-top: 1rem; + } + dl { + dt { + font-weight: 400; + font-size: 0.875rem; + color: rgba(37, 45, 64, 0.66); + } + dd { + font-weight: 600; + font-size: 0.875rem; + } + } + } +} \ No newline at end of file diff --git a/assets/sass/backoffice/_variables.scss b/assets/sass/backoffice/_variables.scss new file mode 100644 index 000000000..c0d2e7962 --- /dev/null +++ b/assets/sass/backoffice/_variables.scss @@ -0,0 +1,58 @@ +// dimensions +$impersonation-bar-height: 3rem; +$chat-header-height: 5rem; +$search-filter-max-width: 31.625rem; + +$gutter: .9375rem; + +$badge-green-bg: #219653; +$badge-green-text: #0E6C36; +$badge-red-bg: #F2994A; +$badge-red-text: #98561B; + +// colours +$primary_0: #FFCD00; +$primary_1: #346094; +$primary_2: #BBC6D6; +$primary: #FED82F; +$secondary: #F8F9FC; +$secondary_0: #00396F; +$secondary_1: #252D40; +$secondary_2: #F3F3F3; +$secondary_3: #F9F1DE; +$secondary_4: #A31F34; +$border: #E8E8E8; +$border-hover: #ACAFB6; +$light-bg: #FAFAFA; +$border_2: #F1F2F3; +$gray: #E5E5E5; +$gray-54: hsl(0, 0, 46); +$gray-64: hsl(0, 0, 36); +$message: #2970EF; +$success-green: #4BC17C; +$darkgray: rgba(37, 45, 64, 0.66); +$bordeaux: #8B1A2C; +$olive: #256540; +$olive-dark: #663939; +$accept: #BDF4D4; +$decline: #FFC6C6; +$placeholder: #757575; +$event: #98561B; +$event-bg: #F2994A; +$flash-ntf: #DBF3E5; +$flash-ntf-text: #187B42; +$red: #D93B3B; +$highlight: #FBF1C8; +$black: hsl(0, 0, 0); +$orange: #ffa500; +$white: #fffffF; + +$footer-bg: $black; + +//filter colours +$filtered-colours: ( + primary: brightness(0) saturate(100%) invert(87%) sepia(98%) saturate(2775%) hue-rotate(328deg) brightness(102%) contrast(99%), + secondary: brightness(0) saturate(100%) invert(100%) sepia(26%) saturate(567%) hue-rotate(178deg) brightness(100%) contrast(98%), + white: brightness(0) saturate(100%) invert(92%) sepia(93%) saturate(32%) hue-rotate(251deg) brightness(107%) contrast(100%), + black: brightness(0) saturate(100%) invert(0%) sepia(9%) saturate(7500%) hue-rotate(154deg) brightness(91%) contrast(108%), +); diff --git a/assets/sass/backoffice/style.scss b/assets/sass/backoffice/style.scss new file mode 100644 index 000000000..c443ab81e --- /dev/null +++ b/assets/sass/backoffice/style.scss @@ -0,0 +1,39 @@ +@charset "UTF-8"; +// IMPORTANT: only add imports to this file + +// External libraries +@import '@fortawesome/fontawesome-free/css/all'; + +@import 'variables'; + +// Bootstrap setup +@import 'bootstrap/scss/functions'; +@import '../bootstrap_import/bootstrap_variables'; +@import '../bootstrap_import/bootstrap'; + +@import "flatpickr"; + +// Mixins +@import '../mixins/mixins'; +@import '../typography'; + +@import '../bootstrap_import/bootstrap_override'; + +@import 'icons'; +@import 'buttons'; +@import 'global'; +@import 'header'; +@import 'dashboard'; +@import 'sidebar'; +@import 'main'; +@import 'modal'; +@import 'top_navigation'; +@import 'spinner'; +@import 'my_account'; +@import 'user_profile'; +@import 'global_styles'; +@import 'filter_form'; +@import 'listing'; +@import 'user_impersonation'; +@import 'print'; + diff --git a/assets/sass/layout/_navigation.scss b/assets/sass/layout/_navigation.scss index cd73948a3..c068f095b 100644 --- a/assets/sass/layout/_navigation.scss +++ b/assets/sass/layout/_navigation.scss @@ -1,43 +1,109 @@ .main-navigation { flex: 1; + position: relative; + margin-left: 2rem; + + @include media-breakpoint-down(xl) { + margin-left: 0; + } + .navbar-nav { display: flex; flex-direction: row; - justify-content: center; list-style-type: none; margin: 0; padding: 0 1rem; - text-align: center; > li { + display: flex; + align-items: center; + padding: 0 1.3333333333em; position: relative; + cursor: pointer; + @extend %hover-underline; + + @media (max-width: 1300px) { + padding: 0 1rem; + } + + @include media-breakpoint-down(xl) { + padding: 0; + } + > a, > span { - @extend %hover-underline; - @include text-link; + @include text-1_125; + font-weight: 600; display: inline-block; - padding: 0 1.3333333333em; + + @include media-breakpoint-down(sm) { + font-size: 1rem; + } + } + + &.submenu-active { + .menu_level_1 { + display: block; + } + .submenu-trigger { + transform: rotate(180deg); + } } } } /* main submenu */ .menu_level_1 { list-style-type: none; - padding: 1rem 0; + padding: 0.875rem 0; margin: 0; display: none; + cursor: auto; + + li { + width: 100%; + + &:not([id*="menu-item"]) { + display: flex; + flex-direction: column; + + > span { + @include text-0_875; + border-top: 2px solid rgba($black, 0.1); + margin-top: 0.75rem; + color: $darkgray; + } + } + } + a { display: block; - padding: .5em 1.5em; + padding: 0.625rem 2rem; + width: max-content; + min-width: 100%; + } + + span { + &:not(.user-icon) { + display: inline-block; + padding: 1.25rem 0 0.75rem; + margin: 0 2rem; + } } } + + .menu_level_2 { + list-style-type: none; + padding: 0; + } + /* large screen sizes */ - @include media-breakpoint-up(lg) { + @include media-breakpoint-up(xl) { + display: flex; + .navbar-nav { > li:hover, .active { > a, > span { - color: $black; &::after { transform: scaleY(1); } @@ -63,7 +129,6 @@ position: absolute; top: 100%; left: 0; - width: 16rem; z-index: 20; background: $primary; box-shadow: 0 .5rem 1rem hsla(0, 0, 0, .12); @@ -75,25 +140,60 @@ } } } + .submenu-trigger { - // NOTE: Removed fontawesome extend rule since the nav will we rewritten - display: none; + margin-left: 1.375rem; + line-height: 0; + + @media (max-width: 1300px) { + margin-left: 0.75rem; + } + + &::before { + content: url('../assets/icons/icon_arrow_down.svg'); + + .inverted & { + @include media-breakpoint-up(xl) { + @include icon-color(secondary_1); + } + } + } } /* small screen sizes */ - @include media-breakpoint-down(lg) { - display: none; + @include media-breakpoint-down(xl) { + transform: translateX(100%); + pointer-events: none; + transition: transform 300ms; position: absolute; - left: calc(var(--bs-gutter-x) * .5); - right: calc(var(--bs-gutter-x) * .5); - top: $header-height-sm; + display: flex; + flex-direction: column; + height: calc(100vh - #{$header-height-sm}); + overflow-y: scroll; + left: 0; + right: 0; + top: calc(#{$header-height-sm} + 2px); z-index: 1000; - background: $primary; + background: $secondary_0; box-shadow: 0 .5rem 1rem hsla(0, 0, 0, .12); + padding-bottom: 1.5rem; .navbar-nav { display: block; margin: 0; - padding: 1rem 0; + padding: 0; > li { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 0.75rem 1.5rem; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + > a, > span { padding: .5em 1em; @@ -107,20 +207,15 @@ margin: 0 1.875rem; } } - &.submenu-active { - .menu_level_1 { - display: block; - } - .submenu-trigger { - &::before { - transform: rotate(180deg); - } - } + + &::after { + display: none; } } } .mainnav-active & { - display: block; + transform: translateX(0); + pointer-events: all; } .submenu-trigger { display: inline-block; @@ -128,31 +223,41 @@ right: 0; top: 0; cursor: pointer; - width: 1.875rem; - height: 1.875rem; line-height: 1.875rem; @include text-0_875; &::before { display: block; - content: '\f078'; } } .menu_level_1 { - padding: 0 0 1rem; + width: 100%; + padding: 0; + background-color: $primary_0; + margin-top: 0.5rem; + border-radius: 2px; + a { - color: $gray-54; - @include text-0_875; + @include text-1_125; + font-weight: 600; + padding-inline: 1.5rem; + + @include media-breakpoint-down(sm) { + font-size: 1rem; + line-height: 1.15; + } + } + + span { + &:not(.user-icon) { + margin: 0 1.5rem; + } + } + + @include media-breakpoint-down(sm) { + padding: 0.5rem 0; } } } - @include media-breakpoint-down(lg) { - overflow: scroll; - position: absolute; - z-index: 1000; - left: 0; - right: 0; - height: calc(100vh - #{$header-height-sm}); - } @include media-breakpoint-down(xs) { left: 0; right: 0; @@ -160,7 +265,6 @@ } .mainnav-toggle { - @extend %hover-underline; display: none; position: relative; width: $header-height-sm; @@ -196,7 +300,6 @@ color: inherit; } .mainnav-active & { - background: $primary; .hamburger { background: transparent; transition: background 100ms ease-out; @@ -208,21 +311,218 @@ } } } - @include media-breakpoint-down(lg) { + + .inverted & { + @include media-breakpoint-up(xl) { + .hamburger { + color: $secondary_1; + } + } + } + + @include media-breakpoint-down(xl) { display: block; + margin-right: -1.5rem; } } +.user-menu { + display: flex; + border-left: 2px solid rgba(249, 241, 222, 0.08); + flex-direction: row; + position: relative; -.mainnav-active { - @include media-breakpoint-down(lg) { - overflow-y: hidden; - position: fixed; - width: 100%; - body { - overflow-y: hidden; - position: fixed; - width: 100%; + @include media-breakpoint-down(xl) { + flex-direction: column; + border-left: none; + border-top: 2px solid rgba(0, 0, 0, 0.08); + margin: 1.5rem; + padding: 1.5rem 0; + } + + &.submenu-active { + .user-dropdown-menu { + display: flex; + } + } + + .submenu-trigger { + display: none; + } + + .user-menu-cta { + display: flex; + align-items: center; + position: relative; + //color: $secondary_3; + @include text-base; + font-weight: 600; + padding-inline: 2rem; + @extend %hover-underline; + + @include media-breakpoint-down(xl) { + padding-inline: 0; + } + + &.prevent-user-menu-cta { + @include media-breakpoint-down(xl) { + display: none; + } + } + + .arrow-icon { + &::after { + content: url('../assets/icons/icon_arrow_right_small.svg'); + @include icon-color(secondary_3); + margin-left: 0.625rem; + + .inverted & { + @include media-breakpoint-up(xl) { + @include icon-color(secondary_1); + } + } + } + + } + + .inverted & { + @include media-breakpoint-up(xl) { + color: $secondary_1; + } + } + + .user-icon { + margin: 0 0.5rem 0 0; + width: 1.875rem; + height: 1.875rem; + background-color: $secondary_3; + font-size: 0; + } + } + + .user-dropdown-menu { + display: none; + position: absolute; + top: 100%; + right: 0; + left: revert; + flex-direction: column; + align-items: center; + padding: 2rem 1.5rem 2.5rem; + border: none; + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.1); + width: max-content; + + @include media-breakpoint-down(xl) { + display: flex; + position: relative; + top: revert; + right: revert; + box-shadow: none; + align-items: flex-start; + padding: 0; + } + + &.menu_level_1 { + @include media-breakpoint-down(xl) { + background-color: revert; + margin-top: 0; + } + } + } + + .user-icon { + width: 3.5rem; + height: 3.5rem; + border-radius: 50%; + background-color: $primary_2; + display: flex; + justify-content: center; + object-fit: cover; + align-items: center; + margin-bottom: 24px; + + &--main { + @include media-breakpoint-down(xl) { + display: none; + } + } + + &::before { + content: url('../assets/icons/icon_user.svg'); + } + } + + .user-menu-title { + @include text-1_25; + font-weight: 600; + margin-bottom: 1rem; + + @include media-breakpoint-down(xl) { + display: none; + } + } + + a.btn-user { + @include text-1_125; + font-weight: 600; + color: $secondary_4; + display: flex; + align-items: center; + + @include media-breakpoint-down(xl) { + justify-content: flex-start; + font-size: 1rem; + font-weight: 600; + color: $secondary_3; + } + + &::after { + content: url('../assets/icons/icon_caret_right.svg'); + margin-left: 0.625rem; + width: 1.5625rem; + height: 1.5625rem; + @include icon-color(secondary_4); + + @include media-breakpoint-down(xl) { + @include icon-color(secondary_3); + } + } + + & + .btn-user { + margin-top: 24px; + } + + &:not(.btn.btn-link) { + @include media-breakpoint-down(xl) { + width: fit-content; + padding: 0; + } + } + + .user-icon { + display: none; + + @include media-breakpoint-down(xl) { + display: inline-flex; + width: 2rem; + height: 2rem; + margin: 0 0.5rem 0 0; + background-color: $white; + } + + &::before { + @include media-breakpoint-down(xl) { + transform: scale(0.9); + } + } + } + + &-register { + @include media-breakpoint-down(xl) { + display: none; + pointer-events: none; + } } } } diff --git a/assets/sass/layout/_print.scss b/assets/sass/layout/_print.scss new file mode 100644 index 000000000..d58c8957c --- /dev/null +++ b/assets/sass/layout/_print.scss @@ -0,0 +1,247 @@ +.print-icon { + // position: fixed; + // bottom: 40%; + // right: 0; + width: 2.5rem; + height: 2.5rem; + display: flex; + justify-content: center; + align-items: center; + background-color: $secondary_0; + color: $white; + cursor: pointer; + transform: translateX(0); + transition: transform 300ms ease-in; + &:hover, &:focus { + opacity: 0.8; + color: $white; + } + &.hide { + transform: translateX(100%); + } + + @include media-breakpoint-down(sm) { + display: none !important; + } +} + +@media print { + .site-header { + display: none !important; + } + .mainnav-toggle, + .nav.navbar-nav { + display: none !important; + } + .breadcrumb-wrapper { + display: none !important; + } + footer { + display: none !important; + } + + .print-icon { + display: none !important; + } + + div[class^="video"] { + display: none !important; + } + + html, + body { + font-size: 10px !important; + width: 210mm !important; + margin: 0px; + background-color: transparent; + } + + #page { + padding-top: 0; + } + + .full-page-image { + transform: translate(0) !important; + } + + .btn:not(.btn-download-link) { + display: none; + } + + .list-group-item{ + border: none; + border-radius: 0; + } + + .full-page-related-grid { + break-inside: avoid; + } + + .ngl-block[class*="bg-color-gray"], + .ngl-block.bg-color-primary, + .ngl-block.bg-color-secondary { + background-color: transparent; + color: revert; + &:before, + &:after { + content: none; + } + } + + .zone.zone-main > .ngl-block:not(.ngl-full_view):not(.ngl-vt-slider), + .zone.zone-post_header > .ngl-block:not(.ngl-full_view):not(.ngl-vt-slider), + .scroll-animation-trigger .full-page-body > .ezxmltext-field > *, + .scroll-animation-trigger .animate-full-page-header { + opacity: 1 !important; + transform: translate(0px, 0px) !important; + background-color: $white !important; + } + + + .view-type-standard.ng-article.vl1 { + opacity: 1 !important; + transform: translate(0px, 0px) !important; + } + + + .ngl-list { + &:not(&.ngl-vt-grid_uneven) { + .col-sm-6 { + flex: 0 0 33.33333% !important; + max-width: 33.33333% !important; + } + + } + } + + .ng-video .article-icon { + background: transparent !important; + } + + figure, .image { + break-inside: avoid; + } + + .break-before { + padding-top: 10px; + break-before: page; + } + + .break-after { + break-after: page; + padding-bottom: 10px; + } + + .break-inside-avoid { + break-inside: avoid; + } + + .print-hidden { + display: none; + } + + .scroll-animation-trigger { + position: static !important; + } + + .container-wide, + .container, + .container-narrow { + max-width: 100%; + padding-left: var(--header-gutter) !important; + padding-right: var(--header-gutter) !important; + } + + .table-bordered th, .table-bordered td { + border: none !important; + } + + .vl9 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + margin-bottom: 1rem; + + &.even { + .article-content { + order: -1; + } + } + } + + .vl10 { + background-color: transparent; + padding: 0; + } + + .vl11 { + display: grid; + + &.view-type-zigzag_blocks { + grid-template-columns: 1fr 1fr; + } + + .article-content { + background-color: transparent; + } + } + + .ngl-block.bg-color-gray_with_logo_left { + &::before { + display: none; + } + } + + .page-floating-widgets { + display: none; + } + + .social-share-wrapper { + display: none; + } + + .layouts-filter-block-form { + display: grid; + grid-template-columns: 1fr 1fr; + max-width: 50%; + } + + .ngl-vt-sushi_bar { + .swiper-slide { + opacity: 1; + } + + .swiper-wrapper { + flex-direction: column; + } + } + + .collapse:not(.show) { + display: block; + } + + .tab-content > .tab-pane { + display: block; + opacity: 1; + } + + .full-page-accordions { + background: transparent; + } + + .row { + --bs-gutter-x: 0; + } + + @page { + size: A4; + width: 210mm; + height: 297mm; + padding: 1cm; + margin: 25mm 25mm 25mm 25mm; + border: 1px #D3D3D3 solid; + border-radius: 0px; + background: white; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + } +} diff --git a/assets/sass/style.scss b/assets/sass/style.scss index 6f526b014..b885dfe82 100644 --- a/assets/sass/style.scss +++ b/assets/sass/style.scss @@ -98,3 +98,15 @@ // Accessibility @import 'accessibility'; + +// Page floating widgets +@import 'page_floating_widgets'; + +// print +@import 'layout/print'; + +// Subscription widget +@import 'subscription_widget'; + +// User impersonation +@import 'user_impersonation'; diff --git a/captainhook.json b/captainhook.json new file mode 120000 index 000000000..90b3edb11 --- /dev/null +++ b/captainhook.json @@ -0,0 +1 @@ +captainhook.template.json \ No newline at end of file diff --git a/captainhook.template.json b/captainhook.template.json index 3794c1c75..30f5cfe6e 100644 --- a/captainhook.template.json +++ b/captainhook.template.json @@ -87,6 +87,6 @@ "actions": [] }, "config": { - "php-path": "/usr/bin/env php8.1" + "php-path": "/usr/bin/env php8.2" } } diff --git a/composer.json b/composer.json index 3c21538cf..50e452a4f 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,10 @@ "netgen/layouts-ibexa": "~1.4.0", "netgen/layouts-ibexa-site-api": "~1.4.0", "netgen/layouts-ibexa-relation-list-query": "^1.4", - "netgen/layouts-ibexa-tags-query": "^1.4" + "netgen/layouts-ibexa-tags-query": "^1.4", + "netgen/conversations": "^1.0", + "netgen/notifications": "^1.0", + "netgen/ibexa-user-impersonation": "dev-NGSTACK-853" }, "require-dev": { "symfony/debug-bundle": "5.4.*", @@ -74,6 +77,9 @@ "symfony/css-selector": "5.4.*", "symfony/phpunit-bridge": "5.4.*" }, + "repositories": [ + { "type": "composer", "url": "https://packagist.netgen.biz", "canonical": false } + ], "config": { "allow-plugins": { "composer/package-versions-deprecated": true, @@ -144,5 +150,7 @@ "branch-alias": { "dev-master": "3.2.x-dev" } - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/config/app/app.yaml b/config/app/app.yaml index 28c57bf4c..3178babce 100644 --- a/config/app/app.yaml +++ b/config/app/app.yaml @@ -13,4 +13,6 @@ parameters: ngsite.default.lazy_loading.enabled: true + ngsite.default.token_validity.email_change: 900 + app.testing.site_domain: '%env(TEST_DOMAIN)%' diff --git a/config/app/doctrine/backoffice/Bookmark.orm.xml b/config/app/doctrine/backoffice/Bookmark.orm.xml new file mode 100644 index 000000000..1d99bf0a8 --- /dev/null +++ b/config/app/doctrine/backoffice/Bookmark.orm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/app/doctrine/backoffice/SecurityToken.orm.xml b/config/app/doctrine/backoffice/SecurityToken.orm.xml new file mode 100644 index 000000000..bd3a34256 --- /dev/null +++ b/config/app/doctrine/backoffice/SecurityToken.orm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/config/app/packages/ibexa_siteaccess.yaml b/config/app/packages/ibexa_siteaccess.yaml index 4792ab925..b76613f43 100644 --- a/config/app/packages/ibexa_siteaccess.yaml +++ b/config/app/packages/ibexa_siteaccess.yaml @@ -8,6 +8,7 @@ ibexa: - fh_eng - bold_eng - bold_ger + - peak_eng - '%ngsite.admin_siteaccess_name%' groups: admin_group: @@ -16,11 +17,17 @@ ibexa: - fh_eng - bold_eng - bold_ger + - peak_eng fh_group: - fh_eng bold_group: - bold_eng - bold_ger + peak_group: + - peak_eng + impersonation_group: + - peak_eng + - '%ngsite.admin_siteaccess_name%' system: admin_group: design: ngadmin @@ -48,6 +55,15 @@ ibexa: location_id: '%ngsite.bold_group.locations.tree_root.id%' session: name: eZSESSID + peak_group: + design: peak + translation_siteaccesses: + - peak_eng + content: + tree_root: + location_id: '%ngsite.peak_group.locations.tree_root.id%' + session: + name: eZSESSID fh_eng: languages: - eng-GB @@ -57,6 +73,9 @@ ibexa: bold_ger: languages: - ger-DE + peak_eng: + languages: + - eng-GB netgen_layouts: design_list: @@ -68,6 +87,11 @@ netgen_layouts: ibexa_design_engine: design_list: + peak: + - peak + - app + - common + - standard bold: - bold - app diff --git a/config/app/packages/netgen_ibexa_user_impersonation.yaml b/config/app/packages/netgen_ibexa_user_impersonation.yaml new file mode 100644 index 000000000..53bb7c7e6 --- /dev/null +++ b/config/app/packages/netgen_ibexa_user_impersonation.yaml @@ -0,0 +1,8 @@ +parameters: + netgen_user_impersonation.default.content_type_identifier.user_groups: ['user_group'] + netgen_user_impersonation.default.content_type_identifier.users: ['user'] + netgen_user_impersonation.default.users.root_location_ids: [5] + netgen_user_impersonation.default.users.name_search_fields: ['first_name', 'last_name'] + netgen_user_impersonation.admin.siteaccess_name: '%ngsite.admin_siteaccess_name%' + netgen_user_impersonation.default.admin.pagelayout: 'backoffice/user_impersonation/layout.html.twig' + netgen_user_impersonation.peak_eng.impersonate.default_target.route: 'backoffice_dashboard_index' diff --git a/config/app/packages/novactive_ez_seo.yaml b/config/app/packages/novactive_ez_seo.yaml index 0b7e07318..a8c1c2bdf 100644 --- a/config/app/packages/novactive_ez_seo.yaml +++ b/config/app/packages/novactive_ez_seo.yaml @@ -1,5 +1,38 @@ nova_ezseo: system: + peak_group: + default_metas: + author: "Netgen" + copyright: ~ + generator: "Ibexa DXP" + MSSmartTagsPreventParsing: "TRUE" + fieldtype_metas_identifier: "metas" + fieldtype_metas: + title: + label: 'Title' + default_pattern: "" + description: + label: 'Description' + default_pattern: "" + maxLength: 150 + keywords: + label: 'Keywords' + default_pattern: ~ + robots: + sitemap: + - url: "https://%ngsite.peak_group.site_domain%/sitemap.xml" + sitemap_includes: + subtrees: + - "%ngsite.bold_group.locations.tree_root.id%" + contentTypeIdentifiers: + - ng_article + - ng audio + - ng_blog_post + - ng_category + - ng_frontpage + - ng_gallery + - ng_job_position + - ng_news bold_group: default_metas: author: "Netgen" diff --git a/config/app/routes.yaml b/config/app/routes.yaml index e69de29bb..d69a0590a 100644 --- a/config/app/routes.yaml +++ b/config/app/routes.yaml @@ -0,0 +1,13 @@ +ngsite_api_bookmarks_create: + path: /api/bookmarks/create/{locationId} + controller: App\Controller\API\Bookmarks\Create + methods: [POST] + requirements: + locationId: \d+ + +ngsite_api_bookmarks_delete: + path: /api/bookmarks/delete/{locationId} + controller: App\Controller\API\Bookmarks\Delete + methods: [POST] + requirements: + locationId: \d+ diff --git a/config/app/routes/backoffice.yaml b/config/app/routes/backoffice.yaml new file mode 100644 index 000000000..c87232689 --- /dev/null +++ b/config/app/routes/backoffice.yaml @@ -0,0 +1,6 @@ +backoffice: + resource: backoffice/* + prefix: /backoffice + trailing_slash_on_root: false + defaults: + allowed_siteaccess: peak_eng diff --git a/config/app/routes/backoffice/bookmarks.yaml b/config/app/routes/backoffice/bookmarks.yaml new file mode 100644 index 000000000..59b3fc270 --- /dev/null +++ b/config/app/routes/backoffice/bookmarks.yaml @@ -0,0 +1,11 @@ +backoffice_bookmarks_index: + path: /bookmarks + controller: App\Backoffice\Controller\Bookmarks\Index + methods: [GET] + +backoffice_bookmarks_delete: + path: /bookmarks/delete/{id} + controller: App\Backoffice\Controller\Bookmarks\Delete + methods: [GET, POST] + requirements: + id: \d+ diff --git a/config/app/routes/backoffice/index.yaml b/config/app/routes/backoffice/index.yaml new file mode 100644 index 000000000..e5bcd77e8 --- /dev/null +++ b/config/app/routes/backoffice/index.yaml @@ -0,0 +1,12 @@ +backoffice_index: + path: / + controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' + methods: [GET] + defaults: + route: backoffice_dashboard_index + permanent: true + +backoffice_dashboard_index: + path: /dashboard + controller: App\Backoffice\Controller\Dashboard\Index + methods: [GET] diff --git a/config/app/routes/backoffice/my_account.yaml b/config/app/routes/backoffice/my_account.yaml new file mode 100644 index 000000000..9cd9e1876 --- /dev/null +++ b/config/app/routes/backoffice/my_account.yaml @@ -0,0 +1,22 @@ +backoffice_my_account_index: + path: /backoffice/my_account + controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' + methods: [GET] + defaults: + route: backoffice_my_account_personal_details + permanent: true + +backoffice_my_account_personal_details: + path: /backoffice/my_account/personal_details + controller: App\Backoffice\Controller\MyAccount\PersonalDetails + methods: [GET, POST] + +backoffice_my_account_user_credentials: + path: /backoffice/my_account/user_credentials + controller: App\Backoffice\Controller\MyAccount\UserCredentials + methods: [GET, POST] + +backoffice_my_account_change_email_confirmation: + path: /backoffice/my_account/change_email_confirmation/{token} + controller: App\Backoffice\Controller\MyAccount\ChangeEmailConfirmation + methods: [GET] diff --git a/config/app/server/dev/app.yaml b/config/app/server/dev/app.yaml index a428577a1..ee768fa46 100644 --- a/config/app/server/dev/app.yaml +++ b/config/app/server/dev/app.yaml @@ -1,12 +1,15 @@ parameters: ngsite.fh_group.site_domain: localhost ngsite.bold_group.site_domain: localhost + ngsite.peak_group.site_domain: localhost ngsite.fh_group.locations.site_info.id: 65 ngsite.bold_group.locations.site_info.id: 442 + ngsite.peak_group.locations.site_info.id: 561 ngsite.fh_group.locations.tree_root.id: 385 ngsite.bold_group.locations.tree_root.id: 386 + ngsite.peak_group.locations.tree_root.id: 555 ngsite.default.locations.ng_component_hero.id: 407 ngsite.default.locations.ng_component_quote.id: 445 @@ -21,3 +24,16 @@ parameters: # This is Netgen Stack Demo GTM code: GTM-WMG2VT2 # Replace it with project specific GTM code before going live ngsite.default.site_settings.google_tag_manager_code: 'GTM-WMG2VT2' + + ngsite.default.pager_limit: 25 + + ngsite.default.bookmarks.content_types: + - ng_article + - ng_category + - ng_job_position + - ng_landing_page + - ng_news + + netgen_user_impersonation.default.excluded_user_groups: + - 13 # administrator users + - 44 # anonymous users diff --git a/config/bundles.php b/config/bundles.php index 7dcc19363..c274408d3 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -82,4 +82,9 @@ Netgen\IbexaFieldTypeEnhancedLinkBundle\NetgenIbexaFieldTypeEnhancedLinkBundle::class => ['all' => true], Netgen\Bundle\BetterIbexaAdminUIBundle\NetgenBetterIbexaAdminUIBundle::class => ['all' => true], Netgen\Bundle\ToolbarBundle\NetgenToolbarBundle::class => ['all' => true], + Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true], + ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], + Netgen\Bundle\ConversationsBundle\NetgenConversationsBundle::class => ['all' => true], + Netgen\Bundle\NotificationsBundle\NetgenNotificationsBundle::class => ['all' => true], + Netgen\IbexaUserImpersonationBundle\NetgenIbexaUserImpersonationBundle::class => ['all' => true], ]; diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml new file mode 100644 index 000000000..2eb7b4c58 --- /dev/null +++ b/config/packages/api_platform.yaml @@ -0,0 +1,7 @@ +api_platform: + mapping: + paths: ['%kernel.project_dir%/src/Entity'] + patch_formats: + json: ['application/merge-patch+json'] + swagger: + versions: [3] diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 6f62bef29..342b1a1de 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -16,6 +16,12 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App + App\Backoffice: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/config/app/doctrine/backoffice' + prefix: 'App\Backoffice\Doctrine\Entity' + alias: Backoffice when@test: doctrine: diff --git a/config/packages/netgen_notifications.yaml b/config/packages/netgen_notifications.yaml new file mode 100644 index 000000000..e1d214f54 --- /dev/null +++ b/config/packages/netgen_notifications.yaml @@ -0,0 +1,23 @@ +netgen_notifications: + categories: + default: + subscription: + enabled: true + resolver: netgen_notifications.ibexa.subtree_update_resolver + digest_actions: + email: + type: digest_email + parameters: + from: '%env(MAIL_SENDER)%' + subject: 'Here is what you missed' + template: backoffice/notifications/digest.html.twig + actions: + email: + type: email + parameters: + from: '%env(MAIL_SENDER)%' + template: backoffice/notifications/notification.html.twig + category_mappings: + ibexa: + subtree: + default: [167] diff --git a/config/packages/ramsey_uuid_doctrine.yaml b/config/packages/ramsey_uuid_doctrine.yaml new file mode 100644 index 000000000..cfc3036f9 --- /dev/null +++ b/config/packages/ramsey_uuid_doctrine.yaml @@ -0,0 +1,4 @@ +doctrine: + dbal: + types: + uuid: 'Ramsey\Uuid\Doctrine\UuidType' diff --git a/config/packages/vich_uploader.yaml b/config/packages/vich_uploader.yaml new file mode 100644 index 000000000..32c852e05 --- /dev/null +++ b/config/packages/vich_uploader.yaml @@ -0,0 +1,8 @@ +vich_uploader: + db_driver: orm + + #mappings: + # products: + # uri_prefix: /images/products + # upload_destination: '%kernel.project_dir%/public/images/products' + # namer: Vich\UploaderBundle\Naming\SmartUniqueNamer diff --git a/config/routes/api_platform.yaml b/config/routes/api_platform.yaml new file mode 100644 index 000000000..38f11cba8 --- /dev/null +++ b/config/routes/api_platform.yaml @@ -0,0 +1,4 @@ +api_platform: + resource: . + type: api_platform + prefix: /api diff --git a/config/routes/netgen_conversations.yaml b/config/routes/netgen_conversations.yaml new file mode 100644 index 000000000..8ef2c4def --- /dev/null +++ b/config/routes/netgen_conversations.yaml @@ -0,0 +1,4 @@ +netgen_conversations: + resource: '@NetgenConversationsBundle/Resources/config/routing.yaml' + defaults: + ngconversations_pagelayout: 'backoffice/conversations/layout.html.twig' diff --git a/config/routes/netgen_ibexa_user_impersonation.yaml b/config/routes/netgen_ibexa_user_impersonation.yaml new file mode 100644 index 000000000..3670e7485 --- /dev/null +++ b/config/routes/netgen_ibexa_user_impersonation.yaml @@ -0,0 +1,3 @@ +netgen_ibexa_user_impersonation: + resource: "@NetgenIbexaUserImpersonationBundle/Resources/config/routing.yaml" + prefix: /user-impersonation diff --git a/config/routes/netgen_notifications.yaml b/config/routes/netgen_notifications.yaml new file mode 100644 index 000000000..6de1f186d --- /dev/null +++ b/config/routes/netgen_notifications.yaml @@ -0,0 +1,5 @@ +netgen_notifications: + resource: '@NetgenNotificationsBundle/Resources/config/routing.yaml' + prefix: '%netgen_notifications.route_prefix%' + defaults: + ngnotifications_pagelayout: 'backoffice/notifications/layout.html.twig' diff --git a/package.json b/package.json index bf4a0ab5b..95d478749 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,11 @@ "webpack-notifier": "^1.6.0" }, "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.2", "@netgen/javascript-cookie-control": "^0.0.8", "@popperjs/core": "^2.11.6", - "bootstrap": "~5.2.1", + "bootstrap": "^5.3.3", + "flatpickr": "^4.6.13", "js-cookie": "^3.0.5", "photoswipe": "^5.3.4", "popper.js": "^1.15.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8daa2f2e3..104b19f70 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,3 +3,5 @@ parameters: - '#Access to an undefined property Ibexa\\Contracts\\Core\\FieldType\\Value::\$\w+#' - '#Cannot access property \$\w+ on Ibexa\\Contracts\\Core\\FieldType\\Value\|null#' - '#Cannot access property \$value on Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Field\|null#' + # Disable errors for various "only read/written" properties + - '#\$\w+ is never (written|read), only (read|written).#' diff --git a/src/Attribute/AsMenuBuilder.php b/src/Attribute/AsMenuBuilder.php new file mode 100644 index 000000000..4c4f80e16 --- /dev/null +++ b/src/Attribute/AsMenuBuilder.php @@ -0,0 +1,19 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + $form = $this->createFormBuilder()->getForm(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->repository->remove($bookmark, true); + + return $this->redirectToRoute('backoffice_bookmarks_index'); + } + + return $this->render( + 'backoffice/bookmarks/delete.html.twig', + [ + 'form' => $form->createView(), + 'bookmark' => $bookmark, + ], + ); + } +} diff --git a/src/Backoffice/Controller/Bookmarks/Index.php b/src/Backoffice/Controller/Bookmarks/Index.php new file mode 100644 index 000000000..ea0d83b31 --- /dev/null +++ b/src/Backoffice/Controller/Bookmarks/Index.php @@ -0,0 +1,79 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + $filterForm = $this->createForm( + FilterType::class, + null, + [ + 'translation_prefix' => 'bookmarks', + 'show_date' => true, + 'show_search' => true, + 'selections' => [ + 'type' => [ + 'options' => new ContentType($this->contentTypeService, $this->configResolver), + ], + ], + ], + ); + + $filterForm->handleRequest($request); + + /** @var \Ibexa\Core\MVC\Symfony\Security\User $sfUser */ + $sfUser = $this->getUser(); + + $adapter = new BookmarksAdapter( + $this->repository, + $sfUser->getAPIUser()->id, + $filterForm->get('selections')->get('type')->getData(), + $filterForm->get('searchText')->getData(), + $filterForm->get('date')->get('dateFrom')->getData(), + $filterForm->get('date')->get('dateTo')->getData(), + ); + + $bookmarks = new Pagerfanta($adapter); + $bookmarks->setNormalizeOutOfRangePages(true); + $bookmarks->setMaxPerPage($this->getConfigResolver()->getParameter('pager_limit', 'ngsite')); + + $page = abs($request->query->getInt('page', 1)); + $bookmarks->setCurrentPage($page > 0 ? $page : 1); + + return $this->render( + 'backoffice/bookmarks/index.html.twig', + [ + 'filter_form' => $filterForm->createView(), + 'bookmarks' => $bookmarks, + ], + ); + } +} diff --git a/src/Backoffice/Controller/Dashboard/Index.php b/src/Backoffice/Controller/Dashboard/Index.php new file mode 100644 index 000000000..be5528751 --- /dev/null +++ b/src/Backoffice/Controller/Dashboard/Index.php @@ -0,0 +1,18 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + return $this->render('backoffice/dashboard/index.html.twig'); + } +} diff --git a/src/Backoffice/Controller/MyAccount/ChangeEmailConfirmation.php b/src/Backoffice/Controller/MyAccount/ChangeEmailConfirmation.php new file mode 100644 index 000000000..23820111a --- /dev/null +++ b/src/Backoffice/Controller/MyAccount/ChangeEmailConfirmation.php @@ -0,0 +1,69 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + /** @var \Ibexa\Core\MVC\Symfony\Security\User $sfUser */ + $sfUser = $this->getUser(); + $user = $sfUser->getAPIUser(); + + $changeEmailToken = $this->tokenRepository->findOneBy( + [ + 'userId' => $user->id, + 'token' => $token, + ], + ); + + if ($changeEmailToken === null) { + throw new BadRequestHttpException('Request for e-mail change with given token does not exist.'); + } + + if ($changeEmailToken->isUsed()) { + $this->addFlash('notice', 'my_account.change_email.already_activated'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + if ($changeEmailToken->getExpiryDate() < new DateTimeImmutable()) { + $this->addFlash('notice', 'my_account.change_email.token_expired'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + $userUpdateStruct = $this->userService->newUserUpdateStruct(); + $userUpdateStruct->email = $changeEmailToken->getData(); + $this->repository->sudo( + fn (): User => $this->userService->updateUser($user, $userUpdateStruct), + ); + + $changeEmailToken->setIsUsed(true); + $this->tokenRepository->save($changeEmailToken, true); + + $this->addFlash('success', 'my_account.change_email.successfully_changed'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } +} diff --git a/src/Backoffice/Controller/MyAccount/PersonalDetails.php b/src/Backoffice/Controller/MyAccount/PersonalDetails.php new file mode 100644 index 000000000..d9458b0cf --- /dev/null +++ b/src/Backoffice/Controller/MyAccount/PersonalDetails.php @@ -0,0 +1,105 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + /** @var \Ibexa\Core\MVC\Symfony\Security\User $sfUser */ + $sfUser = $this->getUser(); + $user = $sfUser->getAPIUser(); + + $form = $this->createForm( + PersonalDetailsType::class, + $user, + ); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /** @var \App\Backoffice\Form\MyAccount\PersonalDetailsData $formData */ + $formData = $form->getData(); + + $updateStruct = $this->userService->newUserUpdateStruct(); + + $updateStruct->contentUpdateStruct = $this->contentService->newContentUpdateStruct(); + + if ($formData->removeImage) { + $updateStruct->contentUpdateStruct->setField('image', null); + } elseif ($formData->image !== null) { + $updateStruct->contentUpdateStruct->setField('image', $formData->image); + } + + $updateStruct->contentUpdateStruct->setField('first_name', $formData->firstName); + $updateStruct->contentUpdateStruct->setField('last_name', $formData->lastName); + + $errors = []; + + try { + $this->repository->sudo( + fn (): User => $this->userService->updateUser( + $user, + $updateStruct, + ), + ); + } catch (ContentFieldValidationException $e) { + foreach ($e->getFieldErrors() as $validationErrors) { + foreach ($validationErrors as $validationError) { + if (is_array($validationError)) { + foreach ($validationError as $item) { + $errors[] = $item->getTranslatableMessage(); + } + } else { + $errors[] = $validationError->getTranslatableMessage(); + } + } + } + + return $this->render( + 'backoffice/my_account/personal_details.html.twig', + [ + 'form' => $form->createView(), + 'errors' => $errors, + ], + ); + } + + $this->addFlash( + 'success', + 'my_account.user_successfully_updated', + ); + + return $this->redirectToRoute('backoffice_my_account_personal_details'); + } + + return $this->render( + 'backoffice/my_account/personal_details.html.twig', + [ + 'form' => $form->createView(), + ], + ); + } +} diff --git a/src/Backoffice/Controller/MyAccount/UserCredentials.php b/src/Backoffice/Controller/MyAccount/UserCredentials.php new file mode 100644 index 000000000..36966955d --- /dev/null +++ b/src/Backoffice/Controller/MyAccount/UserCredentials.php @@ -0,0 +1,148 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + /** @var \Ibexa\Core\MVC\Symfony\Security\User $sfUser */ + $sfUser = $this->getUser(); + $user = $sfUser->getAPIUser(); + + $emailForm = $this->createForm(ChangeEmailType::class, $user); + $emailForm->handleRequest($request); + + if ($emailForm->isSubmitted() && $emailForm->isValid()) { + if ($this->tokenRepository->findUserToken($user, SecurityTokenType::EmailChangeConfirmation) !== null) { + $this->addFlash('notice', 'my_account.change_email.already_requested'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + /** @var \App\Backoffice\Form\MyAccount\ChangeEmailData $data */ + $data = $emailForm->getData(); + $email = $data->email; + + if ($email === null) { + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + if ($email === $user->email) { + $this->addFlash('notice', 'my_account.change_email.same_email'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + try { + $this->userService->loadUserByEmail($email); + $this->addFlash('notice', 'my_account.change_email.email_exists'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } catch (NotFoundException) { + // Do nothing + } + + $token = new SecurityToken( + $user->id, + SecurityTokenType::EmailChangeConfirmation, + $this->configResolver->getParameter('ngsite.default.token_validity.email_change', 'ngsite'), + $email, + ); + + try { + $this->mailHelper->sendMail( + $email, + 'mail', + 'backoffice/email/change_email_confirmation.html.twig', + [ + 'user' => $user, + 'token' => $token->getToken(), + ], + ); + } catch (RfcComplianceException) { + $this->addFlash('notice', 'my_account.change_email.invalid_email'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + $this->tokenRepository->save($token, true); + + $this->addFlash('success', 'my_account.change_email.activation_mail_sent'); + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + $passwordForm = $this->createForm(ChangePasswordType::class); + $passwordForm->handleRequest($request); + + if ($passwordForm->isSubmitted() && $passwordForm->isValid()) { + $data = $passwordForm->getData(); + + if ($data['password'] !== null) { + $userUpdateStruct = $this->userService->newUserUpdateStruct(); + $userUpdateStruct->password = $data['password']; + + $this->repository->sudo( + fn (): User => $this->userService->updateUser($user, $userUpdateStruct), + ); + + $this->addFlash( + 'success', + 'my_account.user_successfully_updated', + ); + } + + return $this->redirectToRoute('backoffice_my_account_user_credentials'); + } + + $currentUserEmail = $user->email; + $emailParts = explode('@', $currentUserEmail); + + if (count($emailParts) !== 2) { + throw new BadRequestHttpException("User's email address is not valid."); + } + + return $this->render( + 'backoffice/my_account/user_credentials.html.twig', + [ + 'email_domain' => $emailParts[1], + 'email_form' => $emailForm->createView(), + 'password_form' => $passwordForm->createView(), + 'user' => $user, + ], + ); + } +} diff --git a/src/Backoffice/Doctrine/Entity/Bookmark.php b/src/Backoffice/Doctrine/Entity/Bookmark.php new file mode 100644 index 000000000..0971b1d25 --- /dev/null +++ b/src/Backoffice/Doctrine/Entity/Bookmark.php @@ -0,0 +1,126 @@ +createdAt = new DateTimeImmutable(); + } + + public function getId(): int + { + return $this->id; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(DateTimeImmutable $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getUserId(): int + { + return $this->userId; + } + + public function setUserId(int $userId): self + { + $this->userId = $userId; + + return $this; + } + + public function getContentId(): int + { + return $this->contentId; + } + + public function setContentId(int $contentId): self + { + $this->contentId = $contentId; + + return $this; + } + + public function getLocationId(): int + { + return $this->locationId; + } + + public function setLocationId(int $locationId): self + { + $this->locationId = $locationId; + + return $this; + } + + public function getContentTypeId(): int + { + return $this->contentTypeId; + } + + public function setContentTypeId(int $contentTypeId): self + { + $this->contentTypeId = $contentTypeId; + + return $this; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getLocation(): ?Location + { + return $this->location; + } + + public function setLocation(Location $location): self + { + $this->location = $location; + + return $this; + } + + public static function create( + int $userId, + int $contentId, + int $locationId, + int $contentTypeId, + string $title, + ): self { + return new self($userId, $contentId, $locationId, $contentTypeId, $title); + } +} diff --git a/src/Backoffice/Doctrine/Entity/SecurityToken.php b/src/Backoffice/Doctrine/Entity/SecurityToken.php new file mode 100644 index 000000000..9545d4a02 --- /dev/null +++ b/src/Backoffice/Doctrine/Entity/SecurityToken.php @@ -0,0 +1,82 @@ +token = hash('sha256', random_bytes(256)); + $this->creationDate = new DateTimeImmutable(); + $this->expiryDate = $this->creationDate->modify(sprintf('+%d seconds', $validity)); + } + + public function getId(): int + { + return $this->id; + } + + public function getCreationDate(): DateTimeImmutable + { + return $this->creationDate; + } + + public function getExpiryDate(): DateTimeImmutable + { + return $this->expiryDate; + } + + public function isUsed(): bool + { + return $this->isUsed; + } + + public function getUserId(): int + { + return $this->userId; + } + + public function getToken(): string + { + return $this->token; + } + + public function getTokenType(): SecurityTokenType + { + return $this->tokenType; + } + + public function getValidity(): int + { + return $this->validity; + } + + public function getData(): ?string + { + return $this->data; + } + + public function setIsUsed(bool $isUsed): void + { + $this->isUsed = $isUsed; + } +} diff --git a/src/Backoffice/Doctrine/EventListener/BookmarkPostLoadListener.php b/src/Backoffice/Doctrine/EventListener/BookmarkPostLoadListener.php new file mode 100644 index 000000000..585d4829f --- /dev/null +++ b/src/Backoffice/Doctrine/EventListener/BookmarkPostLoadListener.php @@ -0,0 +1,40 @@ +enrichEntity($bookmark); + } + + public function postPersist(Bookmark $bookmark, PostPersistEventArgs $args): void + { + $this->enrichEntity($bookmark); + } + + private function enrichEntity(Bookmark $bookmark): void + { + try { + $bookmark->setLocation( + $this->loadService->loadLocation($bookmark->getLocationId()), + ); + } catch (NotFoundException) { + // Do nothing + } + } +} diff --git a/src/Backoffice/Doctrine/Repository/BookmarkRepository.php b/src/Backoffice/Doctrine/Repository/BookmarkRepository.php new file mode 100644 index 000000000..8d1c7d2a5 --- /dev/null +++ b/src/Backoffice/Doctrine/Repository/BookmarkRepository.php @@ -0,0 +1,116 @@ +createQueryBuilder('b'); + + $query->select('COUNT(b)'); + + $this->addFiltersToQuery($query, $userId, $contentTypeId, $searchText, $dateFrom, $dateTo); + + return (int) $query->getQuery()->getSingleScalarResult(); + } + + /** + * @return iterable + */ + public function filterUserBookmarks( + int $userId, + ?int $contentTypeId = null, + ?string $searchText = null, + ?DateTimeImmutable $dateFrom = null, + ?DateTimeImmutable $dateTo = null, + int $offset = 0, + ?int $limit = null, + ): iterable { + $query = $this->createQueryBuilder('b'); + + $this->addFiltersToQuery($query, $userId, $contentTypeId, $searchText, $dateFrom, $dateTo); + + $query + ->orderBy('b.createdAt', 'DESC') + ->setFirstResult($offset) + ->setMaxResults($limit); + + return $query->getQuery()->getResult(); + } + + public function updateBookmarkNames(int $contentId, string $title): void + { + $query = $this->createQueryBuilder('b'); + + $query->update() + ->set('b.title', ':title') + ->where($query->expr()->eq('b.contentId', ':contentId')) + ->setParameter('title', $title) + ->setParameter('contentId', $contentId) + ->getQuery()->execute(); + } + + private function addFiltersToQuery( + QueryBuilder $query, + ?int $userId = null, + ?int $contentTypeId = null, + ?string $searchText = null, + ?DateTimeImmutable $dateFrom = null, + ?DateTimeImmutable $dateTo = null, + ): void { + if ($userId !== null) { + $query->andWhere( + $query->expr()->eq('b.userId', ':userId'), + )->setParameter('userId', $userId); + } + + if ($contentTypeId !== 0 && $contentTypeId !== null) { + $query->andWhere( + $query->expr()->eq('b.contentTypeId', ':contentTypeId'), + )->setParameter('contentTypeId', $contentTypeId); + } + + if ($searchText !== '' && $searchText !== null) { + $query->andWhere( + $query->expr()->like('b.title', ':searchText'), + )->setParameter('searchText', '%' . $searchText . '%'); + } + + if ($dateFrom !== null) { + $query->andWhere( + $query->expr()->gte('b.createdAt', ':dateFrom'), + )->setParameter('dateFrom', $dateFrom); + } + + if ($dateTo !== null) { + $query->andWhere( + $query->expr()->lt('b.createdAt', ':dateTo'), + )->setParameter('dateTo', $dateTo); + } + } +} diff --git a/src/Backoffice/Doctrine/Repository/SecurityTokenRepository.php b/src/Backoffice/Doctrine/Repository/SecurityTokenRepository.php new file mode 100644 index 000000000..62e890160 --- /dev/null +++ b/src/Backoffice/Doctrine/Repository/SecurityTokenRepository.php @@ -0,0 +1,46 @@ +createQueryBuilder('t'); + + $query + ->where( + $query->expr()->andX( + $query->expr()->eq('t.userId', ':userId'), + $query->expr()->eq('t.tokenType', ':tokenType'), + $query->expr()->eq('t.isUsed', 0), + $query->expr()->gt('t.expiryDate', ':expiryDate'), + ), + )->setParameter('userId', $user->id) + ->setParameter('tokenType', $tokenType->value) + ->setParameter('expiryDate', new DateTimeImmutable()); + + return $query->getQuery()->getOneOrNullResult(); + } +} diff --git a/src/Backoffice/Enums/SecurityTokenType.php b/src/Backoffice/Enums/SecurityTokenType.php new file mode 100644 index 000000000..e1b8734da --- /dev/null +++ b/src/Backoffice/Enums/SecurityTokenType.php @@ -0,0 +1,10 @@ + + */ + public function __invoke(): iterable + { + $allowedContentTypes = $this->configResolver->getParameter('bookmarks.content_types', 'ngsite'); + + /** @var \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType[] $contentTypes */ + $contentTypes = (function (): Generator { + $contentTypeGroups = $this->contentTypeService->loadContentTypeGroups(); + foreach ($contentTypeGroups as $contentTypeGroup) { + yield from $this->contentTypeService->loadContentTypes($contentTypeGroup); + } + })(); + + $typeOptions = []; + + foreach ($contentTypes as $contentType) { + if (in_array($contentType->identifier, $allowedContentTypes, true)) { + $contentTypeName = $contentType->getName(); + $typeOptions[$contentTypeName] = $contentType->id; + } + } + + ksort($typeOptions); + + return $typeOptions; + } +} diff --git a/src/Backoffice/Form/Filter/DateRangeType.php b/src/Backoffice/Form/Filter/DateRangeType.php new file mode 100644 index 000000000..d19af2e62 --- /dev/null +++ b/src/Backoffice/Form/Filter/DateRangeType.php @@ -0,0 +1,66 @@ +setRequired('translation_prefix'); + $resolver->setAllowedTypes('translation_prefix', 'string'); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add( + 'date', + Type\TextType::class, + [ + 'required' => false, + 'label' => sprintf('%s.filter.date.label', $options['translation_prefix']), + ], + ); + + $builder->add( + 'dateFrom', + Type\DateTimeType::class, + [ + 'html5' => false, + 'input' => 'string', + 'widget' => 'single_text', + 'required' => false, + ], + ); + + $builder->get('dateFrom')->addModelTransformer(new DateTimeTransformer()); + + $builder->add( + 'dateTo', + Type\DateTimeType::class, + [ + 'html5' => false, + 'input' => 'string', + 'widget' => 'single_text', + 'required' => false, + ], + ); + + $builder->get('dateTo')->addModelTransformer(new DateTimeTransformer()); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['translation_prefix'] = $options['translation_prefix']; + } +} diff --git a/src/Backoffice/Form/Filter/DateTimeTransformer.php b/src/Backoffice/Form/Filter/DateTimeTransformer.php new file mode 100644 index 000000000..09e8de317 --- /dev/null +++ b/src/Backoffice/Form/Filter/DateTimeTransformer.php @@ -0,0 +1,35 @@ + + */ +final class DateTimeTransformer implements DataTransformerInterface +{ + public function transform($value): ?string + { + return $value?->format('Y-m-d H:i:s'); + } + + public function reverseTransform($value): ?DateTimeImmutable + { + try { + return $value !== '' && $value !== null ? + new DateTimeImmutable($value, new DateTimeZone('UTC')) : + null; + } catch (Throwable) { + return null; + } + } +} diff --git a/src/Backoffice/Form/Filter/FilterType.php b/src/Backoffice/Form/Filter/FilterType.php new file mode 100644 index 000000000..50af981e2 --- /dev/null +++ b/src/Backoffice/Form/Filter/FilterType.php @@ -0,0 +1,75 @@ +setDefault('translation_domain', 'backoffice'); + $resolver->setDefault('method', Request::METHOD_GET); + $resolver->setDefault('csrf_protection', false); + + $resolver->setRequired('translation_prefix'); + $resolver->setAllowedTypes('translation_prefix', 'string'); + + $resolver->setRequired('show_date'); + $resolver->setAllowedTypes('show_date', 'bool'); + $resolver->setDefault('show_date', false); + + $resolver->setRequired('show_search'); + $resolver->setAllowedTypes('show_search', 'bool'); + $resolver->setDefault('show_search', false); + + $resolver->setRequired('selections'); + $resolver->setAllowedTypes('selections', 'array'); + $resolver->setDefault('selections', []); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add( + 'selections', + SelectionsType::class, + [ + 'label' => false, + 'translation_prefix' => $options['translation_prefix'], + 'selections' => $options['selections'], + ], + ); + + $builder->add( + 'date', + DateRangeType::class, + [ + 'label' => false, + 'translation_prefix' => $options['translation_prefix'], + ], + ); + + $builder->add( + 'searchText', + Type\SearchType::class, + [ + 'required' => false, + ], + ); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['translation_prefix'] = $options['translation_prefix']; + $view->vars['show_date'] = $options['show_date']; + $view->vars['show_search'] = $options['show_search']; + } +} diff --git a/src/Backoffice/Form/Filter/SelectionsType.php b/src/Backoffice/Form/Filter/SelectionsType.php new file mode 100644 index 000000000..c349c7591 --- /dev/null +++ b/src/Backoffice/Form/Filter/SelectionsType.php @@ -0,0 +1,119 @@ +setDefault('translation_domain', 'backoffice'); + + $resolver->setRequired('translation_prefix'); + $resolver->setAllowedTypes('translation_prefix', 'string'); + + $resolver->setRequired('selections'); + $resolver->setAllowedTypes('selections', 'array'); + $resolver->setDefault('selections', []); + + $resolver->setAllowedValues( + 'selections', + static function (array $selections): bool { + foreach ($selections as $identifier => $selection) { + if ( + is_callable($selection['options']) + || is_array($selection['options']) + || (is_string($selection['options']) && is_a($selection['options'], BackedEnum::class, true)) + ) { + continue; + } + + throw new InvalidOptionsException( + sprintf( + 'Choices for "%s" identifier must be a callable or an array or a class string for a backed enum, %s given.', + $identifier, + get_debug_type($selection['options']), + ), + ); + } + + return true; + }, + ); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + foreach ($options['selections'] as $identifier => $choices) { + if (is_callable($choices['options']) || (is_array($choices['options']) && is_callable($choices['options'][0] ?? null))) { + $callable = $choices['options']; + $params = []; + + if (!is_callable($callable)) { + $callable = $choices['options'][0] ?? static fn (): array => []; + $params = $choices['options'][1] ?? []; + } + + $builder->add( + $identifier, + Type\ChoiceType::class, + [ + 'required' => $choices['required'] ?? false, + 'choices' => $callable(...$params), + 'label' => sprintf('%s.filter.%s.label', $options['translation_prefix'], $identifier), + 'multiple' => $choices['multiple'] ?? false, + 'data' => $choices['default_value'] ?? null, + ], + ); + } elseif (is_array($choices['options'])) { + $builder->add( + $identifier, + Type\ChoiceType::class, + [ + 'required' => $choices['required'] ?? false, + 'choices' => $choices['options'], + 'choice_label' => static fn (string $choice): string => sprintf('%s.%s.%s', $options['translation_prefix'], $identifier, $choice), + 'label' => sprintf('%s.filter.%s.label', $options['translation_prefix'], $identifier), + 'multiple' => $choices['multiple'] ?? false, + 'data' => $choices['default_value'] ?? null, + ], + ); + } elseif (is_string($choices['options'])) { + $builder->add( + $identifier, + Type\EnumType::class, + [ + 'required' => $choices['required'] ?? false, + 'class' => $choices['options'], + 'choice_label' => static fn (BackedEnum $choice): string => sprintf('%s.%s.%s', $options['translation_prefix'], $identifier, $choice->value), + 'label' => sprintf('%s.filter.%s.label', $options['translation_prefix'], $identifier), + 'multiple' => $choices['multiple'] ?? false, + 'data' => $choices['default_value'] ?? null, + ], + ); + } + } + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['translation_prefix'] = $options['translation_prefix']; + } +} diff --git a/src/Backoffice/Form/MyAccount/ChangeEmailData.php b/src/Backoffice/Form/MyAccount/ChangeEmailData.php new file mode 100644 index 000000000..a9a6ab753 --- /dev/null +++ b/src/Backoffice/Form/MyAccount/ChangeEmailData.php @@ -0,0 +1,12 @@ +email); + + if (count($emailParts) !== 2) { + throw new BadRequestHttpException("User's email address is not valid."); + } + + $emailUsername = $emailParts[0]; + + $forms = iterator_to_array($forms); + + $forms['email_username']->setData($emailUsername); + } + + public function mapFormsToData(Traversable $forms, &$viewData): void + { + /** @var \Ibexa\Core\MVC\Symfony\Security\User $symfonyUser */ + $symfonyUser = $this->security->getUser(); + $user = $symfonyUser->getAPIUser(); + + $emailParts = explode('@', $user->email); + + if (count($emailParts) !== 2) { + throw new BadRequestHttpException("User's email address is not valid."); + } + + $forms = iterator_to_array($forms); + $emailDomain = '@' . $emailParts[1]; + + $viewData = new ChangeEmailData( + $forms['email_username']->getData() . $emailDomain, + ); + } +} diff --git a/src/Backoffice/Form/MyAccount/ChangeEmailType.php b/src/Backoffice/Form/MyAccount/ChangeEmailType.php new file mode 100644 index 000000000..2962cd249 --- /dev/null +++ b/src/Backoffice/Form/MyAccount/ChangeEmailType.php @@ -0,0 +1,42 @@ +setDefault('translation_domain', 'backoffice'); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add( + 'email_username', + TextType::class, + [ + 'required' => true, + 'label' => 'form.my_account.user_credentials.email', + 'constraints' => [ + new Constraints\NotBlank(), + new Constraints\Regex(['pattern' => '/^[^@]+$/', 'message' => 'backoffice.change_email.username.invalid']), + ], + ], + ); + + $builder->setDataMapper(new ChangeEmailDataMapper($this->security)); + } +} diff --git a/src/Backoffice/Form/MyAccount/ChangePasswordType.php b/src/Backoffice/Form/MyAccount/ChangePasswordType.php new file mode 100644 index 000000000..3d91edfea --- /dev/null +++ b/src/Backoffice/Form/MyAccount/ChangePasswordType.php @@ -0,0 +1,42 @@ +setDefault('translation_domain', 'backoffice'); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add( + 'current_password', + PasswordType::class, + [ + 'required' => true, + 'constraints' => [ + new UserPassword(['message' => 'backoffice.change_password.current_password.invalid']), + ], + ], + ); + + $builder->add( + 'password', + UserPasswordType::class, + [ + 'required' => true, + ], + ); + } +} diff --git a/src/Backoffice/Form/MyAccount/PersonalDetailsData.php b/src/Backoffice/Form/MyAccount/PersonalDetailsData.php new file mode 100644 index 000000000..b26be384a --- /dev/null +++ b/src/Backoffice/Form/MyAccount/PersonalDetailsData.php @@ -0,0 +1,18 @@ +setData( + $viewData->getField('first_name')->value->text, + ); + + $forms['last_name']->setData( + $viewData->getField('last_name')->value->text, + ); + } + + public function mapFormsToData(Traversable $forms, &$viewData): void + { + $forms = iterator_to_array($forms); + + /** @var \Symfony\Component\HttpFoundation\File\UploadedFile|null $image */ + $image = $forms['image']->getData(); + + if ($image !== null) { + $originalFilename = pathinfo($image->getClientOriginalName(), PATHINFO_FILENAME); + $extension = $image->guessExtension(); + + try { + $imageValue = Value::fromString($forms['image']->getData()->getRealPath()); + $imageValue->fileName = $originalFilename . '.' . $extension; + } catch (InvalidArgumentException) { + $imageValue = null; + } + } + + $viewData = new PersonalDetailsData( + $imageValue ?? null, + $forms['remove_image']->getData(), + $forms['first_name']->getData(), + $forms['last_name']->getData(), + ); + } +} diff --git a/src/Backoffice/Form/MyAccount/PersonalDetailsType.php b/src/Backoffice/Form/MyAccount/PersonalDetailsType.php new file mode 100644 index 000000000..95f86d90c --- /dev/null +++ b/src/Backoffice/Form/MyAccount/PersonalDetailsType.php @@ -0,0 +1,88 @@ +setDefault('translation_domain', 'backoffice'); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $contentType = $this->contentTypeService->loadContentTypeByIdentifier('user'); + + $builder->add( + 'image', + FileType::class, + [ + 'required' => false, + 'label' => 'form.my_account.personal_details.image', + 'constraints' => [ + new Constraints\Image(), + new IbexaFieldValueValid([ + 'field' => 'image', + 'contentType' => 'user', + ]), + ], + ], + ); + + $builder->add( + 'remove_image', + CheckboxType::class, + [ + 'required' => false, + ], + ); + + $builder->add( + 'first_name', + TextType::class, + [ + 'required' => true, + 'label' => 'form.my_account.personal_details.first_name', + 'constraints' => [ + new Constraints\NotBlank(), + new IbexaFieldValueValid([ + 'field' => 'first_name', + 'contentType' => 'user', + ]), + ], + ], + ); + + $builder->add( + 'last_name', + TextType::class, + [ + 'required' => true, + 'label' => 'form.my_account.personal_details.last_name', + 'constraints' => [ + new Constraints\NotBlank(), + new IbexaFieldValueValid([ + 'field' => 'last_name', + 'contentType' => 'user', + ]), + ], + ], + ); + + $builder->setDataMapper(new PersonalDetailsDataMapper()); + } +} diff --git a/src/Backoffice/Form/User/UserPasswordType.php b/src/Backoffice/Form/User/UserPasswordType.php new file mode 100644 index 000000000..eec1bd57f --- /dev/null +++ b/src/Backoffice/Form/User/UserPasswordType.php @@ -0,0 +1,106 @@ +contentTypeService->loadContentTypeByIdentifier('user'); + + /** @var \Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition $userAccountFieldDefinition */ + $userAccountFieldDefinition = $contentType->getFieldDefinition('user_account'); + + $resolver->setDefaults( + [ + 'type' => Type\PasswordType::class, + 'translation_domain' => 'backoffice', + 'invalid_message' => 'backoffice.user_password.invalid', + 'first_options' => [ + 'label' => 'form.user_password.password.label', + ], + 'second_options' => [ + 'label' => 'form.user_password.repeat_password.label', + ], + 'constraints' => fn (Options $options) => $this->getPasswordConstraints($userAccountFieldDefinition, $options), + ], + ); + } + + public function getParent(): string + { + return Type\RepeatedType::class; + } + + /** + * @return \Symfony\Component\Validator\Constraint[] + */ + private function getPasswordConstraints(FieldDefinition $definition, Options $options): array + { + $config = $definition->validatorConfiguration['PasswordValueValidator']; + + $constraints = $options['required'] ? + [new Constraints\NotBlank()] : + []; + + if ($config['requireAtLeastOneUpperCaseCharacter'] ?? false) { + $constraints[] = new Constraints\Regex( + [ + 'pattern' => '/\p{Lu}/u', + 'message' => 'backoffice.register_user.password.at_least_one_uppercase_character', + ], + ); + } + + if ($config['requireAtLeastOneLowerCaseCharacter'] ?? false) { + $constraints[] = new Constraints\Regex( + [ + 'pattern' => '/\p{Ll}/u', + 'message' => 'backoffice.register_user.password.at_least_one_lowercase_character', + ], + ); + } + + if ($config['requireAtLeastOneNumericCharacter'] ?? false) { + $constraints[] = new Constraints\Regex( + [ + 'pattern' => '/\pN/u', + 'message' => 'backoffice.register_user.password.at_least_one_numeric_character', + ], + ); + } + + if ($config['requireAtLeastOneNonAlphanumericCharacter'] ?? false) { + $constraints[] = new Constraints\Regex( + [ + 'pattern' => '/[^\p{Ll}\p{Lu}\pL\pN]/u', + 'message' => 'backoffice.register_user.password.at_least_one_non_alphanumeric_character', + ], + ); + } + + if ($config['requireNotCompromisedPassword'] ?? false) { + $constraints[] = new Constraints\NotCompromisedPassword(); + } + + if (($config['minLength'] ?? 0) > 0) { + $constraints[] = new Constraints\Length(['min' => $config['minLength']]); + } + + return $constraints; + } +} diff --git a/src/Backoffice/Menu/MainMenuBuilder.php b/src/Backoffice/Menu/MainMenuBuilder.php new file mode 100644 index 000000000..48b23c17e --- /dev/null +++ b/src/Backoffice/Menu/MainMenuBuilder.php @@ -0,0 +1,93 @@ +factory->createItem('root'); + + $menu + ->addChild('dashboard', ['route' => 'backoffice_dashboard_index']) + ->setLabel('menu.main_menu.dashboard') + ->setExtra('translation_domain', 'backoffice') + ->setExtra('icon_class', 'icon-dashboard'); + + $menu + ->addChild('bookmarks', ['route' => 'backoffice_bookmarks_index']) + ->setLabel('menu.main_menu.bookmarks') + ->setExtra('translation_domain', 'backoffice') + ->setExtra('icon_class', 'icon-bookmarks'); + + $menu + ->addChild('conversations', ['route' => 'ngconversations_app']) + ->setLabel('menu.main_menu.conversations') + ->setExtra('translation_domain', 'backoffice') + ->setExtra('icon_class', 'icon-conversations') + ->setExtra( + 'icon_count', + $this->conversationRepository->findUnreadParticipantConversationsCount( + $this->participantProvider->provideParticipant(), + ), + ); + + $menu + ->addChild('notifications') + ->setLabel('menu.main_menu.notifications') + ->setExtra('translation_domain', 'backoffice') + ->setExtra('icon_class', 'icon-notifications') + ->setExtra( + 'icon_count', + $this->notificationRepository->getUserNotificationsCount( + $this->userProvider->provideUser(), + false, + ), + ); + + /** @var \Knp\Menu\ItemInterface $notificationsMenu */ + $notificationsMenu = $menu->getChild('notifications'); + + $notificationsMenu + ->addChild('notifications_inbox', ['route' => 'ngnotifications_admin_inbox']) + ->setLabel('menu.main_menu.notifications.inbox') + ->setExtra('translation_domain', 'backoffice'); + + $notificationsMenu + ->addChild('notifications_archive', ['route' => 'ngnotifications_admin_archive']) + ->setLabel('menu.main_menu.notifications.archive') + ->setExtra('translation_domain', 'backoffice'); + + $notificationsMenu + ->addChild('notifications_subscriptions', ['route' => 'ngnotifications_admin_subscriptions']) + ->setLabel('menu.main_menu.notifications.subscriptions') + ->setExtra('translation_domain', 'backoffice'); + + return $menu; + } +} diff --git a/src/Backoffice/Menu/MyAccountMenuBuilder.php b/src/Backoffice/Menu/MyAccountMenuBuilder.php new file mode 100644 index 000000000..6174db72d --- /dev/null +++ b/src/Backoffice/Menu/MyAccountMenuBuilder.php @@ -0,0 +1,34 @@ +factory->createItem('root'); + + $menu + ->addChild('personal_details', ['route' => 'backoffice_my_account_personal_details']) + ->setLabel('menu.main_menu.personal_details') + ->setExtra('translation_domain', 'backoffice'); + + $menu + ->addChild('user_credentials', ['route' => 'backoffice_my_account_user_credentials']) + ->setLabel('menu.main_menu.user_credentials') + ->setExtra('translation_domain', 'backoffice'); + + return $menu; + } +} diff --git a/src/Backoffice/Pagerfanta/BookmarksAdapter.php b/src/Backoffice/Pagerfanta/BookmarksAdapter.php new file mode 100644 index 000000000..e6eba083a --- /dev/null +++ b/src/Backoffice/Pagerfanta/BookmarksAdapter.php @@ -0,0 +1,51 @@ +repository->filterUserBookmarksCount( + $this->userId, + $this->contentTypeId, + $this->searchText, + $this->dateFrom, + $this->dateTo, + ); + } + + /** + * @param int $offset + * @param int $length + * + * @return iterable + */ + public function getSlice($offset, $length): iterable + { + return $this->repository->filterUserBookmarks( + $this->userId, + $this->contentTypeId, + $this->searchText, + $this->dateFrom, + $this->dateTo, + $offset, + $length, + ); + } +} diff --git a/src/Controller/API/Bookmarks/Create.php b/src/Controller/API/Bookmarks/Create.php new file mode 100644 index 000000000..008b735d5 --- /dev/null +++ b/src/Controller/API/Bookmarks/Create.php @@ -0,0 +1,60 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + $allowedContentTypes = $this->configResolver->getParameter('bookmarks.content_types', 'ngsite'); + if (!in_array($location->contentInfo->contentTypeIdentifier, $allowedContentTypes, true)) { + throw new BadRequestHttpException('This location cannot be bookmarked!'); + } + + /** @var \Ibexa\Core\MVC\Symfony\Security\User $sfUser */ + $sfUser = $this->getUser(); + + $existingBookmark = $this->repository->findOneBy( + [ + 'userId' => $sfUser->getAPIUser()->id, + 'locationId' => $location->id, + ], + ); + + if ($existingBookmark instanceof Bookmark) { + throw new BadRequestHttpException('Bookmark already exists!'); + } + + $bookmark = Bookmark::create( + $sfUser->getAPIUser()->id, + $location->contentInfo->id, + $location->id, + $location->contentInfo->contentTypeId, + $location->contentInfo->name ?? '', + ); + + $this->repository->save($bookmark, true); + + return new Response(); + } +} diff --git a/src/Controller/API/Bookmarks/Delete.php b/src/Controller/API/Bookmarks/Delete.php new file mode 100644 index 000000000..efae18cf4 --- /dev/null +++ b/src/Controller/API/Bookmarks/Delete.php @@ -0,0 +1,37 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + /** @var \Ibexa\Core\MVC\Symfony\Security\User $sfUser */ + $sfUser = $this->getUser(); + + $bookmark = $this->repository->findOneBy( + [ + 'userId' => $sfUser->getAPIUser()->id, + 'locationId' => $location->id, + ], + ) ?? throw $this->createNotFoundException(); + + $this->repository->remove($bookmark, true); + + return new Response(); + } +} diff --git a/src/Controller/PageLayout/UserMenu.php b/src/Controller/PageLayout/UserMenu.php new file mode 100644 index 000000000..abe9e39d8 --- /dev/null +++ b/src/Controller/PageLayout/UserMenu.php @@ -0,0 +1,30 @@ +setMaxAge(0); + $response->setSharedMaxAge(0); + $response->setPrivate(); + $response->headers->addCacheControlDirective('no-cache'); + $response->headers->addCacheControlDirective('no-store'); + $response->headers->addCacheControlDirective('must-revalidate'); + + return $this->render( + '@ibexadesign/pagelayout/header/login.html.twig', + [], + $response, + ); + } +} diff --git a/src/DependencyInjection/AppExtension.php b/src/DependencyInjection/AppExtension.php index 1324bf603..6364e6b0b 100644 --- a/src/DependencyInjection/AppExtension.php +++ b/src/DependencyInjection/AppExtension.php @@ -4,7 +4,9 @@ namespace App\DependencyInjection; +use App\Attribute; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\Finder\Finder; @@ -19,6 +21,8 @@ public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); + + $this->registerAttributeAutoConfiguration($container); } public function prepend(ContainerBuilder $container): void @@ -32,4 +36,14 @@ public function prepend(ContainerBuilder $container): void } } } + + private function registerAttributeAutoConfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + Attribute\AsMenuBuilder::class, + static function (ChildDefinition $definition, Attribute\AsMenuBuilder $attribute): void { + $definition->addTag('knp_menu.menu_builder', ['method' => $attribute->method, 'alias' => $attribute->alias]); + }, + ); + } } diff --git a/src/EventListener/Ibexa/Bookmarks/UpdateTitleListener.php b/src/EventListener/Ibexa/Bookmarks/UpdateTitleListener.php new file mode 100644 index 000000000..5a4f8e9d7 --- /dev/null +++ b/src/EventListener/Ibexa/Bookmarks/UpdateTitleListener.php @@ -0,0 +1,28 @@ + 'onUpdateContent', + ]; + } + + public function onUpdateContent(UpdateContentEvent $event): void + { + $content = $event->getContent(); + + $this->bookmarkRepository->updateBookmarkNames($content->id, (string) $content->getName()); + } +} diff --git a/src/EventListener/Ibexa/Notifications/PublishVersionListener.php b/src/EventListener/Ibexa/Notifications/PublishVersionListener.php new file mode 100644 index 000000000..ad63b8e50 --- /dev/null +++ b/src/EventListener/Ibexa/Notifications/PublishVersionListener.php @@ -0,0 +1,122 @@ + $categoryResolver + */ + public function __construct( + private CategoryResolverInterface $categoryResolver, + private NotificationCategoryRegistry $categoryRegistry, + private NotificationManagerInterface $notificationManager, + ) {} + + public static function getSubscribedEvents(): array + { + return [ + PublishVersionEvent::class => 'onPublishVersion', + ]; + } + + public function onPublishVersion(PublishVersionEvent $event): void + { + $mainLocation = $event->getContent()->contentInfo->getMainLocation(); + if ($mainLocation === null) { + return; + } + + $locationCategory = $this->getLocationCategory($mainLocation); + if ($locationCategory === null) { + return; + } + + if ($event->getContent()->versionInfo->versionNo === 1) { + $this->sendNewContentNotification($event->getContent(), $mainLocation, $locationCategory); + + return; + } + + $this->sendUpdatedContentNotification($event->getContent(), $mainLocation, $locationCategory); + } + + private function getLocationCategory(Location $location): ?NotificationCategory + { + $categoryIdentifier = $this->categoryResolver->resolveCategory($location); + + if ($categoryIdentifier !== null) { + $category = $this->categoryRegistry->getCategory($categoryIdentifier); + + if ($category->subscription->enabled) { + return $category; + } + } + + return null; + } + + private function sendNewContentNotification(Content $content, Location $location, NotificationCategory $category): void + { + $this->notificationManager->sendNotification( + $category->identifier, + [], + new NotificationData( + sprintf('New content "%s" has been published', $content->getName()), + new TwigTemplate( + 'backoffice/notifications/new_content.html.twig', + [ + 'content_id' => $content->id, + 'content_name' => $content->getName(), + ], + ), + ), + new Context( + [ + 'contentId' => $content->id, + 'locationId' => $location->id, + ], + ), + ); + } + + private function sendUpdatedContentNotification(Content $content, Location $location, NotificationCategory $category): void + { + $this->notificationManager->sendNotification( + $category->identifier, + [], + new NotificationData( + sprintf('Content "%s" has been updated', $content->getName()), + new TwigTemplate( + 'backoffice/notifications/updated_content.html.twig', + [ + 'content_id' => $content->id, + 'content_name' => $content->getName(), + ], + ), + ), + new Context( + [ + 'contentId' => $content->id, + 'locationId' => $location->id, + ], + ), + ); + } +} diff --git a/src/Repository/Repository.php b/src/Repository/Repository.php new file mode 100644 index 000000000..be773f601 --- /dev/null +++ b/src/Repository/Repository.php @@ -0,0 +1,40 @@ + + */ +abstract class Repository extends ServiceEntityRepository +{ + public function save(object $entity, bool $flush = false, bool $refresh = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->flush(); + + if ($refresh) { + $this->getEntityManager()->refresh($entity); + } + } + } + + public function remove(object $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->flush(); + } + } + + public function flush(): void + { + $this->getEntityManager()->flush(); + } +} diff --git a/src/Twig/BookmarkExtension.php b/src/Twig/BookmarkExtension.php new file mode 100644 index 000000000..13496b9b0 --- /dev/null +++ b/src/Twig/BookmarkExtension.php @@ -0,0 +1,18 @@ +security->getUser(); + + return $this->bookmarkRepository->findOneBy( + [ + 'userId' => $symfonyUser->getAPIUser()->id ?? 0, + 'locationId' => $location->id, + ], + ); + } +} diff --git a/src/Validator/IbexaFieldValueValid.php b/src/Validator/IbexaFieldValueValid.php new file mode 100644 index 000000000..e47ed3f20 --- /dev/null +++ b/src/Validator/IbexaFieldValueValid.php @@ -0,0 +1,14 @@ +field; + + $contentType = $this->contentTypeService->loadContentTypeByIdentifier( + $constraint->contentType, + ); + + /** @var \Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition $fieldDefinition */ + $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier); + $fieldType = $this->fieldTypeRegistry->getFieldType($fieldDefinition->fieldTypeIdentifier); + + try { + if ($value instanceof UploadedFile) { + $value = $value->getRealPath(); + } + + $fieldValue = $fieldType->acceptValue($value); + $fieldErrors = $fieldType->validate($fieldDefinition, $fieldValue); + + if (count($fieldErrors) > 0) { + foreach ($fieldErrors as $fieldError) { + /** @var \Ibexa\Contracts\Core\Repository\Values\Translation\Plural $errorMessage */ + $errorMessage = $fieldError->getTranslatableMessage(); + $this->context->buildViolation((string) $errorMessage) + ->atPath($fieldIdentifier) + ->addViolation(); + } + } + } catch (InvalidArgumentException $e) { + $this->context->buildViolation($e->getMessage()) + ->atPath($fieldIdentifier) + ->addViolation(); + } + } +} diff --git a/templates/backoffice/bookmarks/delete.html.twig b/templates/backoffice/bookmarks/delete.html.twig new file mode 100644 index 000000000..d1922c5f9 --- /dev/null +++ b/templates/backoffice/bookmarks/delete.html.twig @@ -0,0 +1,26 @@ +{% trans_default_domain 'backoffice' %} + + + + + + diff --git a/templates/backoffice/bookmarks/index.html.twig b/templates/backoffice/bookmarks/index.html.twig new file mode 100644 index 000000000..d61341517 --- /dev/null +++ b/templates/backoffice/bookmarks/index.html.twig @@ -0,0 +1,78 @@ +{% extends 'backoffice/layout.html.twig' %} + +{% trans_default_domain 'backoffice' %} + +{% form_theme filter_form 'backoffice/forms/theme.html.twig' %} + +{% block content %} +
+ {{ form_widget(filter_form) }} +
+ + {% if bookmarks|length > 0 %} + + + + + + + + + + + {% for bookmark in bookmarks %} + + + + + + + {% endfor %} + +
{{ 'bookmarks.title'|trans }}{{ 'bookmarks.type'|trans }}{{ 'bookmarks.saved'|trans }}{{ 'bookmarks.options'|trans }}
+ + {{ bookmark.location.content.name }} + + + {% if bookmark.location is not null %} + {{ bookmark.location.contentInfo.contentType.name }} + {% endif %} + {{ bookmark.createdAt|format_datetime }} + +
+ + {% include 'backoffice/parts/confirm_modal.html.twig' %} + + {% if bookmarks.haveToPaginate() %} + {{ pagerfanta(bookmarks, 'ngsite') }} + {% endif %} + {% else %} +
+

{{ 'bookmarks.no_bookmarks'|trans }}

+
+ {% endif %} +{% endblock %} diff --git a/templates/backoffice/conversations/layout.html.twig b/templates/backoffice/conversations/layout.html.twig new file mode 100644 index 000000000..0699d9d0f --- /dev/null +++ b/templates/backoffice/conversations/layout.html.twig @@ -0,0 +1,15 @@ +{% extends 'backoffice/layout.html.twig' %} + +{% block title %}{{ title|default('Conversations') }}{% endblock %} + +{% block meta %} + {% include '@NetgenConversations/app/meta.html.twig' %} +{% endblock %} + +{% block javascripts %} + {% include '@NetgenConversations/app/javascripts.html.twig' %} +{% endblock %} + +{% block stylesheets %} + {% include '@NetgenConversations/app/stylesheets.html.twig' %} +{% endblock %} diff --git a/templates/backoffice/dashboard/index.html.twig b/templates/backoffice/dashboard/index.html.twig new file mode 100644 index 000000000..743bbd90b --- /dev/null +++ b/templates/backoffice/dashboard/index.html.twig @@ -0,0 +1 @@ +{% extends 'backoffice/layout.html.twig' %} diff --git a/templates/backoffice/email/change_email_confirmation.html.twig b/templates/backoffice/email/change_email_confirmation.html.twig new file mode 100644 index 000000000..36bd45940 --- /dev/null +++ b/templates/backoffice/email/change_email_confirmation.html.twig @@ -0,0 +1,5 @@ +

Dear {{ user.name }},

+ +

You requested a change of your email. To confirm, click here.

+ +

This link will be active for 15 minutes.

diff --git a/templates/backoffice/forms/theme.html.twig b/templates/backoffice/forms/theme.html.twig new file mode 100644 index 000000000..2e485bd20 --- /dev/null +++ b/templates/backoffice/forms/theme.html.twig @@ -0,0 +1,97 @@ +{% extends 'backoffice/layout.html.twig' %} + +{% trans_default_domain 'backoffice' %} + +{%- block form_errors -%} + {%- if errors|length > 0 -%} +
    + {%- for error in errors -%} +
  • {{ error.message }}
  • + {%- endfor -%} +
+ {%- endif -%} +{%- endblock form_errors -%} + +{%- block form_row -%} + {% set row_attr = row_attr|default([]) %} + {% set row_attr = row_attr|merge({class: row_attr.class|default('') ~ ' row-input'}) %} + + {% if errors|length > 0 %} + {% set row_attr = row_attr|merge({class: row_attr.class|default('') ~ ' error-input'}) %} + {% endif %} + +
+ {{- form_label(form) -}} + {{- form_widget(form) -}} + {{- form_errors(form) -}} +
+{%- endblock form_row -%} + +{% block filter_widget %} + +{% endblock %} + +{% block date_range_widget %} +
+ {{ form_label(form.date, null, {label_attr: {'class': 'form-label'}}) }} + {{ form_widget( + form.date, { + attr: { + 'readonly': true, + 'placeholder': form.vars.translation_prefix ~ '.filter.date.placeholder', + 'class': 'flatpickr flatpickr-input', + 'data-input': null + } + } + ) }} + + {{ form_widget(form.dateFrom, {'id': 'dateFromHidden', attr: {class: 'form-hidden'}}) }} + {{ form_widget(form.dateTo, {'id': 'dateToHidden', attr: {class: 'form-hidden'}}) }} + + +
+{% endblock %} diff --git a/templates/backoffice/layout.html.twig b/templates/backoffice/layout.html.twig new file mode 100644 index 000000000..5cc276007 --- /dev/null +++ b/templates/backoffice/layout.html.twig @@ -0,0 +1,101 @@ +{% trans_default_domain 'backoffice' %} + +{% set user_initials = ibexa_field_value(app.user.APIUser, 'first_name').text[0:1] ~ ibexa_field_value(app.user.APIUser, 'last_name').text[0:1] %} + + + + + + + + + + {% block meta %}{% endblock %} + + {% for href in encore_entry_css_files('backoffice-index', 'app') %} + + {% endfor %} + + {% block stylesheets %}{% endblock %} + + + {% block title %} + {{ 'layout.title'|trans }} + {% endblock %} + + + +{% if is_granted('ROLE_IMPERSONATED_USER') %} +
+

{{ 'layout.user_settings.you_are_impersonating'|trans }} {{ app.user.APIUser.name }}

+ {{ 'layout.user_settings.end_impersonation'|trans }} +
+{% endif %} + +
+ +
+ + + + +
+ {% block header %} +
+ {{ user_initials }} + +
+

{{ 'layout.hello'|trans }}, {{ app.user.APIUser.name }}

+
+ + {% block header_menu %}{% endblock %} +
+ {% endblock %} +
+
+ +
+ {% block mobile_header_menu %} + {{ block('header_menu') }} + {% endblock %} +
+ +
+ {% block main_menu %} + {% include 'backoffice/main_menu.html.twig' %} + {% endblock %} +
+ +
+
+
+ {% block top_navigation %}{% endblock %} + + {% block pre_content %}{% endblock %} + +
+ {% for type, messages in app.session.flashBag.all %} + {% for message in messages %} +
+ {{ message|trans }} +
+
+ {% endfor %} + {% endfor %} +
+ + {% block content %}{% endblock %} + + {% block post_content %}{% endblock %} +
+
+
+
+ +{% for src in encore_entry_js_files('backoffice-index', 'app') %} + +{% endfor %} + +{% block javascripts %}{% endblock %} + + diff --git a/templates/backoffice/main_menu.html.twig b/templates/backoffice/main_menu.html.twig new file mode 100644 index 000000000..78c9281cf --- /dev/null +++ b/templates/backoffice/main_menu.html.twig @@ -0,0 +1,72 @@ +{% trans_default_domain 'backoffice' %} + +{% set user_initials = ibexa_field_value(app.user.APIUser, 'first_name').text[0:1] ~ ibexa_field_value(app.user.APIUser, 'last_name').text[0:1] %} + +{% set site_name = ngsite.siteInfoContent.fields.site_name.value.text|trim %} + + diff --git a/templates/backoffice/menu/knp_menu.html.twig b/templates/backoffice/menu/knp_menu.html.twig new file mode 100644 index 000000000..e40862efa --- /dev/null +++ b/templates/backoffice/menu/knp_menu.html.twig @@ -0,0 +1,75 @@ +{% extends '@KnpMenu/menu.html.twig' %} + +{# To override this template, copy and paste relevant blocks from '@KnpMenu/menu.html.twig' and 'knp_menu.html.twig' #} + +{% block item %} + {% if item.displayed %} + {# building the class of the item #} + {%- set classes = item.attribute('class') is not empty ? [item.attribute('class')] : [] %} + + {%- if matcher.isCurrent(item) %} + {%- set classes = classes|merge([options.currentClass]) %} + {%- elseif matcher.isAncestor(item, options.matchingDepth) %} + {%- set classes = classes|merge([options.ancestorClass]) %} + {%- endif %} + + {%- if item.actsLikeFirst %} + {%- set classes = classes|merge([options.firstClass]) %} + {%- endif %} + + {%- if item.actsLikeLast %} + {%- set classes = classes|merge([options.lastClass]) %} + {%- endif %} + + {# Mark item as "leaf" (no children) or as "branch" (has children that are displayed) #} + {% if item.hasChildren and options.depth is not same as(0) %} + {% if options.branch_class is not empty and item.displayChildren %} + {%- set classes = classes|merge([options.branch_class]) %} + {% endif %} + {% elseif options.leaf_class is not empty %} + {%- set classes = classes|merge([options.leaf_class]) %} + {%- endif %} + + {%- set attributes = item.attributes %} + + {%- if classes is not empty %} + {%- set attributes = attributes|merge({'class': classes|join(' ')}) %} + {%- endif %} + + {# displaying the item #} + {% import _self as knp_menu %} + + +
+ {% if item.extras.icon_class|default('') is not empty %} + + + + {% endif %} + + {% if item.extras.icon_count|default(0) > 0 %} + + {{ item.extras.icon_count }} + + {% endif %} + + {%- if item.uri is not empty and (not matcher.isCurrent(item) or options.currentAsLink) %} + {{ block('linkElement') }} + {%- else %} + {{ block('spanElement') }} + {%- endif %} + + {% if item.hasChildren and options.depth is not same as(0) %} + + {% endif %} +
+ + {# render the list of children #} + {%- set childrenClasses = item.childrenAttribute('class') is not empty ? [item.childrenAttribute('class')] : [] %} + {%- set childrenClasses = childrenClasses|merge(['menu_level_' ~ item.level]) %} + {%- set listAttributes = item.childrenAttributes|merge({'class': childrenClasses|join(' ') }) %} + + {{ block('list') }} + + {% endif %} +{% endblock %} diff --git a/templates/backoffice/my_account/change_email.html.twig b/templates/backoffice/my_account/change_email.html.twig new file mode 100644 index 000000000..d30972cbc --- /dev/null +++ b/templates/backoffice/my_account/change_email.html.twig @@ -0,0 +1,21 @@ +{% trans_default_domain 'backoffice' %} + + + diff --git a/templates/backoffice/my_account/change_password.html.twig b/templates/backoffice/my_account/change_password.html.twig new file mode 100644 index 000000000..c9ea838e1 --- /dev/null +++ b/templates/backoffice/my_account/change_password.html.twig @@ -0,0 +1,15 @@ +{% trans_default_domain 'backoffice' %} + + diff --git a/templates/backoffice/my_account/menu.html.twig b/templates/backoffice/my_account/menu.html.twig new file mode 100644 index 000000000..b66cdec09 --- /dev/null +++ b/templates/backoffice/my_account/menu.html.twig @@ -0,0 +1,9 @@ +
+ {{ knp_menu_render( + knp_menu_get('backoffice.my_account_menu'), { + 'leaf_class': 'top-navigation-btn', + 'currentClass': 'top-navigation-btn--active', + 'template': 'backoffice/menu/knp_menu.html.twig' + } + ) }} +
diff --git a/templates/backoffice/my_account/personal_details.html.twig b/templates/backoffice/my_account/personal_details.html.twig new file mode 100644 index 000000000..3311e197a --- /dev/null +++ b/templates/backoffice/my_account/personal_details.html.twig @@ -0,0 +1,76 @@ +{% extends 'backoffice/layout.html.twig' %} + +{% trans_default_domain 'backoffice' %} + +{% form_theme form.image _self %} +{% form_theme form.remove_image _self %} + +{% block top_navigation %} + {% include 'backoffice/my_account/menu.html.twig' %} +{% endblock %} + +{% block content %} + +{% endblock %} + +{% block _personal_details_image_widget %} + {% set user = app.user.APIUser %} + + {% set image_class = not ibexa_field_is_empty(user, 'image') ? ' hidden' : '' %} + +
+ {{ form_widget(remove_image_form) }} + {{ form_widget(form, {attr: {accept: 'image/*', class: 'image-upload js-image-upload' ~ image_class }}) }} + + +
+{% endblock %} + +{% block _personal_details_remove_image_widget %} + {% set user = app.user.APIUser %} + +
+
+
+ {{ user.field('image').value.fileName }} +
+ + {{ form_widget(form, {attr: {class: 'remove-image-btn js-remove-image'}}) }} +
+
+{% endblock %} diff --git a/templates/backoffice/my_account/user_credentials.html.twig b/templates/backoffice/my_account/user_credentials.html.twig new file mode 100644 index 000000000..6ae2349b0 --- /dev/null +++ b/templates/backoffice/my_account/user_credentials.html.twig @@ -0,0 +1,13 @@ +{% extends 'backoffice/layout.html.twig' %} + +{% trans_default_domain 'backoffice' %} + +{% block top_navigation %} + {% include 'backoffice/my_account/menu.html.twig' %} +{% endblock %} + +{% block content %} + {% include 'backoffice/my_account/change_email.html.twig' with {form: email_form} only %} +
+ {% include 'backoffice/my_account/change_password.html.twig' with {form: password_form} only %} +{% endblock %} diff --git a/templates/backoffice/notifications/digest.html.twig b/templates/backoffice/notifications/digest.html.twig new file mode 100644 index 000000000..4e06ce9d4 --- /dev/null +++ b/templates/backoffice/notifications/digest.html.twig @@ -0,0 +1,7 @@ +{% for notification in notifications %} +

{{ notification.title }}

+ +

{{ notification.sentAt|format_datetime }}

+ + {{ notification.text|raw }} +{% endfor %} diff --git a/templates/backoffice/notifications/layout.html.twig b/templates/backoffice/notifications/layout.html.twig new file mode 100644 index 000000000..03f878ef3 --- /dev/null +++ b/templates/backoffice/notifications/layout.html.twig @@ -0,0 +1,14 @@ +{% extends 'backoffice/layout.html.twig' %} + +{% block title %}{{ title|default('Notifications') }}{% endblock %} + +{% block javascripts %} + {% include '@NetgenNotifications/admin/javascripts.html.twig' %} +{% endblock %} + +{% block stylesheets %} + {% include '@NetgenNotifications/admin/stylesheets.html.twig' %} +{% endblock %} + +{% block content %} +{% endblock %} diff --git a/templates/backoffice/notifications/new_content.html.twig b/templates/backoffice/notifications/new_content.html.twig new file mode 100644 index 000000000..7aacb2a99 --- /dev/null +++ b/templates/backoffice/notifications/new_content.html.twig @@ -0,0 +1,4 @@ +

New content "{{ content_name }}" has been published.

+ +

See content details here.

+ diff --git a/templates/backoffice/notifications/notification.html.twig b/templates/backoffice/notifications/notification.html.twig new file mode 100644 index 000000000..77030c3fa --- /dev/null +++ b/templates/backoffice/notifications/notification.html.twig @@ -0,0 +1,3 @@ +

{{ notification.sentAt|format_datetime }}

+ +{{ notification.text|raw }} diff --git a/templates/backoffice/notifications/updated_content.html.twig b/templates/backoffice/notifications/updated_content.html.twig new file mode 100644 index 000000000..7aebd68e1 --- /dev/null +++ b/templates/backoffice/notifications/updated_content.html.twig @@ -0,0 +1,3 @@ +

Content "{{ content_name }}" has been updated.

+ +

See content details here.

diff --git a/templates/backoffice/parts/confirm_modal.html.twig b/templates/backoffice/parts/confirm_modal.html.twig new file mode 100644 index 000000000..b8613f1c1 --- /dev/null +++ b/templates/backoffice/parts/confirm_modal.html.twig @@ -0,0 +1,6 @@ + diff --git a/templates/backoffice/user_impersonation/layout.html.twig b/templates/backoffice/user_impersonation/layout.html.twig new file mode 100644 index 000000000..a998ebbe1 --- /dev/null +++ b/templates/backoffice/user_impersonation/layout.html.twig @@ -0,0 +1,15 @@ +{% extends 'backoffice/layout.html.twig' %} + +{% block title %}{{ title|default('User impersonation') }}{% endblock %} + +{% block pre_content %} +
+
+
+{% endblock %} + +{% block post_content %} +
+
+
+{% endblock %} diff --git a/templates/themes/app/pagelayout.html.twig b/templates/themes/app/pagelayout.html.twig index 07b714d45..3003513e8 100644 --- a/templates/themes/app/pagelayout.html.twig +++ b/templates/themes/app/pagelayout.html.twig @@ -60,6 +60,8 @@ {% endblock %} {% endblock %} + {% include '@ibexadesign/parts/page_floating_widgets.html.twig' %} + {{ nglayouts_template_plugin('preview.body') }} diff --git a/templates/themes/app/pagelayout/header/login.html.twig b/templates/themes/app/pagelayout/header/login.html.twig new file mode 100644 index 000000000..e3674667d --- /dev/null +++ b/templates/themes/app/pagelayout/header/login.html.twig @@ -0,0 +1,52 @@ +{% trans_default_domain 'messages' %} + +{% set menu_url = path('login') %} +{% set menu_label = 'ngsite.layout.login'|trans %} + +{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %} + {% set menu_url = '#' %} + {% set menu_label = '%s %s'|format( + ibexa_field_value(app.user.APIUser, 'first_name').text, + ibexa_field_value(app.user.APIUser, 'last_name').text + ) %} +{% endif %} + + diff --git a/templates/themes/app/pagelayout/header/main_menu.html.twig b/templates/themes/app/pagelayout/header/main_menu.html.twig index 9a65a0261..fa8786925 100644 --- a/templates/themes/app/pagelayout/header/main_menu.html.twig +++ b/templates/themes/app/pagelayout/header/main_menu.html.twig @@ -7,5 +7,9 @@ } ) ) }} + + {% if app.request.attributes.get('siteaccess').name == 'peak_eng' %} + {{ render_esi(controller('App\\Controller\\PageLayout\\UserMenu')) }} + {% endif %} {% endblock %} diff --git a/templates/themes/app/parts/page_floating_widgets.html.twig b/templates/themes/app/parts/page_floating_widgets.html.twig new file mode 100644 index 000000000..59b7fcb44 --- /dev/null +++ b/templates/themes/app/parts/page_floating_widgets.html.twig @@ -0,0 +1,51 @@ +{% set notification_category = null %} + +{% if location is defined %} + {% set notification_category = ngnotifications_category(location) %} +{% endif %} + +
+ {% if is_granted('IS_AUTHENTICATED_REMEMBERED') and notification_category is not null %} + + {% endif %} + + + + + + {% if location is defined and is_granted('IS_AUTHENTICATED_REMEMBERED') %} + {% if location.contentInfo.contentType.identifier in ibexa.configResolver.parameter('bookmarks.content_types', 'ngsite') %} + {% set is_bookmarked = ngsite_bookmark(location) is not null %} + + + + + + {% endif %} + {% endif %} +
+ +{% if is_granted('IS_AUTHENTICATED_REMEMBERED') and notification_category is not null %} + +{% endif %} diff --git a/templates/themes/peak/.gitignore b/templates/themes/peak/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/translations/backoffice.en.yaml b/translations/backoffice.en.yaml new file mode 100644 index 000000000..84d0d10d7 --- /dev/null +++ b/translations/backoffice.en.yaml @@ -0,0 +1,68 @@ +layout.title: 'Admin Dashboard' +layout.hello: 'Hello' +layout.report_an_issue: 'Report an issue' +layout.user_settings: 'User settings' +layout.user_settings.end_impersonation: 'End impersonation' +layout.user_settings.you_are_impersonating: 'You are impersonating' + +menu.main_menu.dashboard: 'Dashboard' +menu.main_menu.bookmarks: 'Bookmarks' +menu.main_menu.conversations: 'Conversations' +menu.main_menu.personal_details: 'Personal details' +menu.main_menu.user_credentials: 'User credentials' +menu.main_menu.impersonate: 'Impersonate' +menu.main_menu.preferences: 'Preferences' +menu.main_menu.logout: 'Log out' +menu.main_menu.notifications: 'Notifications' +menu.main_menu.notifications.inbox: 'Inbox' +menu.main_menu.notifications.archive: 'Archive' +menu.main_menu.notifications.subscriptions: 'Subscriptions' + +bookmarks.title: 'Title' +bookmarks.type: 'Type' +bookmarks.saved: 'Saved' +bookmarks.options: 'Options' +bookmarks.no_bookmarks: 'No bookmarks :(' +bookmarks.view_content: 'View content' +bookmarks.delete: 'Delete' +bookmarks.delete.title: 'Delete bookmark' +bookmarks.delete.confirmation: 'Are you sure you want to delete the bookmark for "%content%" page?' +bookmarks.delete.cancel: 'Cancel' +bookmarks.filter.type.label: 'Type' +bookmarks.filter.type.empty_option: 'Select type' +bookmarks.filter.date.label: 'Date' +bookmarks.filter.date.placeholder: 'Select date' +bookmarks.filter.search_text.placeholder: 'Search bookmarks' +bookmarks.filter.button.more: 'More filters' +bookmarks.filter.button.apply: 'Apply filter' +bookmarks.filter.button.reset: 'Reset all' + +my_account.preferences.send_to_email: 'Send to email' +my_account.button.discard: 'Discard' +my_account.button.save_changes: 'Save changes' +my_account.password_requirements: 'The password must have at least 10 characters and at least one uppercase and one lowercase letter.' +my_account.email_requirements: "You will receive a confirmation email with a link to confirm your new email address. The link will be valid for 15 minutes. If you don't confirm, the change won't be accepted." + +my_account.change_email.already_requested: 'You already requested change of e-mail address. Check you inbox or try again later.' +my_account.change_email.same_email: 'You requested the same e-mail address.' +my_account.change_email.email_exists: 'The e-mail address you entered is used by another user.' +my_account.change_email.invalid_email: 'You requested invalid e-mail address.' +my_account.change_email.activation_mail_sent: 'We sent you activation e-mail with confirmation link. Please check your inbox.' +my_account.change_email.already_activated: 'E-mail address has already been activated.' +my_account.change_email.token_expired: 'Your request for e-mail address change has expired. Please try again.' +my_account.change_email.successfully_changed: 'E-mail address successfully changed.' + +my_account.user_successfully_updated: 'User successfully updated.' + +form.my_account.image.replace_image: 'Replace image' +form.my_account.placeholder.search: 'Type to search...' +form.my_account.no_results: 'No results' +form.my_account.searching: 'Searching...' + +form.my_account.user_credentials.email: 'E-mail' + +form.my_account.personal_details.image: 'Image' +form.my_account.personal_details.first_name: 'First name' +form.my_account.personal_details.last_name: 'Last name' +form.user_password.password.label: 'Password' +form.user_password.repeat_password.label: 'Repeat password' diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index b0a148e8e..40c2cd3c3 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -40,3 +40,10 @@ ngsite.job_application.responsibilities: 'Responsibilities' ngsite.job_application.deadline: 'Application deadline' ngsite.skip_to_main_content: 'Skip to main content' +ngsite.widget.subscriptions: 'Subscriptions' + +ngsite.layout.login: 'Log in' +ngsite.layout.register: 'Register' +ngsite.layout.logout: 'Logout' +ngsite.layout.my_dashboard: 'My dashboard' +ngsite.layout.my_profile: 'My profile' diff --git a/translations/ngnotifications.en.yaml b/translations/ngnotifications.en.yaml new file mode 100644 index 000000000..d4043be58 --- /dev/null +++ b/translations/ngnotifications.en.yaml @@ -0,0 +1 @@ +category.default: 'Default' diff --git a/webpack.config.default.js b/webpack.config.default.js index dbecd7dc0..9308d4117 100644 --- a/webpack.config.default.js +++ b/webpack.config.default.js @@ -22,6 +22,7 @@ Encore // will create public/assets/app/build/index.js and public/assets/app/build/index.css .addEntry('index', `./${siteConfig.assetsLocation}/js/index.js`) .addEntry('index-noncritical', `./${siteConfig.assetsLocation}/js/index-noncritical.js`) + .addEntry('backoffice-index', `./${siteConfig.assetsLocation}/js/backoffice/index.js`) // allow sass/scss files to be processed .enableSassLoader((options) => {