mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-02-07 07:53:00 -05:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
82
frontend/src/static/js/components/page-layout/PageHeader/PageHeader.js
Executable file
82
frontend/src/static/js/components/page-layout/PageHeader/PageHeader.js
Executable 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>
|
||||
);
|
||||
}
|
||||
339
frontend/src/static/js/components/page-layout/PageHeader/PageHeader.scss
Executable file
339
frontend/src/static/js/components/page-layout/PageHeader/PageHeader.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
187
frontend/src/static/js/components/page-layout/PageHeader/SearchField.scss
Executable file
187
frontend/src/static/js/components/page-layout/PageHeader/SearchField.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
frontend/src/static/js/components/page-layout/PageMain.jsx
Normal file
13
frontend/src/static/js/components/page-layout/PageMain.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
frontend/src/static/js/components/page-layout/PageMain.scss
Executable file
57
frontend/src/static/js/components/page-layout/PageMain.scss
Executable 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);
|
||||
}
|
||||
}
|
||||
134
frontend/src/static/js/components/page-layout/PageSidebar.jsx
Normal file
134
frontend/src/static/js/components/page-layout/PageSidebar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
250
frontend/src/static/js/components/page-layout/PageSidebar.scss
Executable file
250
frontend/src/static/js/components/page-layout/PageSidebar.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
import './PageSidebarContentOverlay.scss';
|
||||
|
||||
export const PageSidebarContentOverlay = () => <div className="page-sidebar-content-overlay"></div>;
|
||||
36
frontend/src/static/js/components/page-layout/PageSidebarContentOverlay.scss
Executable file
36
frontend/src/static/js/components/page-layout/PageSidebarContentOverlay.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
frontend/src/static/js/components/page-layout/index.js
Normal file
4
frontend/src/static/js/components/page-layout/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './PageHeader/PageHeader';
|
||||
export * from './PageMain';
|
||||
export * from './PageSidebar';
|
||||
export * from './PageSidebarContentOverlay';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()];
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user