From 642b4b7f4cfcc0e350598026cd611f63cfe75af7 Mon Sep 17 00:00:00 2001 From: Tony Air Date: Mon, 9 Aug 2021 15:10:08 +0200 Subject: [PATCH] IMPR: reorganize files --- src/js/_ajax/_lazy-images.js | 79 +++++++ src/js/_ajax/_links.js | 227 +++++++++++++++++++ src/js/_ajax/_online.js | 104 +++++++++ src/js/_main/_css-screen-size.js | 54 +++++ src/js/_main/_funcs.js | 35 +++ src/js/_main/_index.js | 7 + src/js/_main/_loading-spinner.js | 21 ++ src/js/_main/_main.js | 85 +++++++ src/js/_main/_touch.js | 70 ++++++ src/js/_main/_visibility.js | 34 +++ src/js/_ui/_carousel.js | 86 +++++++ src/js/_ui/_instagram.feed.js | 95 ++++++++ src/js/test-build.js | 57 +++++ src/scss/_layout/_forms/_basics.scss | 30 +++ src/scss/_layout/_forms/_index.scss | 1 + src/scss/_layout/_index.scss | 5 + src/scss/_layout/_main/_alerts.scss | 32 +++ src/scss/_layout/_main/_base.scss | 179 +++++++++++++++ src/scss/_layout/_main/_index.scss | 9 + src/scss/_layout/_main/_main.scss | 176 ++++++++++++++ src/scss/_layout/_main/_states/_index.scss | 5 + src/scss/_layout/_main/_states/_mobile.scss | 43 ++++ src/scss/_layout/_main/_states/_network.scss | 39 ++++ src/scss/_layout/_test.scss | 177 +++++++++++++++ src/scss/_libs/_bootstrap-table.scss | 39 ++++ src/scss/_libs/_bootstrap.scss | 51 +++++ src/scss/_libs/_fontawesome.scss | 3 + src/scss/_libs/_silverstripe.scss | 57 +++++ src/scss/_libs/_silverstripe.shop.scss | 8 + src/scss/_ui/_carousel.scss | 93 ++++++++ src/scss/_ui/_flyout.scss | 34 +++ src/scss/_ui/_form.stepped.scss | 9 + src/scss/_ui/_lightbox.scss | 14 ++ src/scss/_ui/_mailchimp.scss | 53 +++++ src/scss/_ui/_map.api.scss | 134 +++++++++++ src/scss/_ui/_multislider.scss | 55 +++++ src/scss/test-build.scss | 3 + 37 files changed, 2203 insertions(+) create mode 100644 src/js/_ajax/_lazy-images.js create mode 100644 src/js/_ajax/_links.js create mode 100644 src/js/_ajax/_online.js create mode 100644 src/js/_main/_css-screen-size.js create mode 100644 src/js/_main/_funcs.js create mode 100644 src/js/_main/_index.js create mode 100644 src/js/_main/_loading-spinner.js create mode 100644 src/js/_main/_main.js create mode 100644 src/js/_main/_touch.js create mode 100644 src/js/_main/_visibility.js create mode 100644 src/js/_ui/_carousel.js create mode 100644 src/js/_ui/_instagram.feed.js create mode 100644 src/js/test-build.js create mode 100644 src/scss/_layout/_forms/_basics.scss create mode 100644 src/scss/_layout/_forms/_index.scss create mode 100644 src/scss/_layout/_index.scss create mode 100644 src/scss/_layout/_main/_alerts.scss create mode 100644 src/scss/_layout/_main/_base.scss create mode 100644 src/scss/_layout/_main/_index.scss create mode 100644 src/scss/_layout/_main/_main.scss create mode 100644 src/scss/_layout/_main/_states/_index.scss create mode 100644 src/scss/_layout/_main/_states/_mobile.scss create mode 100644 src/scss/_layout/_main/_states/_network.scss create mode 100644 src/scss/_layout/_test.scss create mode 100644 src/scss/_libs/_bootstrap-table.scss create mode 100644 src/scss/_libs/_bootstrap.scss create mode 100644 src/scss/_libs/_fontawesome.scss create mode 100644 src/scss/_libs/_silverstripe.scss create mode 100644 src/scss/_libs/_silverstripe.shop.scss create mode 100644 src/scss/_ui/_carousel.scss create mode 100644 src/scss/_ui/_flyout.scss create mode 100644 src/scss/_ui/_form.stepped.scss create mode 100644 src/scss/_ui/_lightbox.scss create mode 100644 src/scss/_ui/_mailchimp.scss create mode 100644 src/scss/_ui/_map.api.scss create mode 100644 src/scss/_ui/_multislider.scss create mode 100644 src/scss/test-build.scss diff --git a/src/js/_ajax/_lazy-images.js b/src/js/_ajax/_lazy-images.js new file mode 100644 index 0000000..9258d64 --- /dev/null +++ b/src/js/_ajax/_lazy-images.js @@ -0,0 +1,79 @@ +// browser tab visibility state detection + +import Events from '../_events'; +import Consts from '../_consts'; + +const axios = require('axios'); + +export default ((W) => { + const NAME = '_main.lazy-images'; + const D = document; + const BODY = D.body; + + const API_STATIC = document.querySelector('meta[name="api_static_domain"]'); + const API_STATIC_URL = API_STATIC ? + API_STATIC.getAttribute('content') : + `${window.location.protocol}//${window.location.host}`; + + console.log(`${NAME} [static url]: ${API_STATIC_URL}`); + + const loadLazyImages = () => { + console.log(`${NAME}: Load lazy images`); + + D.querySelectorAll(`[data-lazy-src]`).forEach((el) => { + el.classList.remove('empty'); + el.classList.add('loading'); + el.classList.remove('loading__network-error'); + + const attr = el.getAttribute('data-lazy-src'); + const imageUrl = attr.startsWith('http') ? attr : API_STATIC_URL + attr; + + // offline response will be served by caching service worker + axios + .get(imageUrl, { + responseType: 'blob', + }) + .then((response) => { + const reader = new FileReader(); // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/FileReader + reader.readAsDataURL(response.data); + reader.onload = () => { + const imageDataUrl = reader.result; + el.setAttribute('src', imageDataUrl); + el.classList.remove('loading'); + el.classList.add('loading__success'); + }; + }) + .catch((e) => { + //el.setAttribute('src', imageUrl); + + if (e.response) { + switch (e.response.status) { + case 404: + msg = 'Not Found.'; + break; + case 500: + msg = 'Server issue, please try again latter.'; + break; + default: + msg = 'Something went wrong.'; + break; + } + + console.error(`${NAME} [${imageUrl}]: ${msg}`); + } else if (e.request) { + msg = 'No response received'; + console.error(`${NAME} [${imageUrl}]: ${msg}`); + } else { + console.error(`${NAME} [${imageUrl}]: ${e.message}`); + } + + el.classList.remove('loading'); + el.classList.add('loading__network-error'); + el.classList.add('empty'); + }); + }); + }; + + W.addEventListener(`${Events.LODEDANDREADY}`, loadLazyImages); + W.addEventListener(`${Events.AJAX}`, loadLazyImages); +})(window); diff --git a/src/js/_ajax/_links.js b/src/js/_ajax/_links.js new file mode 100644 index 0000000..2752b47 --- /dev/null +++ b/src/js/_ajax/_links.js @@ -0,0 +1,227 @@ +// browser tab visibility state detection + +import Events from '../_events'; +import Consts from '../_consts'; +import Page from './models/_page.jsx'; + +import { + getParents, +} from '../_main/_funcs'; + +import { + Collapse, +} from 'bootstrap'; + +import SpinnerUI from '../_main/_loading-spinner'; + +const MainUILinks = ((W) => { + const NAME = '_main.links'; + const D = document; + const BODY = D.body; + + class MainUILinks { + window + static init() { + const ui = this; + ui.GraphPage = null; + + console.log(`${NAME}: init`); + + ui.loaded(); + + // history state switch + W.addEventListener('popstate', (e) => { + ui.popState(e); + }); + } + + static loaded() { + const ui = this; + + D.querySelectorAll('.graphql-page').forEach((el, i) => { + const el_id = el.getAttribute('href'); + el.setAttribute(`data-${ui.name}-id`, el_id); + + el.addEventListener('click', ui.loadClick); + }); + } + + static setActiveLinks(link) { + const ui = this; + D.querySelectorAll(`[data-${ui.name}-id="${link}"]`).forEach( + (el) => { + el.classList.add('active'); + }, + ); + } + + static reset() { + // reset focus + D.activeElement.blur(); + + // remove active and loading classes + D.querySelectorAll('.graphql-page,.nav-item').forEach((el2) => { + el2.classList.remove('active', 'loading'); + }); + } + + static popState(e) { + const ui = this; + + SpinnerUI.show(); + + if (e.state && e.state.page) { + console.log(`${NAME}: [popstate] load`); + const state = JSON.parse(e.state.page); + + state.current = null; + state.popstate = true; + + ui.reset(); + ui.setActiveLinks(e.state.link); + + if (!ui.GraphPage) { + console.log( + `${NAME}: [popstate] GraphPage is missing. Have to render it first`, + ); + + ui.GraphPage = ReactDOM.render( + , + document.getElementById('MainContent'), + ); + } + + ui.GraphPage.setState(state); + SpinnerUI.hide(); + + window.dispatchEvent(new Event(Events.AJAX)); + } else if (e.state && e.state.landing) { + console.log(`${NAME}: [popstate] go to landing`); + W.location.href = e.state.landing; + } else { + console.warn(`${NAME}: [popstate] state is missing`); + console.log(e); + SpinnerUI.hide(); + } + } + + // link specific event {this} = current event, not MainUILinks + static loadClick(e) { + console.groupCollapsed(`${NAME}: load on click`); + e.preventDefault(); + + const ui = MainUILinks; + const el = e.currentTarget; + + SpinnerUI.show(); + + ui.reset(); + el.classList.add('loading'); + el.classList.remove('response-404', 'response-500', 'response-523'); + BODY.classList.add('ajax-loading'); + + // hide parent mobile nav + const navs = getParents(el, '.collapse'); + if (navs.length) { + navs.forEach((nav) => { + const collapseInst = Collapse.getInstance(nav); + if (collapseInst) { + collapseInst.hide(); + } + }); + } + + // hide parent dropdown + /*const dropdowns = getParents(el, '.dropdown-menu'); + if (dropdowns.length) { + const DropdownInst = Dropdown.getInstance(dropdowns[0]); + DropdownInst.hide(); + }*/ + + if (!ui.GraphPage) { + ui.GraphPage = ReactDOM.render( + , + document.getElementById('MainContent'), + ); + } + + const link = el.getAttribute('href') || el.getAttribute('data-href'); + + + ui.GraphPage.state.current = el; + + ui.GraphPage.load(link) + .then((response) => { + BODY.classList.remove('ajax-loading'); + el.classList.remove('loading'); + el.classList.add('active'); + + D.loading_apollo_link = null; + + if (ui.GraphPage.state.Link) { + window.history.pushState({ + page: JSON.stringify(ui.GraphPage.state), + link: el.getAttribute(`data-${ui.name}-id`), + }, + ui.GraphPage.state.Title, + ui.GraphPage.state.Link, + ); + + ui.setActiveLinks(ui.GraphPage.state.Link) + } + + SpinnerUI.hide(); + + window.dispatchEvent(new Event(Events.AJAX)); + console.groupEnd(`${NAME}: load on click`); + }) + .catch((e) => { + console.error(`${NAME}: loading error`); + console.log(e); + + /*BODY.classList.remove('ajax-loading'); + el.classList.remove('loading');*/ + el.classList.add('error', `response-${e.status}`); + /*switch (e.status) { + case 500: + break; + case 404: + el.classList.add('not-found'); + break; + case 523: + el.classList.add('unreachable'); + break; + }*/ + + //SpinnerUI.hide(); + + //window.dispatchEvent(new Event(Events.AJAX)); + console.groupEnd(`${NAME}: load on click`); + + console.log(`${NAME}: reloading page ${link}`); + + // fallback loading + W.location.href = link; + }); + } + } + + W.addEventListener(`${Events.LOADED}`, () => { + MainUILinks.init(); + }); + + W.addEventListener(`${Events.AJAX}`, () => { + MainUILinks.loaded(); + }); + + // fallback + /*W.addEventListener(`${Events.APOLLO_ERROR}`, (e) => { + console.error(`${NAME}: [APOLLO_ERROR] loading failure, reloading the page`); + //W.dispatchEvent(new Event(Events.OFFLINE)); + if (D.loading_apollo_link) { + W.location.href = D.loading_apollo_link; + } + });*/ +})(window); + +export default MainUILinks; diff --git a/src/js/_ajax/_online.js b/src/js/_ajax/_online.js new file mode 100644 index 0000000..2a1034c --- /dev/null +++ b/src/js/_ajax/_online.js @@ -0,0 +1,104 @@ +// ping online/offline state switch and detection + +import Events from '../_events'; +import Consts from '../_consts'; + +const axios = require('axios'); + +export default ((W) => { + const NAME = '_main.online'; + const D = document; + const BODY = D.body; + + let pingInterval; + const PING_META = D.querySelector('meta[name="ping"]'); + + let update_online_status_lock = false; + const UPDATE_ONLINE_STATUS = (online) => { + if (update_online_status_lock) { + return; + } + + update_online_status_lock = true; + if (online) { + if (BODY.classList.contains('is-offline')) { + console.log(`${NAME}: back Online`); + W.dispatchEvent(new Event(Events.BACKONLINE)); + } else { + console.log(`${NAME}: Online`); + W.dispatchEvent(new Event(Events.ONLINE)); + } + + BODY.classList.add('is-online'); + BODY.classList.remove('is-offline'); + + if (PING_META && !pingInterval) { + console.log(`${NAME}: SESSION_PING is active`); + pingInterval = setInterval(SESSION_PING, 300000); // 5 min in ms + } + } else { + console.log(`${NAME}: Offline`); + + BODY.classList.add('is-offline'); + BODY.classList.remove('is-online'); + + clearInterval(pingInterval); + pingInterval = null; + + W.dispatchEvent(new Event(Events.OFFLINE)); + } + + update_online_status_lock = false; + }; + + // session ping + let session_ping_lock = false; + const SESSION_PING = () => { + if (session_ping_lock || BODY.classList.contains('is-offline')) { + return; + } + + const PING_URL = PING_META.getAttribute('content'); + + console.log(`${NAME}: session ping`); + session_ping_lock = true; + + axios + .post(PING_URL, {}) + .then((resp) => { + session_ping_lock = false; + UPDATE_ONLINE_STATUS(true); + }) + .catch((error) => { + console.error(error); + console.warn(`${NAME}: SESSION_PING failed`); + + session_ping_lock = false; + UPDATE_ONLINE_STATUS(false); + }); + }; + + // current browser online state + + + const navigatorStateUpdate = () => { + if (typeof navigator.onLine !== 'undefined') { + if (!navigator.onLine) { + UPDATE_ONLINE_STATUS(false); + } else { + UPDATE_ONLINE_STATUS(true); + } + } + }; + + W.addEventListener(`${Events.OFFLINE}`, () => { + UPDATE_ONLINE_STATUS(false); + }); + + W.addEventListener(`${Events.ONLINE}`, () => { + UPDATE_ONLINE_STATUS(true); + }); + + W.addEventListener(`${Events.LOADED}`, navigatorStateUpdate); + W.addEventListener(`${Events.AJAX}`, navigatorStateUpdate); +})(window); diff --git a/src/js/_main/_css-screen-size.js b/src/js/_main/_css-screen-size.js new file mode 100644 index 0000000..c1749e3 --- /dev/null +++ b/src/js/_main/_css-screen-size.js @@ -0,0 +1,54 @@ +// browser tab visibility state detection + +import Events from '../_events'; +import Consts from '../_consts'; + +export default ((W) => { + const NAME = '_main.css-screen-size'; + const D = document; + const BODY = D.body; + + const detectCSSScreenSize = () => { + const el = D.createElement('div'); + el.className = 'env-test'; + BODY.appendChild(el); + + const envs = [...Consts.ENVS].reverse(); + let curEnv = envs.shift(); + BODY.classList.remove(...envs); + + for (let i = 0; i < envs.length; ++i) { + const env = envs[i]; + el.classList.add(`d-${env}-none`); + + if (W.getComputedStyle(el).display === 'none') { + curEnv = env; + BODY.classList.add(`${curEnv}`); + break; + } + } + + let landscape = true; + if (W.innerWidth > W.innerHeight) { + BODY.classList.add('landscape'); + BODY.classList.remove('portrait'); + } else { + landscape = false; + + BODY.classList.add('portrait'); + BODY.classList.remove('landscape'); + } + + console.log( + `${NAME}: screen size detected ${curEnv} | landscape ${landscape}`, + ); + + BODY.removeChild(el); + + return curEnv; + }; + + W.addEventListener(`${Events.LOADED}`, detectCSSScreenSize); + + W.addEventListener(`${Events.RESIZE}`, detectCSSScreenSize); +})(window); diff --git a/src/js/_main/_funcs.js b/src/js/_main/_funcs.js new file mode 100644 index 0000000..d2e82a9 --- /dev/null +++ b/src/js/_main/_funcs.js @@ -0,0 +1,35 @@ +const funcs = {}; + +/*! + * Get all of an element's parent elements up the DOM tree + * (c) 2019 Chris Ferdinandi, MIT License, https://gomakethings.com + * @param {Node} elem The element + * @param {String} selector Selector to match against [optional] + * @return {Array} The parent elements + */ + +funcs.getParents = (elem, selector) => { + // Setup parents array + const parents = []; + let el = elem; + // Get matching parent elements + while (el && el !== document) { + // If using a selector, add matching parents to array + // Otherwise, add all parents + if (selector) { + if (el.matches(selector)) { + parents.push(el); + } + } else { + parents.push(el); + } + + // Jump to the next parent node + el = el.parentNode; + } + + return parents; +}; + +module.exports = funcs; +module.exports.default = funcs; diff --git a/src/js/_main/_index.js b/src/js/_main/_index.js new file mode 100644 index 0000000..8c7660d --- /dev/null +++ b/src/js/_main/_index.js @@ -0,0 +1,7 @@ +import Events from '../_events'; +import Consts from '../_consts'; + +import './_visibility'; +import './_touch'; +import './_css-screen-size'; +import './_main'; diff --git a/src/js/_main/_loading-spinner.js b/src/js/_main/_loading-spinner.js new file mode 100644 index 0000000..3c7cd20 --- /dev/null +++ b/src/js/_main/_loading-spinner.js @@ -0,0 +1,21 @@ +// browser tab visibility state detection + +import Events from '../_events'; + +const NAME = '_main.loading-spinner'; +const D = document; +const BODY = D.body; +const SPINNER = D.getElementById('PageLoading'); + +class SpinnerUI { + static show() { + console.log(`${NAME}: show`); + SPINNER.classList.remove('d-none'); + } + static hide() { + console.log(`${NAME}: hide`); + SPINNER.classList.add('d-none'); + } +} + +export default SpinnerUI; diff --git a/src/js/_main/_main.js b/src/js/_main/_main.js new file mode 100644 index 0000000..38c1651 --- /dev/null +++ b/src/js/_main/_main.js @@ -0,0 +1,85 @@ +import Events from '../_events'; +import Consts from '../_consts'; +import SpinnerUI from './_loading-spinner'; + +const MainUI = ((W) => { + const NAME = '_main'; + const D = document; + const BODY = D.body; + + console.info( + `%cUI Kit ${UINAME} ${UIVERSION}`, + 'color:yellow;font-size:14px', + ); + console.info( + `%c${UIMetaNAME} ${UIMetaVersion}`, + 'color:yellow;font-size:12px', + ); + console.info( + `%chttps://github.com/a2nt/webpack-bootstrap-ui-kit by ${UIAUTHOR}`, + 'color:yellow;font-size:10px', + ); + + console.info(`%cENV: ${process.env.NODE_ENV}`, 'color:green;font-size:10px'); + console.groupCollapsed('Events'); + Object.keys(Events).forEach((k) => { + console.info(`${k}: ${Events[k]}`); + }); + console.groupEnd('Events'); + + console.groupCollapsed('Consts'); + Object.keys(Consts).forEach((k) => { + console.info(`${k}: ${Consts[k]}`); + }); + console.groupEnd('Events'); + + console.groupCollapsed('Init'); + console.time('init'); + + class MainUI { + // first time the website initialization + static init() { + const ui = this; + + // store landing page state + W.history.replaceState( + { + landing: W.location.href, + }, + D.title, + W.location.href, + ); + // + + ui.loaded(); + } + + // init AJAX components + static loaded() { + const ui = this; + console.log(`${NAME}: loaded`); + } + } + + W.addEventListener(`${Events.LOADED}`, () => { + MainUI.init(); + + BODY.classList.add('loaded'); + SpinnerUI.hide(); + + console.groupEnd('init'); + console.timeEnd('init'); + + W.dispatchEvent(new Event(Events.LODEDANDREADY)); + }); + + W.addEventListener(`${Events.AJAX}`, () => { + MainUI.loaded(); + }); + + W.MainUI = MainUI; + + return MainUI; +})(window); + +export default MainUI; diff --git a/src/js/_main/_touch.js b/src/js/_main/_touch.js new file mode 100644 index 0000000..b28f4c1 --- /dev/null +++ b/src/js/_main/_touch.js @@ -0,0 +1,70 @@ +// touch/mouse detection + +import Events from '../_events'; +import Consts from '../_consts'; + +export default ((W) => { + const NAME = '_main.touch'; + const D = document; + const BODY = D.body; + + let prev_touch_event_name; + let touch_timeout; + const SET_TOUCH_SCREEN = (bool, event_name) => { + if (touch_timeout || event_name === prev_touch_event_name) { + return; + } + + if (bool) { + console.log(`${NAME}: Touch screen enabled`); + + BODY.classList.add('is-touch'); + BODY.classList.remove('is-mouse'); + + W.dispatchEvent(new Event(Events.TOUCHENABLE)); + } else { + console.log(`${NAME}: Touch screen disabled`); + + BODY.classList.add('is-mouse'); + BODY.classList.remove('is-touch'); + + W.dispatchEvent(new Event(Events.TOUCHDISABLED)); + } + + prev_touch_event_name = event_name; + // prevent firing touch and mouse events together + if (!touch_timeout) { + touch_timeout = setTimeout(() => { + clearTimeout(touch_timeout); + touch_timeout = null; + }, 500); + } + }; + + SET_TOUCH_SCREEN( + 'ontouchstart' in W || + navigator.MaxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 || + W.matchMedia('(hover: none)').matches, + 'init', + ); + + D.addEventListener('touchend', (e) => { + let touch = false; + if (e.type !== 'click') { + touch = true; + } + + SET_TOUCH_SCREEN(touch, 'click-touchend'); + }); + + // disable touch on mouse events + D.addEventListener('click', (e) => { + let touch = false; + if (e.type !== 'click') { + touch = true; + } + + SET_TOUCH_SCREEN(touch, 'click-touchend'); + }); +})(window); diff --git a/src/js/_main/_visibility.js b/src/js/_main/_visibility.js new file mode 100644 index 0000000..8459a35 --- /dev/null +++ b/src/js/_main/_visibility.js @@ -0,0 +1,34 @@ +// browser tab visibility state detection + +import Events from '../_events'; +import Consts from '../_consts'; + +export default ((W) => { + const NAME = '_main.visibility'; + const D = document; + const BODY = D.body; + + // update visibility state + // get browser window visibility preferences + // Opera 12.10, Firefox >=18, Chrome >=31, IE11 + const HiddenName = 'hidden'; + const VisibilityChangeEvent = 'visibilitychange'; + + D.addEventListener(VisibilityChangeEvent, () => { + if (D.visibilityState === HiddenName) { + console.log(`${NAME}: Tab: hidden`); + + BODY.classList.add('is-hidden'); + BODY.classList.remove('is-focused'); + + W.dispatchEvent(new Event(Events.TABHIDDEN)); + } else { + console.log(`${NAME}: Tab: focused`); + + BODY.classList.add('is-focused'); + BODY.classList.remove('is-hidden'); + + W.dispatchEvent(new Event(Events.TABFOCUSED)); + } + }); +})(window); diff --git a/src/js/_ui/_carousel.js b/src/js/_ui/_carousel.js new file mode 100644 index 0000000..cd93d43 --- /dev/null +++ b/src/js/_ui/_carousel.js @@ -0,0 +1,86 @@ +import Events from '../_events'; +import Carousel from 'bootstrap/js/src/carousel'; + +const CarouselUI = ((window) => { + const NAME = 'js-carousel'; + + const init = () => { + console.log(`${NAME}: init`); + + document.querySelectorAll(`.${NAME}`).forEach((el, i) => { + const carousel = new Carousel(el); + // create next/prev arrows + if (el.dataset.bsArrows) { + const next = document.createElement('button'); + next.classList.add('carousel-control-next'); + next.setAttribute('type', 'button'); + next.setAttribute('aria-label', 'Next Slide'); + next.setAttribute('data-bs-target', el.getAttribute('id')); + next.setAttribute('data-bs-slide', 'next'); + next.addEventListener('click', (e) => { + carousel.next(); + }); + next.innerHTML = 'Next'; + el.appendChild(next); + + const prev = document.createElement('button'); + prev.setAttribute('type', 'button'); + prev.setAttribute('aria-label', 'Previous Slide'); + prev.classList.add('carousel-control-prev'); + prev.setAttribute('data-bs-target', el.getAttribute('id')); + prev.setAttribute('data-bs-slide', 'prev'); + prev.addEventListener('click', (e) => { + carousel.prev(); + }); + prev.innerHTML = 'Previous'; + el.appendChild(prev); + } + + if (el.dataset.bsIndicators) { + const indicators = document.createElement('div'); + indicators.classList.add('carousel-indicators'); + const items = el.querySelectorAll('.carousel-item'); + let i = 0; + while (i < items.length) { + const ind = document.createElement('button'); + ind.setAttribute('type', 'button'); + ind.setAttribute('aria-label', `Slide to #${ i + 1}`); + if (i == 0) { + ind.classList.add('active'); + } + ind.setAttribute('data-bs-target', el.getAttribute('id')); + ind.setAttribute('data-bs-slide-to', i); + + ind.addEventListener('click', (e) => { + const target = e.target; + carousel.to(target.getAttribute('data-bs-slide-to')); + indicators.querySelectorAll('.active').forEach((ind2) => { + ind2.classList.remove('active'); + }); + target.classList.add('active'); + }); + + indicators.appendChild(ind); + i++; + } + + el.appendChild(indicators); + el.addEventListener('slide.bs.carousel', (e) => { + el.querySelectorAll('.carousel-indicators .active').forEach((ind2) => { + ind2.classList.remove('active'); + }); + el.querySelectorAll(`.carousel-indicators [data-bs-slide-to="${ e.to }"]`).forEach((ind2) => { + ind2.classList.add('active'); + }); + }); + + } + el.classList.add(`${NAME}-active`); + }); + }; + + window.addEventListener(`${Events.LODEDANDREADY}`, init); + window.addEventListener(`${Events.AJAX}`, init); +})(window); + +export default CarouselUI; diff --git a/src/js/_ui/_instagram.feed.js b/src/js/_ui/_instagram.feed.js new file mode 100644 index 0000000..5e03df2 --- /dev/null +++ b/src/js/_ui/_instagram.feed.js @@ -0,0 +1,95 @@ +// api-less instagram feed + +// Visitor network maybe temporary banned by Instagram because of too many requests from external websites +// so it isn't very stable implementation. You should have something for the fall-back. + +import Events from '../_events'; +import Consts from '../_consts'; +import InstagramFeed from '@jsanahuja/instagramfeed/src/InstagramFeed'; + +export default ((window) => { + const NAME = 'js-instagramfeed'; + const BODY = document.body; + + const ig_media_preview = (base64data) => { + const jpegtpl = + '/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEABsaGikdKUEmJkFCLy8vQkc/Pj4/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0cBHSkpNCY0PygoP0c/NT9HR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR//AABEIABQAKgMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AA==', + t = atob(base64data), + p = t.slice(3).split(''), + o = t + .substring(0, 3) + .split('') + .map((e) => { + return e.charCodeAt(0); + }), + c = atob(jpegtpl).split(''); + c[162] = String.fromCharCode(o[1]); + c[160] = String.fromCharCode(o[2]); + return base64data ? + `data:image/jpeg;base64,${btoa(c.concat(p).join(''))}` : + null; + }; + + const loadFeed = () => { + console.log(`${NAME}: loading`); + + document.querySelectorAll(`.${NAME}`).forEach((el, i) => { + const ID = `InstagramFeed${i}`; + const dataset = el.dataset; + + el.classList.add(`${NAME}-loading`); + el.classList.remove(`${NAME}-loaded`, `${NAME}-error`); + + new InstagramFeed({ + username: dataset['username'], + tag: dataset['tag'] || null, + display_profile: dataset['display-profile'], + display_biography: dataset['display-biography'], + display_gallery: dataset['display-gallery'], + display_captions: dataset['display-captions'], + cache_time: dataset['cache_time'] || 360, + items: dataset['items'] || 12, + styling: false, + lazy_load: true, + callback: (data) => { + console.log(`${NAME}: data response received`); + + const list = document.createElement('div'); + list.classList.add(`${NAME}-list`, 'row'); + el.appendChild(list); + + data['edge_owner_to_timeline_media']['edges'].forEach( + (el, i) => { + const item = el['node']; + const preview = ig_media_preview(item['media_preview']); + + list.innerHTML += + `
` + + `${item['accessibility_caption']}` + + '
'; + }, + ); + + el.classList.remove(`${NAME}-loading`); + el.classList.add(`${NAME}-loaded`); + + window.dispatchEvent(new Event('MetaWindowindow.initLinks')); + window.dispatchEvent(new Event(`${NAME}.loaded`)); + }, + on_error: (e) => { + console.error(`${NAME}: ${e}`); + + el.classList.remove(`${NAME}-loading`); + el.classList.add(`${NAME}-error`); + + window.dispatchEvent(new Event(`${NAME}.error`)); + }, + }); + }); + }; + + window.addEventListener(`${Events.LODEDANDREADY}`, loadFeed); + window.addEventListener(`${Events.AJAX}`, loadFeed); +})(window); diff --git a/src/js/test-build.js b/src/js/test-build.js new file mode 100644 index 0000000..a4efbd0 --- /dev/null +++ b/src/js/test-build.js @@ -0,0 +1,57 @@ +'use strict'; + +import '../scss/test-build.scss'; +import '@a2nt/meta-lightbox-js/src/js/test-build'; + +import Events from './_events'; +import MainUI from './_main/_index'; + +/* + * AJAX functionality + */ +import './_ajax/_links'; +import './_ajax/_online'; +import './_ajax/_lazy-images'; + +import './_layout'; + +if (process.env.NODE_ENV === 'development') { + // mocking service worker + const regeneratorRuntime = require('regenerator-runtime'); + const { + worker, + } = require('../mocks/browser'); + worker.start({ + serviceWorker: { + url: '_graphql/mockServiceWorker.js', + options: { + scope: '/', + }, + }, + }); + + // caching service worker (set injectClient: false at webpack.config.serve.js) + /*if ('serviceWorker' in navigator) { + const baseHref = (document.getElementsByTagName('base')[0] || {}).href; + const version = (document.querySelector('meta[name="swversion"]') || {}) + .content; + if (baseHref) { + navigator.serviceWorker + .register(`${baseHref}app_sw.js?v=${version}`) + .then(() => { + console.log('SW: Registered'); + }); + } + }*/ +} + +function importAll(r) { + return r.keys().map(r); +} + +const images = importAll( + require.context('../img/', false, /\.(png|jpe?g|svg)$/), +); +const fontAwesome = importAll( + require.context('font-awesome', false, /\.(otf|eot|svg|ttf|woff|woff2)$/), +); diff --git a/src/scss/_layout/_forms/_basics.scss b/src/scss/_layout/_forms/_basics.scss new file mode 100644 index 0000000..7c3b803 --- /dev/null +++ b/src/scss/_layout/_forms/_basics.scss @@ -0,0 +1,30 @@ +.field { + flex-direction: row; + + &__label { + padding-right: $form-spacer-x; + display: inline-flex; + align-items: center; + + &+.field__content { + padding-left: $form-spacer-x; + } + } + + .field__content { + flex: 1 1 auto; + } + + &.CompositeField { + flex-direction: column; + } +} + +.field.password { + .show-password { + position: absolute; + top: 0.5em; + right: 0.5em; + color: $input-color; + } +} diff --git a/src/scss/_layout/_forms/_index.scss b/src/scss/_layout/_forms/_index.scss new file mode 100644 index 0000000..e427371 --- /dev/null +++ b/src/scss/_layout/_forms/_index.scss @@ -0,0 +1 @@ +@import './_basics'; diff --git a/src/scss/_layout/_index.scss b/src/scss/_layout/_index.scss new file mode 100644 index 0000000..79a4078 --- /dev/null +++ b/src/scss/_layout/_index.scss @@ -0,0 +1,5 @@ +@import '../_variables'; +@import '../_animations'; + +@import './_main'; +@import './_forms'; diff --git a/src/scss/_layout/_main/_alerts.scss b/src/scss/_layout/_main/_alerts.scss new file mode 100644 index 0000000..8341110 --- /dev/null +++ b/src/scss/_layout/_main/_alerts.scss @@ -0,0 +1,32 @@ +@import '../../_variables'; + +#SiteWideAlerts { + position: fixed; + bottom: 0; + right: 0; + z-index: 99999; + + .btn-close { + background: none; + } + + .alert { + margin-bottom: 0; + } +} + +.alert-offline { + display: none; +} + +.is-online { + .alert-offline { + display: none; + } +} + +.is-offline { + .alert-offline { + display: flex; + } +} diff --git a/src/scss/_layout/_main/_base.scss b/src/scss/_layout/_main/_base.scss new file mode 100644 index 0000000..24ec8e1 --- /dev/null +++ b/src/scss/_layout/_main/_base.scss @@ -0,0 +1,179 @@ +/* + * some basic styles + */ + +@import '../../_variables'; +@import '../../_animations'; + +html, +body { + min-height: 100%; + min-height: 100vh; +} + +// sticky footer +body { + display: flex; + flex-direction: column; + --body-gutter-x: #{inspect($body-gutter-x)}; + --body-gutter-y: #{inspect($body-gutter-y)}; + --body-double-gutter-x: #{inspect($body-double-gutter-x)}; + --body-double-gutter-y: #{inspect($body-double-gutter-y)}; + --body-gutter-reduced-x: #{inspect($body-gutter-reduced-x)}; + --body-gutter-reduced-y: #{inspect($body-gutter-reduced-y)}; + --body-gutter-reduced-d-x: #{inspect($body-gutter-reduced-d-x)}; + --body-gutter-reduced-d-y: #{inspect($body-gutter-reduced-d-y)}; + + .wrapper { + flex: 1 0 auto; + margin-bottom: $element-spacer-y; + } + + .footer { + flex-shrink: 0; + margin-top: $element-spacer-y; + } +} + +@media (min-width: $extra-large-screen) { + + html, + body { + font-size: .9vw !important; + } + + .container { + max-width: 80vw; + } +} + +// don't let images be wider than the parent layer +div, +a, +span, +button, +i { + background-repeat: no-repeat; + background-size: contain; +} + +iframe, +img { + max-width: 100%; +} + +ul, +table, +p { + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } +} + +.a { + cursor: pointer; + color: $link-color; + text-decoration: $link-decoration; + + &:hover, + &:focus { + text-decoration: $link-hover-decoration; + color: $link-hover-color; + } +} + +// exclude bootstrap-table +[data-toggle='table'] { + + &:hover, + &.active, + &:focus { + opacity: 1; + } +} + +[data-toggle='collapse'] { + &[aria-expanded='true'] { + .accordion-icon { + &:before { + content: '\f068'; + } + } + } +} + +// transactions +.transition, +a, +a *, +.a, +.a *, +button, +input, +optgroup, +select, +textarea, +.btn, +.btn *, +.dropdown, +.row, +.alert, +.alert *, +.message, +[data-toggle], +[data-toggle] * { + transition: all 0.4s ease; +} + +.a, +a, +[data-toggle], +button, +.btn { + + &:hover, + &.active, + &[aria-expanded='true'] { + + >.fa, + >.far, + >.fas, + >.fab, + &.fa, + &.far, + &.fas, + &.fab { + transform: scale(1.5); + } + } + + &:hover, + &[aria-expanded='true'] { + opacity: 0.8; + } + + &.disabled { + opacity: 0.5; + cursor: default; + + &:hover, + &.active, + &[aria-expanded='true'] { + + >.fa, + >.far, + >.fas, + >.fab, + &.fa, + &.far, + &.fas, + &.fab { + transform: rotate(0deg); + } + } + } +} diff --git a/src/scss/_layout/_main/_index.scss b/src/scss/_layout/_main/_index.scss new file mode 100644 index 0000000..6a76856 --- /dev/null +++ b/src/scss/_layout/_main/_index.scss @@ -0,0 +1,9 @@ +@import '../../_variables'; +@import '../../_animations'; + +@import './_base'; +@import './_main'; +@import './_alerts'; + +// states +@import './_states'; diff --git a/src/scss/_layout/_main/_main.scss b/src/scss/_layout/_main/_main.scss new file mode 100644 index 0000000..e98ea22 --- /dev/null +++ b/src/scss/_layout/_main/_main.scss @@ -0,0 +1,176 @@ +/* + * some basic styles + */ + +.meta-MetaWindow { + z-index: 1031; + + .meta-nav { + text-decoration: none; + } +} + +.pulse { + animation: pulse 0.8s linear infinite; +} + +// navs +.navbar-toggler { + transition: transform ease 0.4s; +} + +.navbar-toggler-icon { + width: auto; + height: auto; +} + +.nav-item, +.nav-link { + display: flex; +} + +button.nav-link { + border: 0; + outline: 0; + text-transform: inherit; + letter-spacing: inherit; +} + +.navbar-toggler { + &[aria-expanded='true'] { + transform: rotate(90deg); + } +} + +.dropdown-toggle { + position: relative; + padding-right: 1.5em; + + &:after { + position: absolute; + right: 0.5em; + bottom: 1em; + } +} + +.navbar-nav .dropdown-toggle.nav-link { + padding-right: 1.5em; +} + +.dropdown.show .dropdown-toggle::after, +.dropdown-toggle.active-dropdown::after, +.dropdown-toggle.active::after { + transform: rotate(-90deg); +} + +.dropdown-menu { + padding: 0; + border-radius: 0; + will-change: max-height, display; + overflow: hidden; + transition: none; + + &.show { + animation: expand 2s; + animation-fill-mode: both; + overflow: visible; + } + + .dropdown-list { + @extend .list-unstyled; + } + + .dropdown-menu { + top: 0; + left: 100%; + } +} + +.dropdown-item { + white-space: normal; +} + +.field { + position: relative; + display: flex; + flex-wrap: wrap; + margin: $form-spacer-y 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } +} + +.btn-toolbar { + margin-top: $form-spacer-y; +} + +// rewrite btn opacity on hover +.btn { + + &:hover, + &.active, + &:focus { + opacity: 1; + } +} + +// SS-messages + +.alert+.alert { + border-top: 0; +} + +/*.message { + @extend .alert; + + @extend .alert-info; + + display: block; + margin: 0.5rem 0; +} + +.message.validation, +.message.required, +.message.error { + @extend .alert; + + @extend .alert-danger; +} + +.message.required, +.message.error { + @extend .alert; + + @extend .alert-danger; +}*/ + +.list-group-item.active { + + a, + .a { + color: $list-group-active-color; + } +} + +[aria-expanded='true'] { + .fa-bars { + &:before { + content: '\f00d'; + } + } +} + +.jsSidebarUI { + position: relative; + min-height: 100%; +} + +.jsSidebarUI__inner { + position: relative; + will-change: position, top; +} diff --git a/src/scss/_layout/_main/_states/_index.scss b/src/scss/_layout/_main/_states/_index.scss new file mode 100644 index 0000000..d891a0d --- /dev/null +++ b/src/scss/_layout/_main/_states/_index.scss @@ -0,0 +1,5 @@ +@import '../../../_variables'; +@import '../../../_animations'; + +@import './_mobile'; +@import './_network'; diff --git a/src/scss/_layout/_main/_states/_mobile.scss b/src/scss/_layout/_main/_states/_mobile.scss new file mode 100644 index 0000000..5d257fd --- /dev/null +++ b/src/scss/_layout/_main/_states/_mobile.scss @@ -0,0 +1,43 @@ +/* + * Mobile/Desktop states + */ + +// display dropdown on hover + focus +@media (min-width: $full-body-min-width) { + .dropdown-hover { + + &:hover, + &:focus { + .dropdown-menu { + display: block; + } + } + } +} + +// custom toggler for mobile view +.dropdown { + >.dropdown-toggle-sm { + @media (min-width: $full-body-min-width) { + display: none; + } + } + + >.dropdown-toggle-fl { + display: none; + + @media (min-width: $full-body-min-width) { + display: inherit; + } + } + + @media not all and (hover: none) { + >.dropdown-toggle-touch { + display: inherit; + } + + >.dropdown-toggle-notouch { + display: none; + } + } +} diff --git a/src/scss/_layout/_main/_states/_network.scss b/src/scss/_layout/_main/_states/_network.scss new file mode 100644 index 0000000..ad1529a --- /dev/null +++ b/src/scss/_layout/_main/_states/_network.scss @@ -0,0 +1,39 @@ +/* + * Network States + */ + +.loading { + animation: fade 0.5s linear infinite; +} + +.graphql-page { + &.response-404 { + filter: grayscale(1); + opacity: 0.5; + cursor: not-allowed; + } +} + +.is-offline { + iframe { + display: none; + } + + .graphql-page { + &.response-523 { + filter: grayscale(1); + opacity: 0.5; + cursor: not-allowed; + } + } +} + +body.ajax-loading { + overflow: hidden; + height: 100vh; + + #Header { + position: relative; + z-index: 2001; + } +} diff --git a/src/scss/_layout/_test.scss b/src/scss/_layout/_test.scss new file mode 100644 index 0000000..81571d9 --- /dev/null +++ b/src/scss/_layout/_test.scss @@ -0,0 +1,177 @@ +.sidebar__col { + position: relative; + margin-top: $element-reduced-spacer-y; + margin-bottom: $element-reduced-spacer-y; +} +.content-holder__sidebar { + > .container { + padding: 0; + } +} + +#SiteWideMessage { + text-align: center; + .alert { + margin-bottom: 0; + .btn-close { + margin-top: -0.5rem; + float: right; + } + } +} + +#Header { + background-color: $header-bg; + color: $header-color; + + a { + color: $header-link; + } + + .nav-container { + display: flex; + justify-content: flex-end; + align-items: flex-end; + position: static; + } + + .logo { + filter: invert(100%); + } + + .tagline { + display: inline-block; + font-size: 1.2rem; + margin-left: 2em; + } +} + +#Navigation { + font-size: 1.5rem; + text-transform: uppercase; + letter-spacing: 0.25rem; + width: 100%; + background: $header-bg; + + .navbar-toggler { + color: $main-nav-link-color; + font-size: $main-nav-toggler-size; + } + + .nav-item, + .nav-link { + flex-direction: column; + /*@media (min-width: $full-body-min-width) { + align-items: center; + justify-content: center; + text-align: center; + }*/ + } + + .nav-link { + color: $main-nav-link-color; + background: $main-nav-link-bg; + + &:focus, + &:hover, + &.active { + background: $main-nav-link-hover-bg; + color: $main-nav-link-hover-color; + } + } + + .active { + > .nav-link { + background: $main-nav-link-hover-bg; + color: $main-nav-link-hover-color; + } + } + + .nav-item .nav-dropdown { + .fa-chevron-right + //&:after + { + display: none; + } + } + + .dropdown-menu { + border-color: $main-nav-dropdown-bg; + background: $main-nav-dropdown-bg; + margin-top: 0; + border-top: 0; + min-width: 100%; + .nav-item-link { + color: $main-nav-dropdown-color; + } + } + + .dropdown-item { + &.active, + &:active, + &:focus, + &:hover { + background: $main-nav-dropdown-hover-bg; + .nav-item-link { + color: $main-nav-dropdown-hover-color; + } + } + .nav-item-link { + width: 100%; + justify-content: flex-start; + align-items: flex-start; + } + } + + @media (min-width: $full-body-min-width) { + .navbar-nav > .nav-item { + padding-right: 2rem; + padding-left: 2rem; + } + .dropdown-item .nav-item-link { + padding-left: 1rem; + padding-right: 1rem; + } + } +} + +/*#MainContent { + padding-top: 2 * $element-reduced-spacer-y; + padding-bottom: 2 * $element-reduced-spacer-y; +}*/ + +#PageBreadcumbs { + position: relative; + z-index: 2; +} + +#Footer { + display: flex; + flex-direction: column; + background-color: $footer-bg; + color: $footer-color; + + > .wrapper { + padding-top: $element-reduced-spacer-y; + padding-bottom: $element-reduced-spacer-y; + } + + a, + .a { + color: $footer-link; + } + + .footer { + padding-top: $element-reduced-spacer-y; + padding-bottom: $element-reduced-spacer-y; + background-color: $footer-footer-bg; + + .copyright { + padding-right: 0.5rem; + } + + li { + padding: 0 0.5rem; + } + } +} diff --git a/src/scss/_libs/_bootstrap-table.scss b/src/scss/_libs/_bootstrap-table.scss new file mode 100644 index 0000000..2b118fe --- /dev/null +++ b/src/scss/_libs/_bootstrap-table.scss @@ -0,0 +1,39 @@ +@import "~bootstrap-table/src/bootstrap-table.scss"; + +.bootstrap-table { + .fixed-table-container { + .table { + thead th { + .both, .asc, .desc { + background-image: none; + + &:after { + margin-left: .5em; + content: ''; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + } + } + + .asc:after { + content: "\f0de"; + } + + .desc:after { + content: "\f0dd"; + } + + .both:after { + content: "\f0dc"; + } + + .th-inner.sortable { + &:hover, + &:focus { + opacity: .8; + } + } + } + } + } +} diff --git a/src/scss/_libs/_bootstrap.scss b/src/scss/_libs/_bootstrap.scss new file mode 100644 index 0000000..4402a2e --- /dev/null +++ b/src/scss/_libs/_bootstrap.scss @@ -0,0 +1,51 @@ +// Bootstrap +// Configuration +@import '~bootstrap/scss/functions'; +@import '~bootstrap/scss/variables'; +@import '~bootstrap/scss/mixins'; +@import '~bootstrap/scss/utilities'; + +// Layout & components +@import '~bootstrap/scss/root'; +@import '~bootstrap/scss/reboot'; +@import '~bootstrap/scss/type'; +@import '~bootstrap/scss/containers'; +@import '~bootstrap/scss/grid'; +@import '~bootstrap/scss/tables'; +@import '~bootstrap/scss/forms'; +@import '~bootstrap/scss/buttons'; +@import '~bootstrap/scss/transitions'; + +// Optional +//@import '~bootstrap/scss/images'; +@import '~bootstrap/scss/dropdown'; +@import '~bootstrap/scss/nav'; +@import '~bootstrap/scss/navbar'; +@import '~bootstrap/scss/breadcrumb'; +@import '~bootstrap/scss/pagination'; +@import '~bootstrap/scss/alert'; +@import '~bootstrap/scss/close'; + +/*@import '~bootstrap/scss/button-group'; +@import '~bootstrap/scss/card'; +@import '~bootstrap/scss/accordion'; +@import '~bootstrap/scss/badge'; +@import '~bootstrap/scss/progress'; +@import '~bootstrap/scss/list-group'; +@import '~bootstrap/scss/toasts'; +@import '~bootstrap/scss/modal'; +@import '~bootstrap/scss/tooltip'; +@import '~bootstrap/scss/popover'; +@import '~bootstrap/scss/spinners';*/ + +// Helpers +@import '~bootstrap/scss/helpers'; + +// Utilities +@import '~bootstrap/scss/utilities/api'; + +@import '../_ui/carousel'; + +.navbar { + justify-content: flex-end; +} diff --git a/src/scss/_libs/_fontawesome.scss b/src/scss/_libs/_fontawesome.scss new file mode 100644 index 0000000..b5c3c8d --- /dev/null +++ b/src/scss/_libs/_fontawesome.scss @@ -0,0 +1,3 @@ +$fa-font-path: "~font-awesome/fonts"; + +@import "~font-awesome/scss/font-awesome"; diff --git a/src/scss/_libs/_silverstripe.scss b/src/scss/_libs/_silverstripe.scss new file mode 100644 index 0000000..e8627c2 --- /dev/null +++ b/src/scss/_libs/_silverstripe.scss @@ -0,0 +1,57 @@ +.message { + @extend .alert !optional; + + &.warning { + @extend .alert-warning !optional; + } + + &.error { + @extend .alert-danger !optional; + } +} + +.embed-responsive-4by3, +.embed-responsive-16by9 { + position: relative; + padding-top: 56.25%; + + iframe { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + } +} + +.embed-responsive-4by3 { + padding-top: 75%; +} + +#ForgotPassword { + margin: 1rem 0; + width: 100%; +} + +#BetterNavigator { + display: none; + top: 50% !important; + margin-top: -41px; + + &.open { + top: 0 !important; + margin-top: 0; + } + + a, + button, + div, + i, + span { + background-size: auto; + } + + @media (min-width: map-get($grid-breakpoints, 'md')) { + display: block; + } +} diff --git a/src/scss/_libs/_silverstripe.shop.scss b/src/scss/_libs/_silverstripe.shop.scss new file mode 100644 index 0000000..ada7ca5 --- /dev/null +++ b/src/scss/_libs/_silverstripe.shop.scss @@ -0,0 +1,8 @@ +.cart-footer { + margin-top: $grid-gutter-height / 2; +} + +.address-panel, +.account-nav { + margin-bottom: $grid-gutter-height / 2; +} diff --git a/src/scss/_ui/_carousel.scss b/src/scss/_ui/_carousel.scss new file mode 100644 index 0000000..e4f9527 --- /dev/null +++ b/src/scss/_ui/_carousel.scss @@ -0,0 +1,93 @@ +@import '~bootstrap/scss/carousel'; + +/* + * Bootstrap carousel improvement + */ + +/*.carousel-item { + &.active { + display: flex !important; + justify-content: center; + align-items: flex-start; + } +}*/ + +$carousel-title-color: $white !default; +$carousel-slide-min-height: 4rem !default; +$carousel-text-shadow: 1px 1px $black !default; +$carousel-controls-font-size: 3rem; +$carousel-controls-zindex: 11 !default; +$carousel-controls-shadow: 1px 1px $black !default; +$carousel-controls-hover-bg: transparentize($black, 0.4) !default; +$carousel-slide-img-loading-max-height: 25vh !default; + +.carousel-slide { + min-height: $carousel-slide-min-height; + display: flex; + justify-content: center; + align-items: flex-start; + flex-direction: column; + + >.container { + position: relative; + } + + .video { + width: 100%; + + iframe { + width: 100% !important; + height: auto !important; + } + } + + .img { + display: block; + width: 100%; + } + + img.loading { + max-height: $carousel-slide-img-loading-max-height; + } +} + +.carousel-control-prev, +.carousel-control-next { + z-index: $carousel-controls-zindex; + font-size: $carousel-controls-font-size; + text-shadow: $carousel-controls-shadow; + + &:hover, + &:focus { + background: $carousel-controls-hover-bg; + } +} + +.carousel-indicators li { + box-shadow: none; +} + +.carousel-title { + color: $carousel-title-color; +} + +.carousel-title, +.carousel-content { + text-shadow: $carousel-text-shadow; +} + +.carousel-caption { + right: 0; + left: auto; + width: 50%; + bottom: 0; +} + +.slide-link__media { + position: absolute; + opacity: 0; + left: 0; + right: 0; + top: 0; + bottom: 0; +} diff --git a/src/scss/_ui/_flyout.scss b/src/scss/_ui/_flyout.scss new file mode 100644 index 0000000..639e216 --- /dev/null +++ b/src/scss/_ui/_flyout.scss @@ -0,0 +1,34 @@ +$flyout-height-padding: 1rem; +$flyout-width-padding: 2rem; +$flyout-padding: $flyout-height-padding $flyout-width-padding; +$flyout-bg: #000 !default; +$flyout-color: #fff !default; +$flyout-title-color: #fff !default; +$flyout-transition: right 2s; + +.flyout-FlyoutUI { + position: absolute; + z-index: 99; + transform: translateY(-50%); + transition: $flyout-transition; + right: -100%; + top: 50%; + background: $flyout-bg; + color: $flyout-color; + padding: $flyout-padding; + + &__active { + display: block; + right: 0; + } + + &__title { + color: $flyout-title-color; + } + + &__close { + position: absolute; + top: $flyout-height-padding; + right: $flyout-width-padding; + } +} diff --git a/src/scss/_ui/_form.stepped.scss b/src/scss/_ui/_form.stepped.scss new file mode 100644 index 0000000..aa6a372 --- /dev/null +++ b/src/scss/_ui/_form.stepped.scss @@ -0,0 +1,9 @@ +.form-stepped { + .step { + display: none !important; + + &.active { + display: flex !important; + } + } +} diff --git a/src/scss/_ui/_lightbox.scss b/src/scss/_ui/_lightbox.scss new file mode 100644 index 0000000..a5efdad --- /dev/null +++ b/src/scss/_ui/_lightbox.scss @@ -0,0 +1,14 @@ +@import '../_variables'; + +/*$lightbox-breakpoint: map-get($grid-breakpoints, 'sm') !default; +$lightbox-link-hover-color: $link-hover-color !default; + +@import '~@a2nt/meta-lightbox/src/scss/app'; + +.lightbox-overlay-custom { + @extend .meta-lightbox-overlay; + @extend .meta-lightbox-theme-default; + @extend .meta-lightbox-effect-fade; + // meta-lightbox-open +} +*/ diff --git a/src/scss/_ui/_mailchimp.scss b/src/scss/_ui/_mailchimp.scss new file mode 100644 index 0000000..a444c1c --- /dev/null +++ b/src/scss/_ui/_mailchimp.scss @@ -0,0 +1,53 @@ +#mc_embed_signup, +.mc_embed_signup { + padding: 2rem; + .mc-field-group { + @extend .form-group; + } + input[type='text'], + input[type='email'] { + @extend .form-control; + } + input[type='submit'] { + @extend .btn; + @extend .btn-primary; + margin: 0 auto; + width: 50%; + display: block; + } + .clear { + float: none; + clear: both; + } + + .input-group { + @extend .form-check; + ul, + li { + list-style: none; + } + input[type='checkbox'] { + @extend .form-check-input; + } + + label { + @extend .form-check-label; + } + } + + .mce_inline_error, + #mce-success-response, + #mce-error-response { + margin-top: 1rem; + @extend .alert; + } + + #mce-success-response { + @extend .alert-success; + } + + .mce_inline_error, + #mce-error-response { + @extend .alert-danger; + } +} diff --git a/src/scss/_ui/_map.api.scss b/src/scss/_ui/_map.api.scss new file mode 100644 index 0000000..72bacf7 --- /dev/null +++ b/src/scss/_ui/_map.api.scss @@ -0,0 +1,134 @@ +@import '../_variables'; +@import '../_animations'; + +//@import "~mapbox-gl/src/css/mapbox-gl.css"; +$map-height: 30rem !default; + +$map-marker-color: $primary !default; +$map-marker-size: 30px !default; + +$map-popup-font-size: 0.8rem !default; +$map-popup-width: 16rem !default; +$map-popup-height: 7rem !default; +$map-popup-bg: $white !default; +$map-popup-color: $body-color !default; + +.mapAPI-map { + height: $map-height; + //margin-bottom: $grid-gutter-element-height; +} + +.mapboxgl { + &-popup { + width: $map-popup-width; + height: $map-popup-height; + font-size: $map-popup-font-size; + line-height: 1.2em; + position: absolute; + top: 0; + left: 0; + display: flex; + pointer-events: none; + z-index: 4; + } + + &-popup-anchor-bottom, + &-popup-anchor-bottom-left, + &-popup-anchor-bottom-right { + flex-direction: column-reverse; + } + + &-popup-content { + min-width: $map-popup-width; + background: $map-popup-bg; + color: $map-popup-color; + position: relative; + pointer-events: auto; + padding: 0.8rem; + border-radius: 0.25rem; + min-height: 5rem; + box-shadow: 0 0.1rem 0.8rem 0 rgba(0, 0, 0, 0.4); + } + + &-popup-close-button { + position: absolute; + right: 0; + top: 0; + font-size: 2rem; + padding: 0.5rem; + border-top-right-radius: 0.25rem; + z-index: 2; + + &:hover, + &:focus { + background: $primary; + color: $white; + } + } + + &-popup-tip { + width: 0; + height: 0; + border: 0.8rem solid transparent; + z-index: 1; + } + + &-popup-anchor-bottom &-popup-tip { + border-top-color: $map-popup-bg; + align-self: center; + border-bottom: none; + } + + &-marker { + width: $map-marker-size; + height: $map-marker-size; + font-size: $map-marker-size; + line-height: 1em; + color: $map-marker-color; + cursor: pointer; + text-align: center; + display: flex; + align-items: flex-end; + justify-content: center; + + .marker-icon, + .fas, + .fab, + .far { + animation: pulse 0.8s linear infinite; + } + } + + &-cluster { + background: $info; + color: color-yiq($info); + border-radius: 100%; + font-weight: bold; + font-size: 1.2rem; + display: flex; + align-items: center; + animation: pulse 0.8s linear infinite; + + &::before, + &::after { + content: ""; + display: block; + position: absolute; + width: 140%; + height: 140%; + + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + background: $info; + opacity: 0.2; + border-radius: 100%; + z-index: -1; + } + + &::after { + width: 180%; + height: 180%; + } + } +} diff --git a/src/scss/_ui/_multislider.scss b/src/scss/_ui/_multislider.scss new file mode 100644 index 0000000..493cc41 --- /dev/null +++ b/src/scss/_ui/_multislider.scss @@ -0,0 +1,55 @@ +@import '../_variables'; +$grid-gutter-element-height: 2rem !default; + +.jsMultiSlider { + position: relative; + display: flex; + margin-bottom: $grid-gutter-element-height/2; + + align-items: center; + justify-content: center; + min-width: 100%; + + &-active { + margin-bottom: 0; + } + + .slide { + position: relative; + padding: 0 0.5rem; + } +} + +.jsMultiSlider-container { + position: relative; + margin-bottom: $grid-gutter-element-height/2; + + .slider-actions { + font-size: 2rem; + .act { + position: absolute; + top: 0; + bottom: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + &-slider-prev { + } + &-slider-next { + left: auto; + right: 0; + } + } + } +} + +.jsMultiSlider-slides-container { + overflow: hidden; + margin: 0 2rem; + + > .slider-nav { + position: relative; + } +} diff --git a/src/scss/test-build.scss b/src/scss/test-build.scss new file mode 100644 index 0000000..af87c95 --- /dev/null +++ b/src/scss/test-build.scss @@ -0,0 +1,3 @@ +@import 'app'; +@import '_layout/_test'; +@import '~bootstrap/scss/accordion';