Frontent dev env (#247)

* Added frontend development files/environment

* More items-categories related removals

* Improvements in pages templates (inc. static pages)

* Improvements in video player

* Added empty home page message + cta

* Updates in media, playlist and management pages

* Improvements in material icons font loading

* Replaced media & playlists links in frontend dev-env

* frontend package version update

* chnaged frontend dev url port

* static files update

* Changed default position of theme switcher

* enabled frontend docker container
This commit is contained in:
Yiannis Stergiou
2021-07-11 18:01:34 +03:00
committed by GitHub
parent 060bb45725
commit aa6520daac
555 changed files with 201927 additions and 66002 deletions

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { PageStore } from '../../../utils/stores/';
import { LinksConsumer, SiteConsumer } from '../../../utils/contexts/';
import { useLayout, useTheme } from '../../../utils/hooks/';
import { CircleIconButton } from '../../_shared';
import { Logo } from './Logo';
export function HeaderLeft() {
const { logo } = useTheme();
const { enabledSidebar, toggleMobileSearch, toggleSidebar } = useLayout();
return (
<SiteConsumer>
{(site) => (
<LinksConsumer>
{(links) => (
<div className="page-header-left">
<div>
<div className="close-search-field">
<CircleIconButton onClick={toggleMobileSearch}>
<i className="material-icons">arrow_back</i>
</CircleIconButton>
</div>
{enabledSidebar ? (
<div className="toggle-sidebar">
<CircleIconButton onClick={toggleSidebar}>
<i className="material-icons">menu</i>
</CircleIconButton>
</div>
) : null}
<Logo src={logo} href={links.home} title={site.title} />
{PageStore.get('config-contents').header.onLogoRight ? (
<div
className="on-logo-right"
dangerouslySetInnerHTML={{ __html: PageStore.get('config-contents').header.onLogoRight }}
></div>
) : null}
</div>
</div>
)}
</LinksConsumer>
)}
</SiteConsumer>
);
}

View File

@@ -0,0 +1,192 @@
import React from 'react';
import { useLayout, usePopup } from '../../../utils/hooks/';
import { PageStore } from '../../../utils/stores/';
import { HeaderConsumer, MemberConsumer, LinksConsumer } from '../../../utils/contexts/';
import { CircleIconButton, MaterialIcon, NavigationContentApp, NavigationMenuList, PopupTop, PopupMain, UserThumbnail } from '../../_shared';
import { HeaderThemeSwitcher } from './HeaderThemeSwitcher';
function headerPopupPages(user, popupNavItems, hasHeaderThemeSwitcher) {
const pages = {
main: null,
};
if (user.is.anonymous) {
pages.main = (
<div>
<PopupMain>
<NavigationMenuList items={popupNavItems.middle} />
</PopupMain>
</div>
);
} else {
const NavMenus = [];
function insertNavMenus(id, arr) {
if (arr.length) {
if (NavMenus.length) {
NavMenus.push(<hr key={id + '-nav-seperator'} />);
}
NavMenus.push(<NavigationMenuList key={id + '-nav'} items={arr} />);
}
}
insertNavMenus('top', popupNavItems.top);
insertNavMenus('middle', popupNavItems.middle);
insertNavMenus('bottom', popupNavItems.bottom);
pages.main = (
<div>
<PopupTop>
<a className="user-menu-top-link" href={user.pages.about} title={user.username}>
<span>
<UserThumbnail size="medium" />
</span>
<span>
<span className="username">{user.username}</span>
</span>
</a>
</PopupTop>
{NavMenus.length ? <PopupMain>{NavMenus}</PopupMain> : null}
</div>
);
}
if (hasHeaderThemeSwitcher) {
pages['switch-theme'] = (
<div>
<PopupTop>
<div>
<span>
<CircleIconButton className="menu-item-icon change-page" data-page-id="main" aria-label="Switch theme">
<i className="material-icons">arrow_back</i>
</CircleIconButton>
</span>
<span>Switch theme</span>
</div>
</PopupTop>
<PopupMain>
<HeaderThemeSwitcher />
</PopupMain>
</div>
);
}
return pages;
}
function UploadMediaButton({ user, links }) {
return !user.is.anonymous && user.can.addMedia ? (
<div className={'hidden-only-in-small'}>
<CircleIconButton type="link" href={links.user.addMedia} title="Upload media">
<MaterialIcon type="video_call" />
<span className="hidden-txt">Upload media</span>
</CircleIconButton>
</div>
) : null;
}
function LoginButton({ user, link, hasHeaderThemeSwitcher }) {
return user.is.anonymous && user.can.login ? (
<div className="sign-in-wrap">
<a
href={link}
rel="noffolow"
className={
'button-link sign-in' + (hasHeaderThemeSwitcher ? ' hidden-only-in-small' : ' hidden-only-in-extra-small')
}
title="Sign in"
>
Sign in
</a>
</div>
) : null;
}
function RegisterButton({ user, link, hasHeaderThemeSwitcher }) {
return user.is.anonymous && user.can.register ? (
<div className="register-wrap">
<a
href={link}
className={
'button-link register-link' +
(hasHeaderThemeSwitcher ? ' hidden-only-in-small' : ' hidden-only-in-extra-small')
}
title="Register"
>
Register
</a>
</div>
) : null;
}
export function HeaderRight(props) {
const { toggleMobileSearch } = useLayout();
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
return (
<HeaderConsumer>
{(header) => (
<MemberConsumer>
{(user) => (
<LinksConsumer>
{(links) => (
<div className="page-header-right">
<div>
<div className="mobile-search-toggle">
<CircleIconButton onClick={toggleMobileSearch} aria-label="Search">
<MaterialIcon type="search" />
</CircleIconButton>
</div>
<UploadMediaButton user={user} links={links} />
<div
className={
(user.is.anonymous ? 'user-options' : 'user-thumb') +
(!user.is.anonymous || header.hasThemeSwitcher ? '' : ' visible-only-in-extra-small')
}
>
<PopupTrigger contentRef={popupContentRef}>
{user.is.anonymous ? (
<CircleIconButton aria-label="Settings">
<MaterialIcon type="more_vert" />
</CircleIconButton>
) : (
<UserThumbnail size="small" isButton={true} />
)}
</PopupTrigger>
<PopupContent contentRef={popupContentRef}>
<NavigationContentApp
initPage="main"
pages={headerPopupPages(user, header.popupNavItems, header.hasThemeSwitcher)}
pageChangeSelector={'.change-page'}
pageIdSelectorAttr={'data-page-id'}
/>
</PopupContent>
</div>
<LoginButton user={user} link={links.signin} hasHeaderThemeSwitcher={header.hasThemeSwitcher} />
<RegisterButton
user={user}
link={links.register}
hasHeaderThemeSwitcher={header.hasThemeSwitcher}
/>
{PageStore.get('config-contents').header.right ? (
<div
className="on-header-right"
dangerouslySetInnerHTML={{ __html: PageStore.get('config-contents').header.right }}
></div>
) : null}
</div>
</div>
)}
</LinksConsumer>
)}
</MemberConsumer>
)}
</HeaderConsumer>
);
}

View File

@@ -0,0 +1,48 @@
import React, { useRef } from 'react';
import { useTheme } from '../../../utils/hooks/';
import './ThemeSwitchOption.scss';
export function HeaderThemeSwitcher() {
const { currentThemeMode, changeThemeMode } = useTheme();
const inputRef = useRef(null);
function onKeyPress(ev) {
if (0 === ev.keyCode) {
changeThemeMode();
}
}
function onClick(ev) {
if (ev.target !== inputRef.current) {
changeThemeMode();
}
}
function onChange(ev) {
ev.stopPropagation();
changeThemeMode();
}
return (
<div className="theme-switch" tabIndex={0} onKeyPress={onKeyPress} onClick={onClick}>
<span>Dark Theme</span>
<span>
<label className="checkbox-label right-selectbox">
<span className="checkbox-switcher-wrap">
<span className="checkbox-switcher">
<input
ref={inputRef}
type="checkbox"
tabIndex={-1}
checked={'dark' === currentThemeMode}
onChange={onChange}
/>
</span>
</span>
</label>
</span>
</div>
);
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
export const Logo = ({ src, loading = 'lazy', title, alt, href = '#' }) => {
return src ? (
<div className="logo">
<a href={href} title={title}>
<span>
<img src={src} alt={alt || title} title={title} loading={loading} />
</span>
</a>
</div>
) : null;
};

View File

@@ -0,0 +1,82 @@
import React, { useEffect } from 'react';
import { PageStore } from '../../../utils/stores/';
import { useUser, useLayout } from '../../../utils/hooks/';
import { addClassname } from '../../../utils/helpers/';
import { SearchField } from './SearchField';
import { HeaderRight } from './HeaderRight';
import { HeaderLeft } from './HeaderLeft';
import '../../../../css/styles.scss';
import './PageHeader.scss';
import '../PageMain.scss';
function Alerts() {
function onClickAlertClose() {
const alertElem = this.parentNode;
addClassname(alertElem, 'hiding');
setTimeout(
function () {
if (alertElem && alertElem.parentNode) {
alertElem.parentNode.removeChild(alertElem);
}
}.bind(this),
400
);
}
setTimeout(
function () {
const closeBtn = document.querySelectorAll('.alert.alert-dismissible .close');
let i;
if (closeBtn.length) {
i = 0;
while (i < closeBtn.length) {
closeBtn[i].addEventListener('click', onClickAlertClose);
i += 1;
}
}
}.bind(this),
1000
); // TODO: Improve this.
}
function MediaUploader() {
let uploaderWrap = document.querySelector('.media-uploader-wrap');
if (uploaderWrap) {
let preUploadMsgEl = document.createElement('div');
preUploadMsgEl.setAttribute('class', 'pre-upload-msg');
preUploadMsgEl.innerHTML = PageStore.get('config-contents').uploader.belowUploadArea;
uploaderWrap.appendChild(preUploadMsgEl);
}
}
export function PageHeader(props) {
const { isAnonymous } = useUser();
const { visibleMobileSearch } = useLayout();
useEffect(() => {
Alerts();
if (void 0 === PageStore.get('current-page') || 'add-media' === PageStore.get('current-page')) {
MediaUploader();
}
}, []);
return (
<header
className={
'page-header' + (visibleMobileSearch ? ' mobile-search-field' : '') + (isAnonymous ? ' anonymous-user' : '')
}
>
<HeaderLeft />
<SearchField />
<HeaderRight />
</header>
);
}

View File

@@ -0,0 +1,339 @@
@import '../../../../css/includes/_variables.scss';
@import '../../../../css/includes/_variables_dimensions.scss';
.page-header {
background-color: var(--header-bg-color);
.circle-icon-button {
color: var(--header-circle-button-color);
}
.page-header-right {
.popup {
.nav-menu {
li {
color: var(--header-popup-menu-color);
.material-icons {
color: var(--header-popup-menu-icon-color);
}
&.link-item {
a {
color: var(--header-popup-menu-color);
}
}
}
}
}
}
}
.page-header {
z-index: +6;
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--header-height);
display: table;
width: 100%;
&:after {
content: '';
position: absolute;
bottom: -5px;
right: 0;
width: 100%;
height: 5px;
left: 0;
opacity: 1;
pointer-events: none;
box-shadow: inset 0px 4px 8px -3px rgba(17, 17, 17, 0.06);
}
}
.page-header-left,
.page-header-right {
position: absolute;
top: 0;
width: auto;
height: 100%;
display: table;
display: table-cell;
> * {
display: table;
height: 100%;
height: var(--header-height);
> * {
height: 100%;
display: table-cell;
vertical-align: middle;
}
}
}
.page-header-right {
padding-right: $header-padding-horizontal;
@media screen and (max-width: 709px) {
padding-right: 8px;
}
}
.page-header-left {
left: 0;
padding-right: 104px;
> * > * {
padding-left: 8px;
@media screen and (min-width: 710px) {
padding-left: 16px;
}
}
> * > * {
padding-right: 16px;
}
}
@media screen and (min-width: 640px) and (max-width: 1023px) {
.page-header-left {
max-width: 55%;
}
.page-header-right {
max-width: 45%;
}
}
$__button-link-vertical-padding: 10;
$__button-link-horizontal-padding: 16;
.page-header-right {
right: 0;
> * > * {
padding-right: 8px;
padding-right: 6px;
@media screen and (max-width: 368px) {
padding-right: 0;
}
}
.button-link {
padding: ($__button-link-vertical-padding * 1px) ($__button-link-horizontal-padding * 1px);
}
.popup {
position: absolute;
top: 100%;
right: 8px;
margin-top: -8px;
max-width: calc(100vw - 38px);
@media screen and (max-width: 1007px) {
right: 16px;
}
@media screen and (max-width: 479px) {
right: 16px;
}
@media screen and (max-width: 359px) {
right: 12px;
}
@media screen and (min-width: 1007px) {
.anonymous-user & {
right: 0;
}
}
}
}
.mobile-search-toggle {
@media screen and (min-width: 1024px) {
display: none;
}
}
.user-thumb {
padding: 0;
}
.user-thumb,
.user-options {
@media screen and (min-width: 1008px) {
position: relative; // Changes popup position.
}
}
.user-thumb {
width: 48px;
@media screen and (min-width: 768px) {
width: 60px;
}
text-align: center;
}
.sign-in-wrap,
.register-wrap {
padding: 0;
}
.button-link.sign-in,
.button-link.register-link {
color: var(--brand-color, var(--default-brand-color));
font-weight: 500;
line-height: 1;
display: block;
text-transform: uppercase;
}
.signin-icon-link {
color: var(--brand-color, var(--default-brand-color));
}
.close-search-field {
display: none;
}
a.user-menu-top-link {
display: table;
padding: 8px;
color: inherit;
text-decoration: none;
&:focus {
outline: var(--dotted-outline);
}
> * {
display: table-cell;
vertical-align: middle;
&:first-child {
width: 56px;
}
&:last-child {
}
.username {
display: block;
font-weight: 500;
line-height: 1.25;
}
.videos-count {
display: block;
line-height: 1.5;
font-size: 0.875em;
}
}
}
.logo {
padding: 0;
margin: 0;
font-size: 1.25em;
font-weight: 300;
@media screen and (max-width: 359px) {
font-size: 1em;
}
a {
color: inherit;
display: block;
&:focus {
span {
&:before {
content: '';
display: block;
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border: 1px dotted var(--body-text-color);
opacity: 0.5;
}
}
}
}
span {
position: relative;
display: block;
float: left;
}
span > img,
picture {
position: relative;
float: left;
max-width: 100%;
max-height: var(--logo-height, var(--default-logo-height));
}
}
@media screen and (max-width: 1023px) {
.mobile-search-field {
.toggle-sidebar,
.logo,
.page-header-right {
display: none;
}
.close-search-field {
display: table-cell;
padding-left: 8px;
padding-right: 8px;
@media screen and (min-width: 710px) {
padding-left: 16px;
padding-right: 16px;
}
}
.page-header-left {
position: relative;
top: auto;
left: auto;
float: left;
}
}
}
@media screen and (max-width: 709px) {
.mobile-search-field {
.close-search-field {
padding-left: 4px;
}
}
}
@media screen and (max-width: 479px) {
.toggle-sidebar {
padding-right: 8px;
}
}
@media screen and (max-width: 359px) {
.toggle-sidebar {
padding-right: 4px;
}
}

View File

@@ -0,0 +1,322 @@
import React, { useState, useEffect, useRef } from 'react';
import { useLayout, usePopup } from '../../../utils/hooks/';
import { LinksContext } from '../../../utils/contexts/';
import { PageStore, SearchFieldStore } from '../../../utils/stores/';
import { SearchFieldActions } from '../../../utils/actions/';
import { MaterialIcon, PopupMain } from '../../_shared';
import './SearchField.scss';
function indexesOf(source, find, caseSensitive) {
let i,
result = [];
caseSensitive = !!caseSensitive;
for (i = 0; i < source.length; ++i) {
if (caseSensitive) {
if (source.substring(i, i + find.length) === find) {
result.push(i);
}
} else {
if (source.substring(i, i + find.length).toLowerCase() === find.toLowerCase()) {
result.push(i);
}
}
}
return result;
}
function SearchPredictionItemList(props) {
const maxHeightValue = () => window.innerHeight - 1.75 * 56;
const onWindowResize = () => setMaxHeight(maxHeightValue());
const [maxHeight, setMaxHeight] = useState(maxHeightValue());
useEffect(() => {
PageStore.on('window_resize', onWindowResize);
return () => PageStore.removeListener('window_resize', onWindowResize);
});
return (
<div className="search-predictions-list" style={{ maxHeight: maxHeight + 'px' }}>
{props.children || null}
</div>
);
}
function SearchPredictionItem(props) {
const containerRef = useRef(null);
function onKeydown(ev) {
let item;
switch (ev.keyCode || ev.charCode) {
case 13: // Enter/Select.
onClick();
break;
case 38: // Arrow Up.
item = props.itemsDomArray(props.previousIndex);
break;
case 40: // Arrow Down.
item = props.itemsDomArray(props.nextIndex);
break;
}
if (void 0 !== item) {
item.focus();
ev.preventDefault();
ev.stopPropagation();
}
}
function onFocus(ev) {
ev.target.onkeydown = onKeydown;
}
function onBlur(ev) {
ev.target.onkeydown = null;
}
function onClick() {
if (props.onSelect instanceof Function) {
props.onSelect(props.value);
}
}
useEffect(() => {
props.onPredictionItemLoad(props.index, containerRef.current);
});
return (
<div
ref={containerRef}
tabIndex="0"
className="search-predictions-item"
onFocus={onFocus}
onBlur={onBlur}
onClick={onClick}
>
<span dangerouslySetInnerHTML={{ __html: props.children || '' }} />
</div>
);
}
export function SearchField(props) {
const searchInputRef = useRef(null);
const formRef = useRef(null);
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
const [itemsDomRef, setItemsDomRef] = useState([]);
const [predictionItems, setPredictionItems] = useState([]);
const [queryVal, setQueryVal] = useState(SearchFieldStore.get('search-query'));
const { visibleMobileSearch } = useLayout();
function getItemsArr(index) {
return -1 === index ? searchInputRef.current : itemsDomRef[index];
}
function onInputFocus() {
if (predictionItems.length) {
searchInputRef.current.onkeydown = searchInputRef.current.onkeydown || onKeydown;
}
}
function onInputBlur() {
searchInputRef.current.onkeydown = null;
}
function onKeydown(e) {
const key = e.keyCode || e.charCode;
let found = false;
switch (key) {
case 38: // Up Arrow.
found = getItemsArr(predictionItems.length - 1);
break;
case 40: // Down Arrow.
found = getItemsArr(0);
break;
}
if (found) {
found.focus();
e.preventDefault();
e.stopPropagation();
}
}
function onQueryChange(ev) {
let val = ev.target.value;
val = 'string' !== typeof val ? val.toString() : val;
setQueryVal(val);
if ('' !== val.trim()) {
SearchFieldActions.requestPredictions(val.trim());
}
}
function onPredictionSelect(val) {
setPredictionItems([]);
setQueryVal(val);
setTimeout(function () {
formRef.current.submit();
}, 50);
}
function onPredictionItemLoad(index, domItem) {
const items = itemsDomRef;
items[index] = domItem;
setItemsDomRef(items);
}
function onLoadPredictions(val, arr) {
let i, j, useItems, itemTxt, pos;
let prevItem, nextItem;
let items = [];
if (val) {
useItems = [];
i = 0;
while (i < arr.length) {
itemTxt = arr[i];
pos = indexesOf(arr[i], val, false);
// NOTE: Disabled to allow display results that don't include the query string (eg. found by tag name).
/*if( ! pos.length ){
i+=1;
continue;
}*/
if (pos.length) {
j = pos.length - 1;
while (j >= 0) {
itemTxt =
itemTxt.substring(0, pos[j]) +
'<b>' +
itemTxt.substring(pos[j], pos[j] + val.length) +
'</b>' +
itemTxt.substring(pos[j] + val.length);
j--;
}
}
useItems.push([arr[i], itemTxt]);
i += 1;
}
i = 0;
while (i < useItems.length) {
if (0 === i) {
prevItem = -1;
nextItem = i + 1;
} else if (i === useItems.length - 1) {
prevItem = i - 1;
nextItem = -1;
} else {
prevItem = i - 1;
nextItem = i + 1;
}
items.push(
<SearchPredictionItem
key={i}
index={i}
onPredictionItemLoad={onPredictionItemLoad}
value={useItems[i][0]}
onSelect={onPredictionSelect}
itemsDomArray={getItemsArr}
nextIndex={nextItem}
previousIndex={prevItem}
>
{useItems[i][1]}
</SearchPredictionItem>
);
i += 1;
}
}
setPredictionItems(items);
}
function onFormSubmit(ev) {
if ('' === searchInputRef.current.value.trim()) {
ev.preventDefault();
ev.stopPropagation();
}
}
function onPopupHide() {
setPredictionItems([]);
}
useEffect(() => {
if (visibleMobileSearch) {
searchInputRef.current.focus();
}
}, [visibleMobileSearch]);
useEffect(() => {
if (predictionItems.length) {
searchInputRef.current.onkeydown = searchInputRef.current.onkeydown || onKeydown;
popupContentRef.current.tryToShow();
} else {
searchInputRef.current.onkeydown = null;
popupContentRef.current.tryToHide();
}
}, [predictionItems]);
useEffect(() => {
SearchFieldStore.on('load_predictions', onLoadPredictions);
return () => {
SearchFieldStore.removeListener('load_predictions', onLoadPredictions);
};
}, []);
return (
<div className="search-field-wrap">
<div>
<form
ref={formRef}
method="get"
action={LinksContext._currentValue.search.base}
autoComplete="off"
onSubmit={onFormSubmit}
>
<div>
<div className="text-field-wrap">
<input
ref={searchInputRef}
type="text"
placeholder="Search"
aria-label="Search"
name="q"
value={queryVal}
onChange={onQueryChange}
onFocus={onInputFocus}
onBlur={onInputBlur}
/>
<PopupContent contentRef={popupContentRef} hideCallback={onPopupHide}>
<PopupMain>
<SearchPredictionItemList>{predictionItems}</SearchPredictionItemList>
</PopupMain>
</PopupContent>
</div>
<button type="submit" aria-label="Search">
<MaterialIcon type="search" />
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,187 @@
.search-field-wrap {
input[type='text'],
button[type='submit'] {
border-color: var(--search-field-input-border-color);
}
input[type='text'] {
color: var(--search-field-input-text-color);
background-color: var(--search-field-input-bg-color);
}
button[type='submit'] {
color: var(--search-field-submit-text-color);
background-color: var(--search-field-submit-bg-color);
border-color: var(--search-field-submit-border-color);
&:hover,
&:focus {
background-color: var(--search-field-submit-bg-color);
border-color: var(--search-field-submit-border-color);
}
}
}
.search-field-wrap {
position: relative;
display: table;
$xx: 5.6;
width: 480px;
max-width: 48%;
max-width: 40%;
height: 100%;
margin: 0 auto;
text-align: center;
> div {
width: 100%;
display: table-cell;
vertical-align: middle;
}
form {
position: relative;
width: 100%;
text-align: right;
}
.text-field-wrap {
display: block;
padding-right: 64px;
.popup {
z-index: -1;
position: absolute;
top: 100%;
left: 0;
right: 64px;
width: auto;
margin-top: 12px;
color: rgb(34, 34, 34);
border-width: 0px 1px 1px;
border-style: solid;
border-color: #ccc;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
}
input[type='text'],
button[type='submit'] {
border-style: solid;
}
input[type='text'] {
width: 100%;
padding: 0 10px;
font-size: 16.52px;
border-width: 1px;
border-radius: 2px 0 0 2px;
}
button[type='submit'] {
position: absolute;
top: 0;
bottom: 0;
right: 0;
width: 64px;
padding: 0;
border-width: 1px 1px 1px 0;
border-radius: 0 2px 2px 0;
.material-icons {
opacity: 0.6;
margin-bottom: 1px;
overflow: hidden;
}
&:hover,
&:focus {
outline: 0;
.material-icons {
opacity: 1;
}
}
}
@media screen and (max-width: 1023px) {
display: none;
.mobile-search-field & {
position: absolute;
right: 0;
top: 0;
right: 16px;
display: block;
left: 56px + 16px;
margin: auto 0;
width: auto;
max-width: 100%;
> div {
display: table;
height: 100%;
}
form {
display: table-cell;
vertical-align: middle;
> div {
position: relative;
}
}
}
}
@media screen and (max-width: 709px) {
.mobile-search-field & {
left: 48px;
}
}
@media screen and (min-width: 1220px) {
width: 640px;
}
}
.search-field-wrap {
input[type='text'],
button[type='submit'] {
line-height: 1.875;
}
button[type='submit'] {
.material-icons {
font-size: 22px;
line-height: 1;
}
}
}
.search-predictions-list {
position: relative;
padding-top: 16px;
padding-bottom: 8px;
line-height: 1.375;
overflow: auto;
}
.search-predictions-item {
display: block;
padding: 4px 24px 4px 10px;
b {
font-weight: 500;
}
&:hover,
&:focus {
outline: 0;
background-color: #eee;
}
}

View File

@@ -0,0 +1,39 @@
@import '../../../../css/includes/_variables.scss';
@import '../../../../css/includes/_variables_dimensions.scss';
.theme-switch {
position: relative;
display: table;
width: 100%;
@if $_use_rem_unit {
padding: 0 1.5rem;
margin: 0.75rem 0;
} @else {
padding: 0 24px;
margin: 12px 0;
}
&:focus {
outline: var(--dotted-outline);
}
> * {
display: table-cell;
vertical-align: middle;
line-height: 40px;
cursor: pointer;
&:last-child {
text-align: right;
}
}
.checkbox-label.right-selectbox {
margin: 0;
.selectbox {
margin: 0;
}
}
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { useLayout } from '../../utils/hooks/';
import { PageSidebarContentOverlay } from './PageSidebarContentOverlay';
export function PageMain(props) {
const { enabledSidebar } = useLayout();
return (
<div className="page-main">
{props.children || null}
{enabledSidebar ? <PageSidebarContentOverlay /> : null}
</div>
);
}

View File

@@ -0,0 +1,57 @@
.page-main-wrap {
padding-top: var(--header-height);
will-change: padding-left;
@media (min-width: 768px) {
.visible-sidebar & {
padding-left: var(--sidebar-width);
opacity: 1;
}
}
.visible-sidebar #page-media & {
padding-left: 0;
}
.visible-sidebar & {
#page-media {
padding-left: 0;
}
}
body.sliding-sidebar & {
transition-property: padding-left;
transition-duration: 0.2s;
}
}
#page-profile-media,
#page-profile-playlists,
#page-profile-about,
#page-liked.profile-page-liked,
#page-history.profile-page-history {
.page-main {
min-height: calc(100vh - var(--header-height));
}
}
.page-main {
position: relative;
width: 100%;
padding-bottom: 16px;
}
.page-main-inner {
display: block;
margin: 1em 1em 0 1em;
}
#page-profile-media,
#page-profile-playlists,
#page-profile-about,
#page-liked.profile-page-liked,
#page-history.profile-page-history {
.page-main-wrap {
background-color: var(--body-bg-color);
}
}

View File

@@ -0,0 +1,134 @@
import React, { useRef, useState, useEffect } from 'react';
import { useLayout } from '../../utils/hooks/';
import { PageStore } from '../../utils/stores/';
import { SidebarNavigationMenu } from './sidebar/SidebarNavigationMenu';
import { SidebarBelowNavigationMenu } from './sidebar/SidebarBelowNavigationMenu';
import { SidebarBelowThemeSwitcher } from './sidebar/SidebarBelowThemeSwitcher';
import { SidebarThemeSwitcher } from './sidebar/SidebarThemeSwitcher';
import { SidebarBottom } from './sidebar/SidebarBottom';
import './PageSidebar.scss';
export function PageSidebar() {
const { visibleSidebar, toggleSidebar } = useLayout();
const containerRef = useRef(null);
const [isRendered, setIsRendered] = useState(visibleSidebar || 492 > window.innerWidth);
const [isFixedBottom, setIsFixedBottom] = useState(true);
let sidebarBottomDom = null;
let sidebarBottomDomPrevSibling = null;
let bottomInited = false;
let isAbsoluteThemeSwitcher = false;
function initBottom() {
if (bottomInited || !PageStore.get('config-contents').sidebar.footer) {
return;
}
sidebarBottomDom = document.querySelector('.page-sidebar-bottom');
sidebarBottomDomPrevSibling = sidebarBottomDom.previousSibling;
if ('relative' !== getComputedStyle(sidebarBottomDomPrevSibling).position) {
isAbsoluteThemeSwitcher = true;
}
bottomInited = true;
PageStore.on('window_resize', onWindowResize);
let cntr = 0;
let sameCntr = 0;
let siblingBottomPosition = 0;
function bottomInitPos() {
const newSiblingBottomPosition = sidebarBottomDomPrevSibling.offsetTop + sidebarBottomDomPrevSibling.offsetHeight;
if (newSiblingBottomPosition !== siblingBottomPosition) {
siblingBottomPosition = newSiblingBottomPosition;
} else {
sameCntr += 1;
}
cntr += 1;
// Check every 10ms, until there is no change within 100ms or passed 500ms.
if (10 > sameCntr && 50 > cntr) {
setTimeout(bottomInitPos, 10);
}
onWindowResize();
}
bottomInitPos();
}
function onWindowResize() {
let prevElem = sidebarBottomDomPrevSibling;
let bottomElHeight = sidebarBottomDom.offsetHeight;
if (isAbsoluteThemeSwitcher) {
bottomElHeight += prevElem.offsetHeight;
prevElem = prevElem.previousSibling;
}
setIsFixedBottom(
!(
prevElem.offsetTop + prevElem.offsetHeight + bottomElHeight >
window.innerHeight - containerRef.current.offsetTop
)
);
}
function onClickSidebarContentOverlay(ev) {
ev.preventDefault();
ev.stopPropagation();
toggleSidebar();
}
useEffect(() => {
setIsRendered(true);
setTimeout(initBottom, 20); // Must delay at least 20ms.
}, [visibleSidebar]);
useEffect(() => {
if (visibleSidebar || isRendered) {
initBottom();
}
const sidebarContentOverlay = document.querySelector('.page-sidebar-content-overlay');
if (sidebarContentOverlay) {
sidebarContentOverlay.addEventListener('click', onClickSidebarContentOverlay);
}
return () => {
if (bottomInited) {
PageStore.removeListener('window_resize', onWindowResize);
}
if (sidebarContentOverlay) {
sidebarContentOverlay.removeEventListener('click', onClickSidebarContentOverlay);
}
};
}, []);
return (
<div ref={containerRef} className={'page-sidebar' + (isFixedBottom ? ' fixed-bottom' : '')}>
<div className="page-sidebar-inner">
{visibleSidebar || isRendered ? (
<>
<SidebarNavigationMenu />
<SidebarBelowNavigationMenu />
<SidebarThemeSwitcher />
<SidebarBelowThemeSwitcher />
<SidebarBottom />
</>
) : null}
</div>
</div>
);
}

View File

@@ -0,0 +1,250 @@
.page-sidebar {
background-color: var(--sidebar-bg-color);
.nav-menu + .nav-menu {
border-top-color: var(--sidebar-nav-border-color);
}
.nav-menu {
color: var(--sidebar-nav-item-text-color);
.menu-item-icon {
color: var(--sidebar-nav-item-icon-color);
max-width: 24px;
overflow: hidden;
}
}
.page-sidebar-bottom {
background-color: var(--sidebar-bg-color);
a {
color: var(--sidebar-bottom-link-color);
}
}
}
.page-sidebar {
z-index: +6;
@media (min-width: 768px) {
z-index: +5;
}
position: fixed;
display: block;
top: var(--header-height);
left: 0;
bottom: 0;
width: var(--sidebar-width);
overflow: auto;
transform: translate(calc(-1 * var(--sidebar-width)), 0px);
.visible-sidebar & {
transform: translate(0px, 0px);
}
body.sliding-sidebar & {
transition-property: transform;
transition-duration: 0.2s;
}
.page-sidebar-inner {
display: block;
max-height: 100%;
}
.nav-menu + .nav-menu {
border-top-width: 1px;
border-top-style: solid;
}
.page-sidebar-bottom {
position: relative;
width: 100%;
float: left;
padding: 1rem 1.5rem;
font-size: 12px;
color: rgb(136, 136, 136);
a {
text-decoration: none;
}
}
&.fixed-bottom {
.page-sidebar-bottom {
position: absolute;
bottom: 0;
left: 0;
}
}
&.rendering {
.page-sidebar-bottom {
opacity: 0;
}
}
}
.sidebar-theme-switcher {
position: relative;
width: 100%;
float: left;
display: block;
padding: 24px 24px;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--sidebar-nav-border-color);
}
.sidebar-theme-switcher-inner {
display: table;
width: 100%;
> * {
display: table-cell;
vertical-align: middle;
text-align: center;
width: 44px;
&:first-child {
width: auto;
text-align: right;
padding-right: 10px;
i {
font-size: 21px;
}
}
&:last-child {
width: auto;
text-align: left;
padding-left: 14px;
i {
font-size: 21px;
transform: rotate(140deg);
margin-top: -1px;
}
}
}
.theme-icon {
i {
color: var(--sidebar-nav-item-icon-color);
}
&.active {
i {
color: var(--theme-color, var(--default-theme-color));
}
}
}
}
.checkbox-switcher-wrap {
position: relative;
width: 36px;
display: inline-block;
vertical-align: middle;
text-align: center;
margin-top: -2px;
margin-left: 8px;
.checkbox-switcher {
height: 15px;
input[type='checkbox'] {
&:after {
// top: -4px;
width: 20px;
height: 20px;
}
&:checked {
&:after {
background: var(--theme-color, var(--default-theme-color));
}
}
}
}
}
.checkbox-switcher {
position: relative;
width: 100%;
height: 17px;
display: block;
margin: 0 auto;
input[type='checkbox'] {
position: absolute;
display: block;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
outline: 0;
border: 0;
background: none;
cursor: pointer;
-webkit-appearance: none;
-webkit-tap-highlight-color: transparent;
&:before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--logged-in-user-thumb-bg-color);
border-radius: 24px;
}
&:after {
content: '';
display: block;
position: absolute;
left: 0;
top: -3px;
width: 22px;
height: 22px;
border-radius: 50%;
background: #fff;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.15);
}
&:focus,
&:active,
&:checked {
outline: 0;
background: none;
box-shadow: none;
}
&:checked {
&:after {
left: 100%;
margin-left: -17px;
box-shadow: -1px 1px 3px rgba(0, 0, 0, 0.6);
}
}
}
input[type='checkbox'],
input[type='checkbox']:before,
input[type='checkbox']:after,
input[type='checkbox']:checked:before,
input[type='checkbox']:checked:after {
transition: ease 0.2s;
-webkit-transition: ease 0.2s;
-moz-transition: ease 0.2s;
-o-transition: ease 0.2s;
}
}

View File

@@ -0,0 +1,4 @@
import React from 'react';
import './PageSidebarContentOverlay.scss';
export const PageSidebarContentOverlay = () => <div className="page-sidebar-content-overlay"></div>;

View File

@@ -0,0 +1,36 @@
@import '../../../css/includes/_variables_dimensions.scss';
.page-sidebar-content-overlay {
position: fixed;
top: $header-height;
left: 0;
right: 0;
bottom: 0;
z-index: +4;
background-color: #000;
opacity: 0;
visibility: hidden;
transform: translateZ(0);
transition-property: opacity;
transition-duration: 0.2s;
#page-media &,
#page-media-video &,
#page-media-audio &,
#page-media-image &,
#page-media-pdf & {
body.visible-sidebar & {
display: block;
opacity: 0.5;
visibility: visible;
}
}
@media (max-width: 767px) {
body.visible-sidebar & {
display: block;
opacity: 0.5;
visibility: visible;
}
}
}

View File

@@ -0,0 +1,4 @@
export * from './PageHeader/PageHeader';
export * from './PageMain';
export * from './PageSidebar';
export * from './PageSidebarContentOverlay';

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { PageStore } from '../../../utils/stores/';
export function SidebarBelowNavigationMenu() {
const content = PageStore.get('config-contents').sidebar.belowNavMenu;
return content ? (
<div className="page-sidebar-under-nav-menus" dangerouslySetInnerHTML={{ __html: content }}></div>
) : null;
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { PageStore } from '../../../utils/stores/';
export function SidebarBelowThemeSwitcher() {
const content = PageStore.get('config-contents').sidebar.belowThemeSwitcher;
return content ? (
<div className="page-sidebar-below-theme-switcher" dangerouslySetInnerHTML={{ __html: content }}></div>
) : null;
}

View File

@@ -0,0 +1,7 @@
import React from 'react';
import { PageStore } from '../../../utils/stores/';
export function SidebarBottom() {
const content = PageStore.get('config-contents').sidebar.footer;
return content ? <div className="page-sidebar-bottom" dangerouslySetInnerHTML={{ __html: content }}></div> : null;
}

View File

@@ -0,0 +1,230 @@
import React, { useContext } from 'react';
import urlParse from 'url-parse';
import { useUser } from '../../../utils/hooks/';
import { PageStore } from '../../../utils/stores/';
import { LinksContext, SidebarContext } from '../../../utils/contexts/';
import { NavigationMenuList } from '../../_shared';
export function SidebarNavigationMenu() {
const { userCan, isAnonymous, pages: userPages } = useUser();
const links = useContext(LinksContext);
const sidebar = useContext(SidebarContext);
const currentUrl = urlParse(window.location.href);
const currentHostPath = (currentUrl.host + currentUrl.pathname).replace(/\/+$/, '');
function formatItems(items) {
return items.map((item) => {
const url = urlParse(item.link);
const active = currentHostPath === url.host + url.pathname;
return {
active,
itemType: 'link',
link: item.link || '#',
icon: item.icon || null,
iconPos: 'left',
text: item.text || item.link || '#',
itemAttr: {
className: item.className || '',
},
};
});
}
function MainMenuFirstSection() {
const items = [];
if (!sidebar.hideHomeLink) {
items.push({
link: links.home,
icon: 'home',
text: 'Home',
className: 'nav-item-home',
});
}
if (PageStore.get('config-enabled').pages.featured && PageStore.get('config-enabled').pages.featured.enabled) {
items.push({
link: links.featured,
icon: 'star',
text: PageStore.get('config-enabled').pages.featured.title,
className: 'nav-item-featured',
});
}
if (
PageStore.get('config-enabled').pages.recommended &&
PageStore.get('config-enabled').pages.recommended.enabled
) {
items.push({
link: links.recommended,
icon: 'done_outline',
text: PageStore.get('config-enabled').pages.recommended.title,
className: 'nav-item-recommended',
});
}
if (PageStore.get('config-enabled').pages.latest && PageStore.get('config-enabled').pages.latest.enabled) {
items.push({
link: links.latest,
icon: 'new_releases',
text: PageStore.get('config-enabled').pages.latest.title,
className: 'nav-item-latest',
});
}
if (
!sidebar.hideTagsLink &&
PageStore.get('config-enabled').taxonomies.tags &&
PageStore.get('config-enabled').taxonomies.tags.enabled
) {
items.push({
link: links.archive.tags,
icon: 'local_offer',
text: PageStore.get('config-enabled').taxonomies.tags.title,
className: 'nav-item-tags',
});
}
if (
!sidebar.hideCategoriesLink &&
PageStore.get('config-enabled').taxonomies.categories &&
PageStore.get('config-enabled').taxonomies.categories.enabled
) {
items.push({
link: links.archive.categories,
icon: 'list_alt',
text: PageStore.get('config-enabled').taxonomies.categories.title,
className: 'nav-item-categories',
});
}
if (PageStore.get('config-enabled').pages.members && PageStore.get('config-enabled').pages.members.enabled) {
items.push({
link: links.members,
icon: 'people',
text: PageStore.get('config-enabled').pages.members.title,
className: 'nav-item-members',
});
}
const extraItems = PageStore.get('config-contents').sidebar.mainMenuExtra.items;
extraItems.forEach((navitem) => {
items.push({
link: navitem.link,
icon: navitem.icon,
text: navitem.text,
className: navitem.className,
});
});
return items.length ? <NavigationMenuList key="main-first" items={formatItems(items)} /> : null;
}
function MainMenuSecondSection() {
const items = [];
if (!isAnonymous) {
if (userCan.addMedia) {
items.push({
link: links.user.addMedia,
icon: 'video_call',
text: 'Upload media',
className: 'nav-item-upload-media',
});
if (userPages.media) {
items.push({
link: userPages.media,
icon: 'video_library',
text: 'My media',
className: 'nav-item-my-media',
});
}
}
if (userCan.saveMedia) {
items.push({
link: userPages.playlists,
icon: 'playlist_play',
text: 'My playlists',
className: 'nav-item-my-playlists',
});
}
}
return items.length ? <NavigationMenuList key="main-second" items={formatItems(items)} /> : null;
}
function UserMenuSection() {
const items = [];
if (PageStore.get('config-enabled').pages.history && PageStore.get('config-enabled').pages.history.enabled) {
items.push({
link: links.user.history,
icon: 'history',
text: PageStore.get('config-enabled').pages.history.title,
className: 'nav-item-history',
});
}
if (
userCan.likeMedia &&
PageStore.get('config-enabled').pages.liked &&
PageStore.get('config-enabled').pages.liked.enabled
) {
items.push({
link: links.user.liked,
icon: 'thumb_up',
text: PageStore.get('config-enabled').pages.liked.title,
className: 'nav-item-liked',
});
}
return items.length ? <NavigationMenuList key="user" items={formatItems(items)} /> : null;
}
function CustomMenuSection() {
const items = PageStore.get('config-contents').sidebar.navMenu.items;
return items.length ? <NavigationMenuList key="custom" items={formatItems(items)} /> : null;
}
function AdminMenuSection() {
const items = [];
if (userCan.manageMedia) {
items.push({
link: links.manage.media,
icon: 'miscellaneous_services',
text: 'Manage media',
className: 'nav-item-manage-media',
});
}
if (userCan.manageUsers) {
items.push({
link: links.manage.users,
icon: 'miscellaneous_services',
text: 'Manage users',
className: 'nav-item-manage-users',
});
}
if (userCan.manageComments) {
items.push({
link: links.manage.comments,
icon: 'miscellaneous_services',
text: 'Manage comments',
className: 'nav-item-manage-comments',
});
}
return items.length ? <NavigationMenuList key="admin" items={formatItems(items)} /> : null;
}
return [MainMenuFirstSection(), MainMenuSecondSection(), UserMenuSection(), CustomMenuSection(), AdminMenuSection()];
}

View File

@@ -0,0 +1,27 @@
import React, { useEffect } from 'react';
import { useTheme } from '../../../utils/hooks/';
export const SidebarThemeSwitcher: React.FC = () => {
const { currentThemeMode, changeThemeMode, themeModeSwitcher } = useTheme();
return (
themeModeSwitcher.enabled &&
'sidebar' === themeModeSwitcher.position && (
<div className="sidebar-theme-switcher">
<div className="sidebar-theme-switcher-inner">
<span className={'theme-icon' + ('dark' === currentThemeMode ? '' : ' active')}>
<i className="material-icons" data-icon="wb_sunny"></i>
</span>
<span>
<span className="checkbox-switcher">
<input type="checkbox" checked={'dark' === currentThemeMode} onChange={changeThemeMode} />
</span>
</span>
<span className={'theme-icon' + ('dark' === currentThemeMode ? ' active' : '')}>
<i className="material-icons" data-icon="brightness_3"></i>
</span>
</div>
</div>
)
);
};