mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-21 22:07:59 -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:
27
frontend/src/static/js/components/MediaListHeader.tsx
Executable file
27
frontend/src/static/js/components/MediaListHeader.tsx
Executable file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
interface MediaListHeaderProps {
|
||||
title?: string;
|
||||
viewAllLink?: string;
|
||||
viewAllText?: string;
|
||||
className?: string;
|
||||
style?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export const MediaListHeader: React.FC<MediaListHeaderProps> = (props) => {
|
||||
const viewAllText = props.viewAllText || 'VIEW ALL';
|
||||
return (
|
||||
<div className={(props.className ? props.className + ' ' : '') + 'media-list-header'} style={props.style}>
|
||||
<h2>{props.title}</h2>
|
||||
{props.viewAllLink ? (
|
||||
<h3>
|
||||
{' '}
|
||||
<a href={props.viewAllLink} title={viewAllText}>
|
||||
{' '}
|
||||
{viewAllText || props.viewAllLink}{' '}
|
||||
</a>{' '}
|
||||
</h3>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
frontend/src/static/js/components/MediaListRow.tsx
Normal file
21
frontend/src/static/js/components/MediaListRow.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { MediaListHeader } from './MediaListHeader';
|
||||
|
||||
interface MediaListRowProps {
|
||||
title?: string;
|
||||
viewAllLink?: string;
|
||||
viewAllText?: string;
|
||||
className?: string;
|
||||
style?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export const MediaListRow: React.FC<MediaListRowProps> = (props) => {
|
||||
return (
|
||||
<div className={(props.className ? props.className + ' ' : '') + 'media-list-row'} style={props.style}>
|
||||
{props.title ? (
|
||||
<MediaListHeader title={props.title} viewAllLink={props.viewAllLink} viewAllText={props.viewAllText} />
|
||||
) : null}
|
||||
{props.children || null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
372
frontend/src/static/js/components/MediaListWrapper.scss
Executable file
372
frontend/src/static/js/components/MediaListWrapper.scss
Executable file
@@ -0,0 +1,372 @@
|
||||
@import '../../css/config/index.scss';
|
||||
|
||||
.media-list-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.media-list-row {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
clear: both;
|
||||
min-height: 136px;
|
||||
|
||||
+ .media-list-row {
|
||||
border-width: 1px 0 0;
|
||||
border-style: solid;
|
||||
border-color: var(--media-list-row-border-color);
|
||||
}
|
||||
|
||||
.spinner-loader {
|
||||
margin: 3.5rem auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-list-header {
|
||||
}
|
||||
|
||||
.media-list-row {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
.media-list-wrapper {
|
||||
max-width: calc(var(--item-width, var(--default-item-width)) * var(--max-row-items, var(--default-max-row-items)));
|
||||
|
||||
&.items-list-hor,
|
||||
&.items-list-ver {
|
||||
padding: 0 16px;
|
||||
|
||||
@media (min-width: 710px) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.media-list-row {
|
||||
.sliding-sidebar & {
|
||||
transition-property: width;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.items-list-hor {
|
||||
.media-list-row {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.items-list-ver {
|
||||
.media-list-row {
|
||||
max-width: var(--max-item-width, var(--default-max-item-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 710px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
@media (min-width: 400px) {
|
||||
.media-list-wrapper.items-list-hor .media-list-row {
|
||||
max-width: calc(var(--item-width, var(--default-item-width)) * var(--max-row-items, var(--default-max-row-items)));
|
||||
}
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
$item-width: 260px;
|
||||
$item-width: 218px;
|
||||
$side-empty-space: 40px;
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 2 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 2 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(2 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 3 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(3 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
$item-width: 218px;
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 4 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(4 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 5 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(5 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 6 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(6 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 7 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(7 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 8 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(8 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 9 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(9 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 10 * $item-width ) )) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(10 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 401px) and (max-width: 599px) {
|
||||
.media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: var(--max-item-width, var(--default-max-item-width));
|
||||
}
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 3 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(2 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 4 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(3 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 5 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(4 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 6 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(5 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 7 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(6 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 8 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(7 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 9 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(8 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 10 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(9 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 11 * $item-width ) )) and (min-width: 768px) {
|
||||
.visible-sidebar .media-list-wrapper.items-list-ver .media-list-row {
|
||||
width: calc(10 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
.media-list-wrapper .media-filters-row {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 16px 0;
|
||||
|
||||
@media (min-width: 600px) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.media-list-wrapper .media-list-header + .media-filters-row {
|
||||
margin-top: -12px;
|
||||
}
|
||||
|
||||
.media-filters-row + .media-list-header {
|
||||
padding-top: 0;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-filters-row-inner {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.media-type-filters,
|
||||
.media-filters-sort {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--header-circle-button-color);
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.dark_theme & {
|
||||
color: inherit;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
color: var(--header-circle-button-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popup-trigger {
|
||||
.filter-button-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.media-type-filters {
|
||||
margin-right: 8px;
|
||||
|
||||
.popup-trigger {
|
||||
.filter-button-label {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-filters-sort {
|
||||
position: relative;
|
||||
float: right;
|
||||
clear: right;
|
||||
|
||||
.popup-trigger {
|
||||
.filter-button-label {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.007px;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-filter {
|
||||
.media-filter-option-list {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
|
||||
.media-filter-option {
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
text-align: initial;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
color: inherit;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--in-popup-nav-menu-item-hover-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
frontend/src/static/js/components/MediaListWrapper.tsx
Normal file
27
frontend/src/static/js/components/MediaListWrapper.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { MediaListRow } from './MediaListRow';
|
||||
import './MediaListWrapper.scss';
|
||||
|
||||
interface MediaListWrapperProps {
|
||||
title?: string;
|
||||
viewAllLink?: string;
|
||||
viewAllText?: string;
|
||||
className?: string;
|
||||
style?: { [key: string]: any };
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
||||
title,
|
||||
viewAllLink,
|
||||
viewAllText,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
}) => (
|
||||
<div className={(className ? className + ' ' : '') + 'media-list-wrapper'} style={style}>
|
||||
<MediaListRow title={title} viewAllLink={viewAllLink} viewAllText={viewAllText}>
|
||||
{children || null}
|
||||
</MediaListRow>
|
||||
</div>
|
||||
);
|
||||
14
frontend/src/static/js/components/MediaMultiListWrapper.tsx
Executable file
14
frontend/src/static/js/components/MediaMultiListWrapper.tsx
Executable file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import './MediaListWrapper.scss';
|
||||
|
||||
interface MediaMultiListWrapperProps {
|
||||
className?: string;
|
||||
style?: { [key: string]: any };
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export const MediaMultiListWrapper: React.FC<MediaMultiListWrapperProps> = ({ className, style, children }) => (
|
||||
<div className={(className ? className + ' ' : '') + 'media-list-wrapper'} style={style}>
|
||||
{children || null}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './CircleIconButton.scss';
|
||||
|
||||
export function CircleIconButton(props) {
|
||||
const children = (
|
||||
<span>
|
||||
<span>{props.children}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
const attr = {
|
||||
tabIndex: props.tabIndex || null,
|
||||
title: props.title || null,
|
||||
className:
|
||||
'circle-icon-button' +
|
||||
(void 0 !== props.className ? ' ' + props.className : '') +
|
||||
(props.buttonShadow ? ' button-shadow' : ''),
|
||||
};
|
||||
|
||||
if (void 0 !== props['data-page-id']) {
|
||||
attr['data-page-id'] = props['data-page-id'];
|
||||
}
|
||||
|
||||
if (void 0 !== props['aria-label']) {
|
||||
attr['aria-label'] = props['aria-label'];
|
||||
}
|
||||
|
||||
if ('link' === props.type) {
|
||||
return (
|
||||
<a {...attr} href={props.href || null} rel={props.rel || null}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if ('span' === props.type) {
|
||||
return (
|
||||
<span {...attr} onClick={props.onClick || null}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button {...attr} onClick={props.onClick || null}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
CircleIconButton.propTypes = {
|
||||
type: PropTypes.oneOf(['button', 'link', 'span']),
|
||||
buttonShadow: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
CircleIconButton.defaultProps = {
|
||||
type: 'button',
|
||||
buttonShadow: false,
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
.circle-icon-button {
|
||||
--txt-color: rgba(17, 17, 17, 0.4);
|
||||
--bg-color: #fff;
|
||||
|
||||
--bg-focus-color: rgba(0, 0, 0, 0.07);
|
||||
--bg-active-color: rgba(0, 0, 0, 0.11);
|
||||
}
|
||||
|
||||
body.dark_theme .circle-icon-button {
|
||||
--txt-color: rgba(255, 255, 255, 0.5);
|
||||
--bg-color: #272727;
|
||||
|
||||
--bg-focus-color: rgba(255, 255, 255, 0.14);
|
||||
--bg-active-color: rgba(255, 255, 255, 0.34);
|
||||
}
|
||||
|
||||
.circle-icon-button {
|
||||
color: var(--txt-color);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
a.circle-icon-button,
|
||||
button.circle-icon-button {
|
||||
&:focus {
|
||||
> * {
|
||||
background-color: var(--bg-focus-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
> * {
|
||||
background-color: var(--bg-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-player .more-media {
|
||||
// In video player "More videos" section, use dark theme properties.
|
||||
|
||||
a.circle-icon-button,
|
||||
button.circle-icon-button {
|
||||
&:focus {
|
||||
> * {
|
||||
background-color: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
> * {
|
||||
background-color: rgba(0, 0, 0, 0.11);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.circle-icon-button {
|
||||
display: block;
|
||||
padding: 0;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
-webkit-tap-highlight-color: rgba(#000, 0);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
outline-width: 0;
|
||||
border-width: 0;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
> * {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.button-shadow {
|
||||
box-shadow: 0 4px 4px rgba(#000, 0.3), 0 0 4px rgba(#000, 0.2);
|
||||
}
|
||||
|
||||
i {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MaterialIcon } from '../material-icon/MaterialIcon.jsx';
|
||||
|
||||
export function FilterOptions(props) {
|
||||
return props.options.map((filter) => {
|
||||
return (
|
||||
<div key={filter.id} className={filter.id === props.selected ? 'active' : ''}>
|
||||
<button onClick={props.onSelect} filter={props.id} value={filter.id}>
|
||||
<span>{filter.title}</span>
|
||||
{filter.id === props.selected ? <MaterialIcon type="close" /> : null}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
FilterOptions.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
selected: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MaterialIcon } from '../material-icon/MaterialIcon.jsx';
|
||||
|
||||
export function FiltersToggleButton(props) {
|
||||
const [isActive, setIsActive] = useState(props.active);
|
||||
|
||||
function onClick() {
|
||||
setIsActive(!isActive);
|
||||
if (void 0 !== props.onClick) {
|
||||
props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mi-filters-toggle">
|
||||
<button className={isActive ? 'active' : ''} aria-label="Filter" onClick={onClick}>
|
||||
<MaterialIcon type="filter_list" />
|
||||
<span className="filter-button-label">
|
||||
<span className="filter-button-label-text">FILTERS</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FiltersToggleButton.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
};
|
||||
|
||||
FiltersToggleButton.defaultProps = {
|
||||
active: false,
|
||||
};
|
||||
11
frontend/src/static/js/components/_shared/index.js
Normal file
11
frontend/src/static/js/components/_shared/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './circle-icon-button/CircleIconButton.jsx';
|
||||
export * from './filter-options/FilterOptions.jsx';
|
||||
export * from './filters-toggle-button/FiltersToggleButton.jsx';
|
||||
export * from './material-icon/MaterialIcon.jsx';
|
||||
export * from './navigation-content-app/NavigationContentApp.jsx';
|
||||
export * from './navigation-menu-list/NavigationMenuList.jsx';
|
||||
export * from './notifications/Notifications.jsx';
|
||||
export * from './numeric-input-with-unit/NumericInputWithUnit.jsx';
|
||||
export * from './popup/Popup.jsx';
|
||||
export * from './spinner-loader/SpinnerLoader.jsx';
|
||||
export * from './user-thumbnail/UserThumbnail.jsx';
|
||||
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
import './MaterialIcon.scss';
|
||||
export const MaterialIcon = ({ type }) => (type ? <i className="material-icons" data-icon={type}></i> : null);
|
||||
@@ -0,0 +1,11 @@
|
||||
.material-icons {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.material-icons[data-icon]::after {
|
||||
display: block;
|
||||
content: attr(data-icon);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export function NavigationContentApp(props) {
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(null);
|
||||
|
||||
let changePageElements = [];
|
||||
|
||||
function initEvents() {
|
||||
let domElem = findDOMNode(containerRef.current);
|
||||
let elems = domElem.querySelectorAll(props.pageChangeSelector);
|
||||
|
||||
let i, pageId;
|
||||
|
||||
if (elems.length) {
|
||||
i = 0;
|
||||
while (i < elems.length) {
|
||||
pageId = elems[i].getAttribute(props.pageIdSelectorAttr);
|
||||
pageId = pageId ? pageId.trim() : pageId;
|
||||
|
||||
if (pageId) {
|
||||
changePageElements[i] = {
|
||||
id: pageId,
|
||||
elem: elems[i],
|
||||
};
|
||||
|
||||
changePageElements[i].listener = (
|
||||
(index) => (event) =>
|
||||
changePageListener(index, event)
|
||||
)(i);
|
||||
changePageElements[i].elem.addEventListener('click', changePageElements[i].listener);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.focusFirstItemOnPageChange) {
|
||||
domElem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function clearEvents() {
|
||||
let i = 0;
|
||||
while (i < changePageElements.length) {
|
||||
changePageElements[i].elem.removeEventListener('click', changePageElements[i].listener);
|
||||
i += 1;
|
||||
}
|
||||
changePageElements = [];
|
||||
}
|
||||
|
||||
function changePageListener(index, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
changePage(changePageElements[index].id);
|
||||
}
|
||||
|
||||
function changePage(newPage) {
|
||||
if (void 0 !== props.pages[newPage]) {
|
||||
setCurrentPage(newPage);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (void 0 !== props.pages[props.initPage]) {
|
||||
setCurrentPage(props.initPage);
|
||||
} else if (Object.keys(props.pages).length) {
|
||||
setCurrentPage(Object.keys(props.pages)[0]);
|
||||
} else {
|
||||
setCurrentPage(null);
|
||||
}
|
||||
}, [props.initPage]);
|
||||
|
||||
useEffect(() => {
|
||||
clearEvents();
|
||||
|
||||
if (currentPage) {
|
||||
initEvents();
|
||||
|
||||
if ('function' === typeof props.pageChangeCallback) {
|
||||
props.pageChangeCallback(currentPage);
|
||||
}
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
return !currentPage ? null : <div ref={containerRef}>{React.cloneElement(props.pages[currentPage])}</div>;
|
||||
}
|
||||
|
||||
NavigationContentApp.propTypes = {
|
||||
initPage: PropTypes.string,
|
||||
pages: PropTypes.object.isRequired,
|
||||
pageChangeSelector: PropTypes.string.isRequired,
|
||||
pageIdSelectorAttr: PropTypes.string.isRequired,
|
||||
focusFirstItemOnPageChange: PropTypes.bool,
|
||||
pageChangeCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
NavigationContentApp.defaultProps = {
|
||||
focusFirstItemOnPageChange: true,
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MaterialIcon } from '../material-icon/MaterialIcon.jsx';
|
||||
import './NavigationMenuList.scss';
|
||||
|
||||
// TODO: Improve components.
|
||||
|
||||
function NavigationMenuListItem(props) {
|
||||
let children = [];
|
||||
|
||||
const attr = props.itemAttr || {};
|
||||
|
||||
if (void 0 === attr.className) {
|
||||
attr.className = '';
|
||||
} else if (attr.className) {
|
||||
attr.className += ' ';
|
||||
}
|
||||
|
||||
let textPosIndex = props.text ? (!props.icon || 'right' === props.iconPos ? 0 : 1) : -1;
|
||||
let iconPosIndex = props.icon ? (props.text && 'right' === props.iconPos ? 1 : 0) : -1;
|
||||
|
||||
if (-1 < textPosIndex) {
|
||||
children[textPosIndex] = <span key="Text">{props.text}</span>;
|
||||
}
|
||||
|
||||
if (-1 < iconPosIndex) {
|
||||
children[iconPosIndex] = (
|
||||
<span key="Icon" className={'right' === props.iconPos ? 'menu-item-icon-right' : 'menu-item-icon'}>
|
||||
{<MaterialIcon type={props.icon} />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
switch (props.itemType) {
|
||||
case 'link':
|
||||
children = (
|
||||
<a {...(props.linkAttr || {})} href={props.link} title={props.text || null}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
attr.className += 'link-item' + (props.active ? ' active' : '');
|
||||
break;
|
||||
case 'button':
|
||||
case 'open-subpage':
|
||||
children = (
|
||||
<button {...(props.buttonAttr || {})} key="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
break;
|
||||
case 'label':
|
||||
children = (
|
||||
<button {...(props.buttonAttr || {})} key="button">
|
||||
<span>{props.text || null}</span>
|
||||
</button>
|
||||
);
|
||||
attr.className = 'label-item';
|
||||
break;
|
||||
case 'div':
|
||||
children = (
|
||||
<div {...(props.divAttr || {})} key="div">
|
||||
{props.text || null}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if ('' !== attr.className) {
|
||||
attr.className = ' ' + attr.className;
|
||||
}
|
||||
|
||||
attr.className = attr.className.trim();
|
||||
|
||||
return <li {...attr}>{children}</li>;
|
||||
}
|
||||
|
||||
NavigationMenuListItem.propTypes = {
|
||||
itemType: PropTypes.oneOf(['link', 'open-subpage', 'button', 'label', 'div']),
|
||||
link: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
iconPos: PropTypes.oneOf(['left', 'right']),
|
||||
text: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
divAttr: PropTypes.object,
|
||||
buttonAttr: PropTypes.object,
|
||||
itemAttr: PropTypes.object,
|
||||
linkAttr: PropTypes.object,
|
||||
};
|
||||
|
||||
NavigationMenuListItem.defaultProps = {
|
||||
itemType: 'link',
|
||||
iconPos: 'left',
|
||||
active: !1,
|
||||
};
|
||||
|
||||
export function NavigationMenuList(props) {
|
||||
const menuItems = props.items.map((item, index) => <NavigationMenuListItem key={index} {...item} />);
|
||||
return menuItems.length ? (
|
||||
<div className={'nav-menu' + (props.removeVerticalPadding ? ' pv0' : '')}>
|
||||
<nav>
|
||||
<ul>{menuItems}</ul>
|
||||
</nav>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
NavigationMenuList.propTypes = {
|
||||
removeVerticalPadding: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(PropTypes.shape(NavigationMenuListItem.propTypes)).isRequired,
|
||||
};
|
||||
|
||||
NavigationMenuList.defaultProps = {
|
||||
removeVerticalPadding: false,
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
@import '../../../../css/includes/_variables_dimensions.scss';
|
||||
|
||||
.nav-menu {
|
||||
padding: 12px 0;
|
||||
|
||||
&.pv0 {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
> * {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: $sidebar-nav-padding;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
> * {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.link-item {
|
||||
&.active {
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
&:hover,
|
||||
&:focus {
|
||||
.popup & {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.label-item {
|
||||
button {
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reported-label {
|
||||
padding: 0 1rem;
|
||||
line-height: 48px;
|
||||
font-size: 13px;
|
||||
color: initial;
|
||||
color: red;
|
||||
|
||||
&:before {
|
||||
content: '\e153';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 1rem;
|
||||
padding: 0;
|
||||
margin: 0 1.5rem 0 0;
|
||||
font-family: 'Material Icons';
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
margin-right: 24px;
|
||||
|
||||
color: #888;
|
||||
|
||||
.material-icons {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-icon-right {
|
||||
float: right;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
li {
|
||||
&.link-item {
|
||||
&.active {
|
||||
background-color: var(--nav-menu-active-item-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--nav-menu-item-hover-bg-color);
|
||||
|
||||
.popup & {
|
||||
background-color: var(--in-popup-nav-menu-item-hover-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
li {
|
||||
> * {
|
||||
text-align: initial;
|
||||
text-decoration: none;
|
||||
|
||||
> * {
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&.link-item {
|
||||
&.active {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
.material-icons {
|
||||
font-size: 1.715em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import PageStore from '../../../utils/stores/PageStore.js';
|
||||
|
||||
import './Notifications.scss';
|
||||
|
||||
let visibleNotifications = [];
|
||||
|
||||
function NotificationItem(props) {
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
let timeout1 = null;
|
||||
let timeout2 = null;
|
||||
|
||||
useEffect(() => {
|
||||
timeout1 = setTimeout(function () {
|
||||
timeout2 = setTimeout(function () {
|
||||
setIsVisible(false);
|
||||
timeout2 = null;
|
||||
}, 1000);
|
||||
|
||||
timeout1 = null;
|
||||
setIsHidden(true);
|
||||
props.onHide(props.id);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
if (timeout1) {
|
||||
clearTimeout(timeout1);
|
||||
}
|
||||
|
||||
if (timeout2) {
|
||||
clearTimeout(timeout2);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !isVisible ? null : (
|
||||
<div className={'notification-item' + (isHidden ? ' hidden' : '')}>
|
||||
<div>{props.children || null}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Notifications() {
|
||||
const [notificationsLength, setNotificationsLength] = useState(visibleNotifications.length);
|
||||
|
||||
function onNotificationsUpdate() {
|
||||
setNotificationsLength(PageStore.get('notifications-size') + visibleNotifications.length);
|
||||
}
|
||||
|
||||
function onNotificationHide(id) {
|
||||
const newVisibleNotifications = [];
|
||||
visibleNotifications.map((item) => {
|
||||
if (item[0] !== id) {
|
||||
newVisibleNotifications.push(item);
|
||||
}
|
||||
});
|
||||
visibleNotifications = newVisibleNotifications;
|
||||
}
|
||||
|
||||
function notificationsContent() {
|
||||
const newItems = PageStore.get('notifications');
|
||||
|
||||
const oldNotifications = visibleNotifications.map((n) => {
|
||||
return (
|
||||
<NotificationItem key={n[0]} id={n[0]} onHide={onNotificationHide}>
|
||||
{n[1]}
|
||||
</NotificationItem>
|
||||
);
|
||||
});
|
||||
|
||||
const newNotifications = newItems.map((n) => {
|
||||
visibleNotifications.push(n);
|
||||
return (
|
||||
<NotificationItem key={n[0]} id={n[0]} onHide={onNotificationHide}>
|
||||
{n[1]}
|
||||
</NotificationItem>
|
||||
);
|
||||
});
|
||||
|
||||
return [...oldNotifications, ...newNotifications];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onNotificationsUpdate();
|
||||
PageStore.on('added_notification', onNotificationsUpdate);
|
||||
return () => PageStore.removeListener('added_notification', onNotificationsUpdate);
|
||||
}, []);
|
||||
|
||||
return !notificationsLength ? null : (
|
||||
<div className="notifications">
|
||||
<div>{notificationsContent()}</div>{' '}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
frontend/src/static/js/components/_shared/notifications/Notifications.scss
Executable file
43
frontend/src/static/js/components/_shared/notifications/Notifications.scss
Executable file
@@ -0,0 +1,43 @@
|
||||
@import '../../../../css/includes/_variables.scss';
|
||||
|
||||
.notifications {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
height: auto !important;
|
||||
max-width: 100%;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
z-index: +5;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: table;
|
||||
width: 288px;
|
||||
max-width: 100%;
|
||||
min-height: 48px;
|
||||
margin: 12px;
|
||||
color: #f1f1f1;
|
||||
background-color: #323232;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 5px 0 rgba(#000, 0.26);
|
||||
|
||||
transition: opacity 500ms linear;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
line-height: 20px;
|
||||
padding: 8px 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function setValue(value, min, max) {
|
||||
if (void 0 !== value) {
|
||||
let ret = null;
|
||||
ret = void 0 !== min && min > value ? min : value;
|
||||
ret = void 0 !== max && max < ret ? max : ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (void 0 !== min) {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (void 0 !== max) {
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
function setUnit(value, units) {
|
||||
if (!units || !units.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (i < units.length) {
|
||||
if (void 0 !== units[i].key && value === units[i].key) {
|
||||
return units[i].key;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return units[0].key;
|
||||
}
|
||||
|
||||
export function NumericInputWithUnit(props) {
|
||||
const valueInputRef = useRef(null);
|
||||
const valueUnitRef = useRef(null);
|
||||
|
||||
const [currentValue, setCurrentValue] = useState(null);
|
||||
const [currentUnit, setCurrentUnit] = useState(null);
|
||||
|
||||
function onChangeValue() {
|
||||
setCurrentValue(valueInputRef.current.value);
|
||||
|
||||
if (void 0 !== props.valueCallback) {
|
||||
props.valueCallback(valueInputRef.current.value);
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeUnit() {
|
||||
setCurrentUnit(valueUnitRef.current.value);
|
||||
|
||||
if (void 0 !== props.unitCallback) {
|
||||
props.unitCallback(valueUnitRef.current.value);
|
||||
}
|
||||
}
|
||||
|
||||
function unitOptions() {
|
||||
if (!props.units.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ret = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < props.units.length) {
|
||||
if (void 0 !== props.units[i].key) {
|
||||
ret.push(
|
||||
<option key={props.units[i].key} value={props.units[i].key}>
|
||||
{void 0 !== props.units[i].label ? props.units[i].label : props.units[i].key}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentValue(setValue(0 + props.defaultValue, props.minValue, props.maxValue));
|
||||
setCurrentUnit(setUnit(props.defaultUnit, props.units));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="num-value-unit">
|
||||
{void 0 !== props.label ? <span className="label">{props.label}</span> : null}
|
||||
<input
|
||||
ref={valueInputRef}
|
||||
className="value-input"
|
||||
type="number"
|
||||
value={null !== currentValue ? currentValue : ''}
|
||||
min={void 0 !== props.minValue ? props.minValue : null}
|
||||
max={void 0 !== props.maxValue ? props.maxValue : null}
|
||||
onChange={onChangeValue}
|
||||
/>
|
||||
<select
|
||||
ref={valueUnitRef}
|
||||
className="value-unit"
|
||||
onChange={onChangeUnit}
|
||||
value={null !== currentUnit ? currentUnit : ''}
|
||||
>
|
||||
{unitOptions()}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NumericInputWithUnit.propTypes = {
|
||||
label: PropTypes.string,
|
||||
units: PropTypes.array.isRequired,
|
||||
defaultUnit: PropTypes.string,
|
||||
defaultValue: PropTypes.number,
|
||||
minValue: PropTypes.number,
|
||||
maxValue: PropTypes.number,
|
||||
valueCallback: PropTypes.func,
|
||||
unitCallback: PropTypes.func,
|
||||
};
|
||||
29
frontend/src/static/js/components/_shared/popup/Popup.jsx
Normal file
29
frontend/src/static/js/components/_shared/popup/Popup.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import './Popup.scss';
|
||||
|
||||
const Popup = React.forwardRef((props, ref) => {
|
||||
return void 0 !== props.children ? (
|
||||
<div ref={ref} className={'popup' + (void 0 !== props.className ? ' ' + props.className : '')} style={props.style}>
|
||||
{props.children}
|
||||
</div>
|
||||
) : null;
|
||||
});
|
||||
|
||||
export default Popup;
|
||||
|
||||
export function PopupTop(props) {
|
||||
return void 0 !== props.children ? (
|
||||
<div className={'popup-top' + (void 0 !== props.className ? ' ' + props.className : '')} style={props.style}>
|
||||
{props.children}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export function PopupMain(props) {
|
||||
return void 0 !== props.children ? (
|
||||
<div className={'popup-main' + (void 0 !== props.className ? ' ' + props.className : '')} style={props.style}>
|
||||
{props.children}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
135
frontend/src/static/js/components/_shared/popup/Popup.scss
Executable file
135
frontend/src/static/js/components/_shared/popup/Popup.scss
Executable file
@@ -0,0 +1,135 @@
|
||||
.popup {
|
||||
background-color: var(--popup-bg-color);
|
||||
|
||||
hr {
|
||||
background-color: var(--popup-hr-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popup-top {
|
||||
color: var(--popup-top-text-color);
|
||||
background-color: var(--popup-top-bg-color);
|
||||
|
||||
.circle-icon-button.menu-item-icon {
|
||||
color: inherit;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-message-title {
|
||||
color: var(--popup-msg-title-text-color);
|
||||
}
|
||||
|
||||
.popup-message-main {
|
||||
color: var(--popup-msg-main-text-color);
|
||||
}
|
||||
|
||||
.popup {
|
||||
z-index: +4;
|
||||
display: block;
|
||||
width: 300px;
|
||||
text-align: initial;
|
||||
cursor: default;
|
||||
box-shadow: 0 16px 24px 2px rgba(#000, 0.14), 0 6px 30px 5px rgba(#000, 0.12), 0 8px 10px -5px rgba(#000, 0.4);
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-top {
|
||||
padding: 16px * 0.25 8px * 0.5;
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
display: table;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
min-width: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-icon-button.menu-item-icon {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-main {
|
||||
overflow: hidden;
|
||||
}
|
||||
.popup-message {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popup-message-title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 24px;
|
||||
padding: 0 24px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.popup-message-main {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
margin-bottom: 32px;
|
||||
margin-top: 4px;
|
||||
padding: 0 24px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.popup-message-bottom {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
float: left;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.popup-fullscreen {
|
||||
z-index: +4;
|
||||
position: fixed;
|
||||
display: table;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 24px 40px;
|
||||
padding-top: calc(var(--header-height) + 24px);
|
||||
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
|
||||
.popup-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 1840px;
|
||||
max-height: 940px;
|
||||
margin: 0 auto;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
|
||||
text-align: center;
|
||||
|
||||
.popup-fullscreen-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #000;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useRef, useState, useImperativeHandle, useCallback } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { hasClassname } from '../../../utils/helpers/dom';
|
||||
import { default as Popup } from './Popup.jsx';
|
||||
|
||||
export function PopupContent(props) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
const [isVisible, setVisibility] = useState(false);
|
||||
|
||||
const onClickOutside = useCallback((ev) => {
|
||||
if (hasClassname(ev.target, 'popup-fullscreen-overlay')) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const domElem = findDOMNode(wrapperRef.current);
|
||||
|
||||
if (-1 === ev.path.indexOf(domElem)) {
|
||||
hide();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback((ev) => {
|
||||
let key = ev.keyCode || ev.charCode;
|
||||
if (27 === key) {
|
||||
onClickOutside(ev);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function enableListeners() {
|
||||
document.addEventListener('click', onClickOutside);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
}
|
||||
|
||||
function disableListeners() {
|
||||
document.removeEventListener('click', onClickOutside);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
}
|
||||
|
||||
function show() {
|
||||
setVisibility(true);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
disableListeners();
|
||||
setVisibility(false);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
function tryToHide() {
|
||||
if (isVisible) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
function tryToShow() {
|
||||
if (!isVisible) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
enableListeners();
|
||||
if ('function' === typeof props.showCallback) {
|
||||
props.showCallback();
|
||||
}
|
||||
} else {
|
||||
if ('function' === typeof props.hideCallback) {
|
||||
props.hideCallback();
|
||||
}
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
useImperativeHandle(props.contentRef, () => ({
|
||||
toggle,
|
||||
tryToHide,
|
||||
tryToShow,
|
||||
}));
|
||||
|
||||
return isVisible ? (
|
||||
<Popup ref={wrapperRef} className={props.className} style={props.style}>
|
||||
{props.children}
|
||||
</Popup>
|
||||
) : null;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
export function PopupTrigger(props) {
|
||||
const onClick = () => props.contentRef.current.toggle();
|
||||
return React.cloneElement(props.children, { onClick });
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './SpinnerLoader.scss';
|
||||
|
||||
export function SpinnerLoader(props) {
|
||||
let classname = 'spinner-loader';
|
||||
|
||||
switch (props.size) {
|
||||
case 'tiny':
|
||||
case 'x-small':
|
||||
case 'small':
|
||||
case 'large':
|
||||
case 'x-large':
|
||||
classname += ' ' + props.size;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classname}>
|
||||
<svg className="circular" viewBox="25 25 50 50">
|
||||
<circle className="path" cx="50" cy="50" r="20" fill="none" strokeWidth="1.5" strokeMiterlimit="10" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SpinnerLoader.propTypes = {
|
||||
size: PropTypes.oneOf(['tiny', 'x-small', 'small', 'medium', 'large', 'x-large']),
|
||||
};
|
||||
|
||||
SpinnerLoader.defaultProps = {
|
||||
size: 'medium',
|
||||
};
|
||||
87
frontend/src/static/js/components/_shared/spinner-loader/SpinnerLoader.scss
Executable file
87
frontend/src/static/js/components/_shared/spinner-loader/SpinnerLoader.scss
Executable file
@@ -0,0 +1,87 @@
|
||||
@import '../../../../css/includes/_variables.scss';
|
||||
|
||||
$green: #008744;
|
||||
$blue: #0057e7;
|
||||
$red: #d62d20;
|
||||
$yellow: #ffa700;
|
||||
|
||||
.spinner-loader {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
&.tiny {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.x-small {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.small {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
&.x-large {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
transform-origin: center center;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
circle {
|
||||
stroke: var(--spinner-loader-color);
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linecap: round;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 89, 200;
|
||||
stroke-dashoffset: -35px;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 89, 200;
|
||||
stroke-dashoffset: -124px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useUser } from '../../../utils/hooks/useUser';
|
||||
import { CircleIconButton } from '../circle-icon-button/CircleIconButton.jsx';
|
||||
import { MaterialIcon } from '../material-icon/MaterialIcon.jsx';
|
||||
|
||||
import './UserThumbnail.scss';
|
||||
|
||||
export function UserThumbnail(props) {
|
||||
const { thumbnail } = useUser();
|
||||
|
||||
const attr = {
|
||||
'aria-label': 'Account profile photo that opens list of options and settings pages links',
|
||||
className: 'thumbnail',
|
||||
};
|
||||
|
||||
if (props.isButton) {
|
||||
if (void 0 !== props.onClick) {
|
||||
attr.onClick = props.onClick;
|
||||
}
|
||||
} else {
|
||||
attr.type = 'span';
|
||||
}
|
||||
|
||||
switch (props.size) {
|
||||
case 'small':
|
||||
case 'large':
|
||||
attr.className += ' ' + props.size + '-thumb';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<CircleIconButton {...attr}>
|
||||
{thumbnail ? <img src={thumbnail} alt="" /> : <MaterialIcon type="person" />}
|
||||
</CircleIconButton>
|
||||
);
|
||||
}
|
||||
|
||||
UserThumbnail.propTypes = {
|
||||
isButton: PropTypes.bool,
|
||||
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
UserThumbnail.defaultProps = {
|
||||
isButton: false,
|
||||
size: 'medium',
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
.thumbnail,
|
||||
.thumbnail.circle-icon-button {
|
||||
background-color: var(--logged-in-user-thumb-bg-color);
|
||||
}
|
||||
|
||||
a.thumbnail.circle-icon-button,
|
||||
button.thumbnail.circle-icon-button {
|
||||
&:focus,
|
||||
&:active {
|
||||
background-color: var(--logged-in-user-thumb-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail,
|
||||
.thumbnail.circle-icon-button,
|
||||
.thumbnail img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&.small-thumb,
|
||||
&.small-thumb img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
&.large-thumb,
|
||||
&.large-thumb img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail.circle-icon-button {
|
||||
.material-icons {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
&.small-thumb {
|
||||
.material-icons {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
&.large-thumb {
|
||||
.material-icons {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 0 16px 0 rgba(#000, 0.1);
|
||||
|
||||
img {
|
||||
vertical-align: inherit;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
455
frontend/src/static/js/components/comments/Comments.jsx
Normal file
455
frontend/src/static/js/components/comments/Comments.jsx
Normal file
@@ -0,0 +1,455 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { format } from 'timeago.js';
|
||||
import { usePopup } from '../../utils/hooks/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/';
|
||||
import { PopupMain, UserThumbnail } from '../_shared';
|
||||
|
||||
import './Comments.scss';
|
||||
|
||||
const commentsText = {
|
||||
single: 'comment',
|
||||
uppercaseSingle: 'COMMENT',
|
||||
ucfirstSingle: 'Comment',
|
||||
ucfirstPlural: 'Comments',
|
||||
submitCommentText: 'SUBMIT',
|
||||
disabledCommentsMsg: 'Comments are disabled',
|
||||
};
|
||||
|
||||
function CommentForm(props) {
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
const [madeChanges, setMadeChanges] = useState(false);
|
||||
const [textareaFocused, setTextareaFocused] = useState(false);
|
||||
const [textareaLineHeight, setTextareaLineHeight] = useState(-1);
|
||||
|
||||
const [loginUrl] = useState(
|
||||
!MemberContext._currentValue.is.anonymous
|
||||
? null
|
||||
: LinksContext._currentValue.signin +
|
||||
'?next=/' +
|
||||
window.location.href.replace(SiteContext._currentValue.url, '').replace(/^\//g, '')
|
||||
);
|
||||
|
||||
function onFocus() {
|
||||
setTextareaFocused(true);
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
setTextareaFocused(false);
|
||||
}
|
||||
|
||||
function onCommentSubmit() {
|
||||
textareaRef.current.style.height = '';
|
||||
|
||||
const contentHeight = textareaRef.current.scrollHeight;
|
||||
const contentLineHeight =
|
||||
0 < textareaLineHeight ? textareaLineHeight : parseFloat(window.getComputedStyle(textareaRef.current).lineHeight);
|
||||
|
||||
setValue('');
|
||||
setMadeChanges(false);
|
||||
setTextareaLineHeight(contentLineHeight);
|
||||
|
||||
textareaRef.current.style.height =
|
||||
Math.max(20, textareaLineHeight * Math.ceil(contentHeight / contentLineHeight)) + 'px';
|
||||
}
|
||||
|
||||
function onCommentSubmitFail() {
|
||||
setMadeChanges(false);
|
||||
}
|
||||
|
||||
function onChange(event) {
|
||||
textareaRef.current.style.height = '';
|
||||
|
||||
const contentHeight = textareaRef.current.scrollHeight;
|
||||
const contentLineHeight =
|
||||
0 < textareaLineHeight ? textareaLineHeight : parseFloat(window.getComputedStyle(textareaRef.current).lineHeight);
|
||||
|
||||
setValue(textareaRef.current.value);
|
||||
setMadeChanges(true);
|
||||
setTextareaLineHeight(contentLineHeight);
|
||||
|
||||
textareaRef.current.style.height =
|
||||
Math.max(20, textareaLineHeight * Math.ceil(contentHeight / contentLineHeight)) + 'px';
|
||||
}
|
||||
|
||||
function submitComment() {
|
||||
if (!madeChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
const val = textareaRef.current.value.trim();
|
||||
|
||||
if ('' !== val) {
|
||||
MediaPageActions.submitComment(val);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
MediaPageStore.on('comment_submit', onCommentSubmit);
|
||||
MediaPageStore.on('comment_submit_fail', onCommentSubmitFail);
|
||||
|
||||
return () => {
|
||||
MediaPageStore.removeListener('comment_submit', onCommentSubmit);
|
||||
MediaPageStore.removeListener('comment_submit_fail', onCommentSubmitFail);
|
||||
};
|
||||
});
|
||||
|
||||
return !MemberContext._currentValue.is.anonymous ? (
|
||||
<div className="comments-form">
|
||||
<div className="comments-form-inner">
|
||||
<UserThumbnail />
|
||||
<div className="form">
|
||||
<div className={'form-textarea-wrap' + (textareaFocused ? ' focused' : '')}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="form-textarea"
|
||||
rows="1"
|
||||
placeholder={'Add a ' + commentsText.single + '...'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="form-buttons">
|
||||
<button className={'' === value.trim() ? 'disabled' : ''} onClick={submitComment}>
|
||||
{commentsText.submitCommentText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="comments-form">
|
||||
<div className="comments-form-inner">
|
||||
<UserThumbnail />
|
||||
<div className="form">
|
||||
<a
|
||||
href={loginUrl}
|
||||
rel="noffolow"
|
||||
className="form-textarea-wrap"
|
||||
title={'Add a ' + commentsText.single + '...'}
|
||||
>
|
||||
<span className="form-textarea">{'Add a ' + commentsText.single + '...'}</span>
|
||||
</a>
|
||||
<div className="form-buttons">
|
||||
<a href={loginUrl} rel="noffolow" className="disabled">
|
||||
{commentsText.submitCommentText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CommentForm.propTypes = {
|
||||
comment_type: PropTypes.oneOf(['new', 'reply']),
|
||||
media_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
reply_comment_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
CommentForm.defaultProps = {
|
||||
comment_type: 'new',
|
||||
};
|
||||
|
||||
const ENABLED_COMMENTS_READ_MORE = false;
|
||||
|
||||
function CommentActions(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
function cancelCommentRemoval() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
|
||||
function proceedCommentRemoval() {
|
||||
popupContentRef.current.toggle();
|
||||
MediaPageActions.deleteComment(props.comment_id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="comment-actions">
|
||||
{/*<div className="comment-action like-action"><CircleIconButton><MaterialIcon type="thumb_up" /></CircleIconButton><span className="likes-num">145</span></div>*/}
|
||||
{/*<div className="comment-action dislike-action"><CircleIconButton><MaterialIcon type="thumb_down" /></CircleIconButton><span className="dislikes-num">19</span></div>*/}
|
||||
{/*<div className="comment-action replay-comment"><button>REPLY</button></div>*/}
|
||||
|
||||
{MemberContext._currentValue.can.deleteComment ? (
|
||||
<div className="comment-action remove-comment">
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button>DELETE {commentsText.uppercaseSingle}</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef}>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">{commentsText.ucfirstSingle} removal</span>
|
||||
<span className="popup-message-main">You're willing to remove {commentsText.single} permanently?</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-comment-removal" onClick={cancelCommentRemoval}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-comment-removal" onClick={proceedCommentRemoval}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Comment(props) {
|
||||
const commentTextRef = useRef(null);
|
||||
const commentTextInnerRef = useRef(null);
|
||||
|
||||
const [viewMoreContent, setViewMoreContent] = useState(!ENABLED_COMMENTS_READ_MORE || false);
|
||||
const [enabledViewMoreContent, setEnabledViewMoreContent] = useState(false);
|
||||
|
||||
function onWindowResize() {
|
||||
const newval = enabledViewMoreContent || commentTextInnerRef.offsetHeight > commentTextRef.offsetHeight;
|
||||
setEnabledViewMoreContent(newval);
|
||||
setViewMoreContent(newval || false);
|
||||
}
|
||||
|
||||
function toggleMore() {
|
||||
setViewMoreContent(!viewMoreContent);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ENABLED_COMMENTS_READ_MORE) {
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
setEnabledViewMoreContent(commentTextInnerRef.offsetHeight > commentTextRef.offsetHeight);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (ENABLED_COMMENTS_READ_MORE) {
|
||||
PageStore.removeListener('window_resize', onWindowResize);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="comment">
|
||||
<div className="comment-inner">
|
||||
<a className="comment-author-thumb" href={props.author_link} title={props.author_name}>
|
||||
<img src={props.author_thumb} alt={props.author_name} />
|
||||
</a>
|
||||
<div className="comment-content">
|
||||
<div className="comment-meta">
|
||||
<div className="comment-author">
|
||||
<a href={props.author_link} title={props.author_name}>
|
||||
{props.author_name}
|
||||
</a>
|
||||
</div>
|
||||
<div className="comment-date">{format(new Date(props.publish_date))}</div>
|
||||
</div>
|
||||
<div ref={commentTextRef} className={'comment-text' + (viewMoreContent ? ' show-all' : '')}>
|
||||
<div
|
||||
ref={commentTextInnerRef}
|
||||
className="comment-text-inner"
|
||||
dangerouslySetInnerHTML={{ __html: props.text }}
|
||||
></div>
|
||||
</div>
|
||||
{enabledViewMoreContent ? (
|
||||
<button className="toggle-more" onClick={toggleMore}>
|
||||
{viewMoreContent ? 'Show less' : 'Read more'}
|
||||
</button>
|
||||
) : null}
|
||||
{MemberContext._currentValue.can.deleteComment ? <CommentActions comment_id={props.comment_id} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Comment.propTypes = {
|
||||
comment_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
media_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
text: PropTypes.string,
|
||||
author_name: PropTypes.string,
|
||||
author_link: PropTypes.string,
|
||||
author_thumb: PropTypes.string,
|
||||
publish_date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
likes: PropTypes.number,
|
||||
dislikes: PropTypes.number,
|
||||
};
|
||||
|
||||
Comment.defaultProps = {
|
||||
author_name: '',
|
||||
author_link: '#',
|
||||
publish_date: 0,
|
||||
likes: 0,
|
||||
dislikes: 0,
|
||||
};
|
||||
|
||||
function displayCommentsRelatedAlert() {
|
||||
// TODO: Improve this and move it into Media Page code.
|
||||
|
||||
var pageMainEl = document.querySelector('.page-main');
|
||||
var noCommentDiv = pageMainEl.querySelector('.no-comment');
|
||||
|
||||
const postUploadMessage = PageStore.get('config-contents').uploader.postUploadMessage;
|
||||
|
||||
if ('' === postUploadMessage) {
|
||||
if (noCommentDiv && 0 === comm.length) {
|
||||
noCommentDiv.parentNode.removeChild(noCommentDiv);
|
||||
}
|
||||
} else if (0 === comm.length && 'unlisted' === MediaPageStore.get('media-data').state) {
|
||||
if (-1 < LinksContext._currentValue.profile.media.indexOf(MediaPageStore.get('media-data').author_profile)) {
|
||||
if (!noCommentDiv) {
|
||||
const missingCommentariesUnlistedMsgElem = document.createElement('div');
|
||||
|
||||
missingCommentariesUnlistedMsgElem.setAttribute('role', 'alert');
|
||||
missingCommentariesUnlistedMsgElem.setAttribute('class', 'alert info alert-dismissible no-comment');
|
||||
missingCommentariesUnlistedMsgElem.innerHTML =
|
||||
'<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>' +
|
||||
postUploadMessage;
|
||||
|
||||
if (pageMainEl.firstChild) {
|
||||
pageMainEl.insertBefore(missingCommentariesUnlistedMsgElem, pageMainEl.firstChild);
|
||||
} else {
|
||||
pageMainEl.appendChild(missingCommentariesUnlistedMsgElem);
|
||||
}
|
||||
|
||||
missingCommentariesUnlistedMsgElem.querySelector('button.close').addEventListener('click', function (ev) {
|
||||
missingCommentariesUnlistedMsgElem.setAttribute('class', 'alert info alert-dismissible hiding');
|
||||
setTimeout(function () {
|
||||
missingCommentariesUnlistedMsgElem.parentNode.removeChild(missingCommentariesUnlistedMsgElem);
|
||||
}, 400);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (noCommentDiv && 0 < comm.length) {
|
||||
noCommentDiv.parentNode.removeChild(noCommentDiv);
|
||||
}
|
||||
}
|
||||
|
||||
const CommentsListHeader = ({ commentsLength }) => {
|
||||
return (
|
||||
<>
|
||||
{!MemberContext._currentValue.can.readComment || MediaPageStore.get('media-data').enable_comments ? null : (
|
||||
<span className="disabled-comments-msg">{commentsText.disabledCommentsMsg}</span>
|
||||
)}
|
||||
|
||||
{MemberContext._currentValue.can.readComment &&
|
||||
(MediaPageStore.get('media-data').enable_comments || MemberContext._currentValue.can.editMedia) ? (
|
||||
<h2>
|
||||
{commentsLength
|
||||
? 1 < commentsLength
|
||||
? commentsLength + ' ' + commentsText.ucfirstPlural
|
||||
: commentsLength + ' ' + commentsText.ucfirstSingle
|
||||
: MediaPageStore.get('media-data').enable_comments
|
||||
? 'No ' + commentsText.single + ' yet'
|
||||
: ''}
|
||||
</h2>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function CommentsList(props) {
|
||||
const [mediaId, setMediaId] = useState(MediaPageStore.get('media-id'));
|
||||
|
||||
const [comments, setComments] = useState(
|
||||
MemberContext._currentValue.can.readComment ? MediaPageStore.get('media-comments') : []
|
||||
);
|
||||
|
||||
const [displayComments, setDisplayComments] = useState(false);
|
||||
|
||||
function onCommentsLoad() {
|
||||
displayCommentsRelatedAlert();
|
||||
setComments([...MediaPageStore.get('media-comments')]);
|
||||
}
|
||||
|
||||
function onCommentSubmit(commentId) {
|
||||
onCommentsLoad();
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(() => PageActions.addNotification(commentsText.ucfirstSingle + ' added', 'commentSubmit'), 100);
|
||||
}
|
||||
|
||||
function onCommentSubmitFail() {
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(
|
||||
() => PageActions.addNotification(commentsText.ucfirstSingle + ' submition failed', 'commentSubmitFail'),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
function onCommentDelete(commentId) {
|
||||
onCommentsLoad();
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(() => PageActions.addNotification(commentsText.ucfirstSingle + ' removed', 'commentDelete'), 100);
|
||||
}
|
||||
|
||||
function onCommentDeleteFail(commentId) {
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(
|
||||
() => PageActions.addNotification(commentsText.ucfirstSingle + ' removal failed', 'commentDeleteFail'),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayComments(
|
||||
comments.length &&
|
||||
MemberContext._currentValue.can.readComment &&
|
||||
(MediaPageStore.get('media-data').enable_comments || MemberContext._currentValue.can.editMedia)
|
||||
);
|
||||
}, [comments]);
|
||||
|
||||
useEffect(() => {
|
||||
MediaPageStore.on('comments_load', onCommentsLoad);
|
||||
MediaPageStore.on('comment_submit', onCommentSubmit);
|
||||
MediaPageStore.on('comment_submit_fail', onCommentSubmitFail);
|
||||
MediaPageStore.on('comment_delete', onCommentDelete);
|
||||
MediaPageStore.on('comment_delete_fail', onCommentDeleteFail);
|
||||
|
||||
return () => {
|
||||
MediaPageStore.removeListener('comments_load', onCommentsLoad);
|
||||
MediaPageStore.removeListener('comment_submit', onCommentSubmit);
|
||||
MediaPageStore.removeListener('comment_submit_fail', onCommentSubmitFail);
|
||||
MediaPageStore.removeListener('comment_delete', onCommentDelete);
|
||||
MediaPageStore.removeListener('comment_delete_fail', onCommentDeleteFail);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="comments-list">
|
||||
<div className="comments-list-inner">
|
||||
<CommentsListHeader commentsLength={comments.length} />
|
||||
|
||||
{MediaPageStore.get('media-data').enable_comments ? <CommentForm media_id={mediaId} /> : null}
|
||||
|
||||
{displayComments
|
||||
? comments.map((c) => {
|
||||
return (
|
||||
<Comment
|
||||
key={c.uid}
|
||||
comment_id={c.uid}
|
||||
media_id={mediaId}
|
||||
text={c.text}
|
||||
author_name={c.author_name}
|
||||
author_link={c.author_profile}
|
||||
author_thumb={SiteContext._currentValue.url + '/' + c.author_thumbnail_url.replace(/^\//g, '')}
|
||||
publish_date={c.add_date}
|
||||
likes={0}
|
||||
dislikes={0}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
511
frontend/src/static/js/components/comments/Comments.scss
Executable file
511
frontend/src/static/js/components/comments/Comments.scss
Executable file
@@ -0,0 +1,511 @@
|
||||
@import '../../../css/includes/_variables.scss';
|
||||
|
||||
.comments-form-inner {
|
||||
.form {
|
||||
.form-textarea-wrap {
|
||||
border-color: var(--comments-textarea-wrapper-border-color);
|
||||
|
||||
&:after {
|
||||
background-color: var(--comments-textarea-wrapper-after-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
color: var(--comments-textarea-text-color);
|
||||
|
||||
&:placeholder {
|
||||
color: var(--comments-textarea-text-placeholder-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comments-list-inner {
|
||||
border-color: var(--comments-list-inner-border-color);
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
color: var(--comment-author-text-color);
|
||||
|
||||
a {
|
||||
color: var(--comment-author-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
color: var(--comment-date-text-color);
|
||||
text-decoration-color: var(--comment-date-text-color);
|
||||
|
||||
a {
|
||||
color: var(--comment-date-text-color);
|
||||
text-decoration-color: var(--comment-date-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--comment-date-hover-text-color);
|
||||
text-decoration-color: var(--comment-date-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
color: var(--comment-text-color);
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
button {
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-icon-button {
|
||||
background-color: var(--body-bg-color);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--comment-actions-material-icon-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.likes-num,
|
||||
.dislikes-num {
|
||||
color: var(--comment-actions-likes-num-text-color);
|
||||
}
|
||||
|
||||
.reply-comment {
|
||||
> button {
|
||||
color: var(--comment-actions-reply-button-text-color);
|
||||
|
||||
background: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--comment-actions-reply-button-hover-text-color);
|
||||
|
||||
.material-icons {
|
||||
color: var(--comment-actions-reply-button-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remove-comment {
|
||||
.popup-message-bottom {
|
||||
button {
|
||||
&.cancel-comment-removal {
|
||||
color: var(--comment-actions-cancel-removal-button-text-color);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--comment-actions-cancel-removal-button-hover-text-color);
|
||||
|
||||
.material-icons {
|
||||
color: var(--comment-actions-cancel-removal-button-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comments-form {
|
||||
position: relative;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.comments-form-inner {
|
||||
min-height: 40px;
|
||||
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-left: 56px;
|
||||
|
||||
.form-textarea-wrap {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0 0 0.3em;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
&:after {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
position: relative;
|
||||
resize: none;
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
border-style: solid;
|
||||
border: 0;
|
||||
min-height: 20px;
|
||||
height: auto;
|
||||
text-decoration: none;
|
||||
overflow-y: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
vertical-align: baseline;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color: transparent;
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
|
||||
&:placeholder {
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.form-textarea-wrap {
|
||||
&:focus {
|
||||
outline: 1px dotted rgba(#0a0a0a, 0.5);
|
||||
}
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
.form-textarea {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
margin-top: 8px;
|
||||
text-align: right;
|
||||
|
||||
a,
|
||||
button {
|
||||
display: inline-block;
|
||||
padding: 12px 16px 10px;
|
||||
margin-left: 8px;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
border-radius: 1px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comments-list {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comments-list-inner {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
|
||||
padding-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1008px) {
|
||||
padding-top: 24px;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin: 0 2rem 1.5rem 0;
|
||||
}
|
||||
|
||||
.disabled-comments-msg {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ratings-container + .comments-list .comments-list-inner {
|
||||
margin-top: -16px;
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.comment-replies {
|
||||
}
|
||||
|
||||
.comment-replies-inner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.comment {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comment-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment-author-thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
border-radius: 9999px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
position: relative;
|
||||
width: auto;
|
||||
margin: 0 0 0 56px;
|
||||
display: inline-block;
|
||||
|
||||
.toggle-more {
|
||||
padding: 0;
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
overflow-wrap: break-word;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
display: inline-block;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
margin: 0 4px 2px 0;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
display: inline-block;
|
||||
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
|
||||
a {
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
|
||||
max-height: (5 * 20px);
|
||||
|
||||
@media screen and (min-width: 1008px) {
|
||||
max-height: (4 * 20px);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1216px) {
|
||||
max-height: (3 * 20px);
|
||||
}
|
||||
|
||||
&.show-all {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-text-inner {
|
||||
}
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
color: #909090;
|
||||
|
||||
.circle-icon-button {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
color: #909090;
|
||||
font-size: 16px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.comment-action {
|
||||
display: inline-block;
|
||||
|
||||
~ * {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.likes-num,
|
||||
.dislikes-num {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.reply-comment,
|
||||
.remove-comment {
|
||||
> button {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
line-height: 15px;
|
||||
border: 0;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-comment {
|
||||
> button {
|
||||
}
|
||||
}
|
||||
|
||||
.remove-comment {
|
||||
position: relative;
|
||||
width: auto;
|
||||
// float:right;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
> button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.popup-message-bottom {
|
||||
button {
|
||||
position: relative;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
&.cancel-comment-removal,
|
||||
&.proceed-comment-removal {
|
||||
background-color: transparent;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
&.proceed-comment-removal {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.cancel-comment-removal {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-replies {
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { useLayout, useItemListInlineSlider } from '../../utils/hooks/';
|
||||
import { ItemsStaticListHandler } from './includes/itemLists/ItemsStaticListHandler';
|
||||
import { ItemList } from './ItemList';
|
||||
import { PendingItemsList } from './PendingItemsList';
|
||||
import { ListItem, listItemProps } from '../list-item/ListItem';
|
||||
|
||||
export function InlineSliderItemList(props) {
|
||||
const { visibleSidebar } = useLayout();
|
||||
|
||||
const [
|
||||
items,
|
||||
countedItems,
|
||||
listHandler,
|
||||
classname,
|
||||
setListHandler,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
winResizeListener,
|
||||
sidebarVisibilityChangeListener,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
renderBeforeListWrap,
|
||||
renderAfterListWrap,
|
||||
] = useItemListInlineSlider(props);
|
||||
|
||||
useEffect(() => {
|
||||
sidebarVisibilityChangeListener();
|
||||
}, [visibleSidebar]);
|
||||
|
||||
useEffect(() => {
|
||||
setListHandler(new ItemsStaticListHandler(props.items, props.pageItems, props.maxItems, onItemsCount, onItemsLoad));
|
||||
|
||||
PageStore.on('window_resize', winResizeListener);
|
||||
|
||||
return () => {
|
||||
PageStore.removeListener('window_resize', winResizeListener);
|
||||
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
<div className={classname.listOuter}>
|
||||
{renderBeforeListWrap()}
|
||||
|
||||
<div ref={itemsListWrapperRef} className="items-list-wrap">
|
||||
<div ref={itemsListRef} className={classname.list}>
|
||||
{items.map((itm, index) => (
|
||||
<ListItem key={index} {...listItemProps(props, itm, index)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderAfterListWrap()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InlineSliderItemList.propTypes = {
|
||||
...ItemList.propTypes,
|
||||
};
|
||||
|
||||
InlineSliderItemList.defaultProps = {
|
||||
...ItemList.defaultProps,
|
||||
pageItems: 12,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { useLayout, useItemListInlineSlider } from '../../utils/hooks/';
|
||||
import { ItemListAsync } from './ItemListAsync';
|
||||
import { PendingItemsList } from './PendingItemsList';
|
||||
import { ListItem, listItemProps } from '../list-item/ListItem';
|
||||
import { ItemsListHandler } from './includes/itemLists/ItemsListHandler';
|
||||
|
||||
export function InlineSliderItemListAsync(props) {
|
||||
const { visibleSidebar } = useLayout();
|
||||
|
||||
const [
|
||||
items,
|
||||
countedItems,
|
||||
listHandler,
|
||||
classname,
|
||||
setListHandler,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
winResizeListener,
|
||||
sidebarVisibilityChangeListener,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
renderBeforeListWrap,
|
||||
renderAfterListWrap,
|
||||
] = useItemListInlineSlider(props);
|
||||
|
||||
useEffect(() => {
|
||||
sidebarVisibilityChangeListener();
|
||||
}, [visibleSidebar]);
|
||||
|
||||
useEffect(() => {
|
||||
setListHandler(
|
||||
new ItemsListHandler(
|
||||
props.pageItems,
|
||||
props.maxItems,
|
||||
props.firstItemRequestUrl,
|
||||
props.requestUrl,
|
||||
onItemsCount,
|
||||
onItemsLoad
|
||||
)
|
||||
);
|
||||
|
||||
PageStore.on('window_resize', winResizeListener);
|
||||
|
||||
return () => {
|
||||
PageStore.removeListener('window_resize', winResizeListener);
|
||||
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
<div className={classname.listOuter}>
|
||||
{renderBeforeListWrap()}
|
||||
|
||||
<div ref={itemsListWrapperRef} className="items-list-wrap">
|
||||
<div ref={itemsListRef} className={classname.list}>
|
||||
{items.map((itm, index) => (
|
||||
<ListItem key={index} {...listItemProps(props, itm, index)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderAfterListWrap()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InlineSliderItemListAsync.propTypes = {
|
||||
...ItemListAsync.propTypes,
|
||||
};
|
||||
|
||||
InlineSliderItemListAsync.defaultProps = {
|
||||
...ItemListAsync.defaultProps,
|
||||
pageItems: 12,
|
||||
};
|
||||
105
frontend/src/static/js/components/item-list/ItemList.jsx
Normal file
105
frontend/src/static/js/components/item-list/ItemList.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useItemListSync } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers';
|
||||
import { PendingItemsList } from './PendingItemsList';
|
||||
import { ListItem, listItemProps } from '../list-item/ListItem';
|
||||
import { ItemsStaticListHandler } from './includes/itemLists/ItemsStaticListHandler';
|
||||
|
||||
export function ItemList(props) {
|
||||
const [
|
||||
countedItems,
|
||||
items,
|
||||
listHandler,
|
||||
setListHandler,
|
||||
classname,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
renderBeforeListWrap,
|
||||
renderAfterListWrap,
|
||||
] = useItemListSync(props);
|
||||
|
||||
useEffect(() => {
|
||||
setListHandler(new ItemsStaticListHandler(props.items, props.pageItems, props.maxItems, onItemsCount, onItemsLoad));
|
||||
|
||||
return () => {
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
<div className={classname.listOuter}>
|
||||
{renderBeforeListWrap()}
|
||||
|
||||
<div ref={itemsListWrapperRef} className="items-list-wrap">
|
||||
<div ref={itemsListRef} className={classname.list}>
|
||||
{items.map((itm, index) => (
|
||||
<ListItem key={index} {...listItemProps(props, itm, index)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderAfterListWrap()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ItemList.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
className: PropTypes.string,
|
||||
hideDate: PropTypes.bool,
|
||||
hideViews: PropTypes.bool,
|
||||
hideAuthor: PropTypes.bool,
|
||||
hidePlaylistOptions: PropTypes.bool,
|
||||
hidePlaylistOrderNumber: PropTypes.bool,
|
||||
hideAllMeta: PropTypes.bool,
|
||||
preferSummary: PropTypes.bool,
|
||||
inPlaylistView: PropTypes.bool,
|
||||
inPlaylistPage: PropTypes.bool,
|
||||
playlistActiveItem: PositiveIntegerOrZero,
|
||||
playlistId: PropTypes.string,
|
||||
/* ################################################## */
|
||||
maxItems: PropTypes.number.isRequired,
|
||||
pageItems: PropTypes.number.isRequired,
|
||||
horizontalItemsOrientation: PropTypes.bool.isRequired,
|
||||
singleLinkContent: PropTypes.bool.isRequired,
|
||||
inTagsList: PropTypes.bool,
|
||||
inCategoriesList: PropTypes.bool,
|
||||
itemsCountCallback: PropTypes.func,
|
||||
itemsLoadCallback: PropTypes.func,
|
||||
firstItemViewer: PropTypes.bool,
|
||||
firstItemDescr: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
};
|
||||
|
||||
ItemList.defaultProps = {
|
||||
hideDate: false,
|
||||
hideViews: false,
|
||||
hideAuthor: false,
|
||||
hidePlaylistOptions: true,
|
||||
hidePlaylistOrderNumber: true,
|
||||
hideAllMeta: false,
|
||||
preferSummary: false,
|
||||
inPlaylistView: false,
|
||||
inPlaylistPage: false,
|
||||
playlistActiveItem: 1,
|
||||
playlistId: void 0,
|
||||
/* ################################################## */
|
||||
maxItems: 99999,
|
||||
// pageItems: 48,
|
||||
pageItems: 24,
|
||||
horizontalItemsOrientation: false,
|
||||
singleLinkContent: false,
|
||||
inTagsList: false,
|
||||
inCategoriesList: false,
|
||||
firstItemViewer: false,
|
||||
firstItemDescr: false,
|
||||
canEdit: false,
|
||||
};
|
||||
149
frontend/src/static/js/components/item-list/ItemList.scss
Executable file
149
frontend/src/static/js/components/item-list/ItemList.scss
Executable file
@@ -0,0 +1,149 @@
|
||||
@import '../../../css/includes/_variables.scss';
|
||||
@import '../../../css/includes/_variables_dimensions.scss';
|
||||
|
||||
@import '../../../css/config/index.scss';
|
||||
|
||||
.items-list-outer {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
&.list-inline.list-slider {
|
||||
margin: 0 8px;
|
||||
|
||||
.previous-slide,
|
||||
.next-slide {
|
||||
position: absolute;
|
||||
z-index: +1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding-top: 28.125%;
|
||||
|
||||
.circle-icon-button {
|
||||
margin-top: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
.previous-slide {
|
||||
left: -12px;
|
||||
}
|
||||
|
||||
.next-slide {
|
||||
right: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
&.list-inline.list-slider {
|
||||
margin: 0;
|
||||
|
||||
.previous-slide {
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
.next-slide {
|
||||
right: -20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
&.list-inline.list-slider {
|
||||
.previous-slide,
|
||||
.next-slide {
|
||||
padding-top: calc(0.28125 * calc(var(--item-width, var(--default-item-width))));
|
||||
}
|
||||
|
||||
.next-slide {
|
||||
right: calc(-20px + var(--item-margin-right-width, var(--default-item-margin-right-width)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.items-list-header,
|
||||
.media-list-header {
|
||||
display: block;
|
||||
padding: 12px 0;
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
display: inline-block;
|
||||
margin: 12px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
margin: 10px 16px;
|
||||
text-decoration: none;
|
||||
color: var(--media-list-header-title-link-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.items-list-wrap {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: 218px;
|
||||
|
||||
.list-inline & {
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
will-change: width, scroll-position, scroll-behavior;
|
||||
|
||||
.item {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.list-slider & {
|
||||
overflow: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
.list-slider .items-list-wrap.resizing {
|
||||
scroll-behavior: unset;
|
||||
}
|
||||
|
||||
.items-list {
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
|
||||
img,
|
||||
picture {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button.load-more {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.007px;
|
||||
margin: 0 auto 24px 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
color: var(--item-list-load-more-text-color);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--item-list-load-more-hover-text-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@import '../list-item/Item.scss';
|
||||
@import '../list-item/ItemVertical.scss';
|
||||
@import '../list-item/ItemHorizontal.scss';
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useItemListSync } from '../../utils/hooks/';
|
||||
import { ItemList } from './ItemList';
|
||||
import { PendingItemsList } from './PendingItemsList';
|
||||
import { ListItem, listItemProps } from '../list-item/ListItem';
|
||||
import { ItemsListHandler } from './includes/itemLists/ItemsListHandler';
|
||||
|
||||
export function ItemListAsync(props) {
|
||||
const [
|
||||
countedItems,
|
||||
items,
|
||||
listHandler,
|
||||
setListHandler,
|
||||
classname,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
renderBeforeListWrap,
|
||||
renderAfterListWrap,
|
||||
] = useItemListSync(props);
|
||||
|
||||
useEffect(() => {
|
||||
setListHandler(
|
||||
new ItemsListHandler(
|
||||
props.pageItems,
|
||||
props.maxItems,
|
||||
props.firstItemRequestUrl,
|
||||
props.requestUrl,
|
||||
onItemsCount,
|
||||
onItemsLoad
|
||||
)
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
<div className={classname.listOuter}>
|
||||
{renderBeforeListWrap()}
|
||||
|
||||
<div ref={itemsListWrapperRef} className="items-list-wrap">
|
||||
<div ref={itemsListRef} className={classname.list}>
|
||||
{items.map((itm, index) => (
|
||||
<ListItem key={index} {...listItemProps(props, itm, index)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderAfterListWrap()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ItemListAsync.propTypes = {
|
||||
...ItemList.propTypes,
|
||||
items: PropTypes.array, // Reset 'isRequired' feature.
|
||||
requestUrl: PropTypes.string.isRequired,
|
||||
firstItemRequestUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
ItemListAsync.defaultProps = {
|
||||
...ItemList.defaultProps,
|
||||
requestUrl: null,
|
||||
firstItemRequestUrl: null,
|
||||
pageItems: 24,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { useItemListLazyLoad } from '../../utils/hooks/';
|
||||
import { ItemList } from './ItemList';
|
||||
import { PendingItemsList } from './PendingItemsList';
|
||||
import { ListItem, listItemProps } from '../list-item/ListItem';
|
||||
import { ItemsStaticListHandler } from './includes/itemLists/ItemsStaticListHandler';
|
||||
|
||||
export function LazyLoadItemList(props) {
|
||||
const [
|
||||
items,
|
||||
countedItems,
|
||||
listHandler,
|
||||
setListHandler,
|
||||
classname,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
onWindowScroll,
|
||||
onDocumentVisibilityChange,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
renderBeforeListWrap,
|
||||
renderAfterListWrap,
|
||||
] = useItemListLazyLoad(props);
|
||||
|
||||
useEffect(() => {
|
||||
setListHandler(new ItemsStaticListHandler(props.items, props.pageItems, props.maxItems, onItemsCount, onItemsLoad));
|
||||
|
||||
PageStore.on('window_scroll', onWindowScroll);
|
||||
PageStore.on('document_visibility_change', onDocumentVisibilityChange);
|
||||
|
||||
onWindowScroll();
|
||||
|
||||
return () => {
|
||||
PageStore.removeListener('window_scroll', onWindowScroll);
|
||||
PageStore.removeListener('document_visibility_change', onDocumentVisibilityChange);
|
||||
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
<div className={classname.listOuter}>
|
||||
{renderBeforeListWrap()}
|
||||
|
||||
<div ref={itemsListWrapperRef} className="items-list-wrap">
|
||||
<div ref={itemsListRef} className={classname.list}>
|
||||
{items.map((itm, index) => (
|
||||
<ListItem key={index} {...listItemProps(props, itm, index)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderAfterListWrap()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LazyLoadItemList.propTypes = {
|
||||
...ItemList.propTypes,
|
||||
};
|
||||
|
||||
LazyLoadItemList.defaultProps = {
|
||||
...ItemList.defaultProps,
|
||||
pageItems: 2,
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { useItemListLazyLoad } from '../../utils/hooks/';
|
||||
import { ItemListAsync } from './ItemListAsync';
|
||||
import { PendingItemsList } from './PendingItemsList';
|
||||
import { ListItem, listItemProps } from '../list-item/ListItem';
|
||||
import { ItemsListHandler } from './includes/itemLists/ItemsListHandler';
|
||||
|
||||
export function LazyLoadItemListAsync(props) {
|
||||
const [
|
||||
items,
|
||||
countedItems,
|
||||
listHandler,
|
||||
setListHandler,
|
||||
classname,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
onWindowScroll,
|
||||
onDocumentVisibilityChange,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
renderBeforeListWrap,
|
||||
renderAfterListWrap,
|
||||
] = useItemListLazyLoad(props);
|
||||
|
||||
useEffect(() => {
|
||||
setListHandler(
|
||||
new ItemsListHandler(
|
||||
props.pageItems,
|
||||
props.maxItems,
|
||||
props.firstItemRequestUrl,
|
||||
props.requestUrl,
|
||||
onItemsCount,
|
||||
onItemsLoad
|
||||
)
|
||||
);
|
||||
|
||||
PageStore.on('window_scroll', onWindowScroll);
|
||||
PageStore.on('document_visibility_change', onDocumentVisibilityChange);
|
||||
|
||||
onWindowScroll();
|
||||
|
||||
return () => {
|
||||
PageStore.removeListener('window_scroll', onWindowScroll);
|
||||
PageStore.removeListener('document_visibility_change', onDocumentVisibilityChange);
|
||||
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
<div className={classname.listOuter}>
|
||||
{renderBeforeListWrap()}
|
||||
|
||||
<div ref={itemsListWrapperRef} className="items-list-wrap">
|
||||
<div ref={itemsListRef} className={classname.list}>
|
||||
{items.map((itm, index) => (
|
||||
<ListItem key={index} {...listItemProps(props, itm, index)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderAfterListWrap()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LazyLoadItemListAsync.propTypes = {
|
||||
...ItemListAsync.propTypes,
|
||||
};
|
||||
|
||||
LazyLoadItemListAsync.defaultProps = {
|
||||
...ItemListAsync.defaultProps,
|
||||
pageItems: 2,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { SpinnerLoader } from '../_shared/';
|
||||
|
||||
export function PendingItemsList(props) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className="items-list-wrap items-list-wrap-waiting">
|
||||
<SpinnerLoader />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
function calcCurrentSlide(wrapperDom, itemWidth, currentSlide) {
|
||||
return wrapperDom.scrollLeft ? 1 + Math.ceil(wrapperDom.scrollLeft / itemWidth) : currentSlide;
|
||||
}
|
||||
|
||||
export default function ItemsInlineSlider(container, itemSelector) {
|
||||
if (void 0 === container) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = {
|
||||
dom: {
|
||||
wrapper: container,
|
||||
firstItem: container.querySelector(itemSelector),
|
||||
},
|
||||
item: {
|
||||
// selector: itemSelector,
|
||||
width: null,
|
||||
},
|
||||
};
|
||||
|
||||
this.data.item.width = this.data.dom.firstItem.offsetWidth;
|
||||
|
||||
this.state = {
|
||||
initedAllStateValues: false,
|
||||
currentSlide: 1,
|
||||
maxSlideIndex: null,
|
||||
slideItemsFit: null,
|
||||
slideItems: null,
|
||||
totalItems: null,
|
||||
wrapper: {
|
||||
width: null,
|
||||
scrollWidth: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ItemsInlineSlider.prototype.updateDataStateOnResize = function (totalItems, itemsLoadedAll) {
|
||||
this.data.item.width = this.data.dom.firstItem.offsetWidth;
|
||||
|
||||
this.state.wrapper.width = this.data.dom.wrapper.offsetWidth;
|
||||
this.state.wrapper.scrollWidth = this.data.dom.wrapper.scrollWidth;
|
||||
this.state.slideItemsFit = Math.floor(this.state.wrapper.width / this.data.item.width);
|
||||
|
||||
this.state.slideItems = Math.max(1, this.state.slideItemsFit);
|
||||
|
||||
if (itemsLoadedAll && this.state.slideItems <= this.state.slideItemsFit) {
|
||||
this.state.itemsLengthFit = this.state.slideItems;
|
||||
}
|
||||
|
||||
this.state.totalItems = totalItems;
|
||||
|
||||
this.state.maxSlideIndex = Math.max(1 + (this.state.totalItems - this.state.slideItemsFit));
|
||||
|
||||
this.state.currentSlide = Math.min(this.state.currentSlide, this.state.maxSlideIndex || 1);
|
||||
this.state.currentSlide = 0 >= this.state.currentSlide ? 1 : this.state.currentSlide;
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.updateDataState = function (totalItems, itemsLoadedAll, forcedRefresh) {
|
||||
if (forcedRefresh || !this.state.initedAllStateValues) {
|
||||
this.state.initedAllStateValues = true;
|
||||
|
||||
this.state.wrapper.width = this.data.dom.wrapper.offsetWidth;
|
||||
this.state.wrapper.scrollWidth = this.data.dom.wrapper.scrollWidth;
|
||||
this.state.slideItemsFit = Math.floor(this.state.wrapper.width / this.data.item.width);
|
||||
|
||||
this.state.slideItems = Math.max(1, this.state.slideItemsFit);
|
||||
|
||||
if (itemsLoadedAll && this.state.slideItems <= this.state.slideItemsFit) {
|
||||
this.state.itemsLengthFit = this.state.slideItems;
|
||||
}
|
||||
}
|
||||
|
||||
this.state.totalItems = totalItems;
|
||||
|
||||
this.state.maxSlideIndex = Math.max(1, 1 + (this.state.totalItems - this.state.slideItemsFit));
|
||||
|
||||
this.state.currentSlide = Math.min(this.state.currentSlide, this.state.maxSlideIndex);
|
||||
this.state.currentSlide = 0 >= this.state.currentSlide ? 1 : this.state.currentSlide;
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.nextSlide = function () {
|
||||
this.state.currentSlide = Math.min(
|
||||
calcCurrentSlide(this.data.dom.wrapper, this.data.item.width, this.state.currentSlide) + this.state.slideItems,
|
||||
this.state.maxSlideIndex
|
||||
);
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.previousSlide = function () {
|
||||
this.state.currentSlide = Math.max(
|
||||
1,
|
||||
calcCurrentSlide(this.data.dom.wrapper, this.data.item.width, this.state.currentSlide) - this.state.slideItems
|
||||
);
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.scrollToCurrentSlide = function () {
|
||||
this.data.dom.wrapper.scrollLeft = this.data.item.width * (this.state.currentSlide - 1);
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.hasNextSlide = function () {
|
||||
return this.state.currentSlide < this.state.maxSlideIndex;
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.hasPreviousSlide = function () {
|
||||
return 1 < this.state.currentSlide;
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.currentSlide = function () {
|
||||
return this.state.currentSlide;
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.loadItemsToFit = function () {
|
||||
// Set slider minimum items length ( 2 * this.state.slideItemsFit ).
|
||||
return 2 * this.state.slideItemsFit > this.state.totalItems;
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.loadMoreItems = function () {
|
||||
return this.state.currentSlide + this.state.slideItemsFit >= this.state.maxSlideIndex;
|
||||
};
|
||||
|
||||
ItemsInlineSlider.prototype.itemsFit = function () {
|
||||
return this.state.slideItemsFit;
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import { PageStore } from '../../../../utils/stores/';
|
||||
import { formatInnerLink, getRequest } from '../../../../utils/helpers/';
|
||||
|
||||
export function ItemsListHandler(
|
||||
itemsPerPage,
|
||||
maxItems,
|
||||
first_item_request_url,
|
||||
request_url,
|
||||
itemsCountCallback,
|
||||
loadItemsCallback
|
||||
) {
|
||||
const config = {
|
||||
maxItems: maxItems || 255,
|
||||
pageItems: itemsPerPage ? Math.min(maxItems, itemsPerPage) : 1,
|
||||
};
|
||||
|
||||
const state = {
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
nextRequestUrl: formatInnerLink(request_url, PageStore.get('config-site').url),
|
||||
};
|
||||
|
||||
const waiting = {
|
||||
pageItems: 0,
|
||||
requestResponse: false,
|
||||
};
|
||||
|
||||
let firstItemUrl = null;
|
||||
|
||||
const items = [];
|
||||
const responseItems = [];
|
||||
|
||||
const callbacks = {
|
||||
itemsCount: function () {
|
||||
if ('function' === typeof itemsCountCallback) {
|
||||
itemsCountCallback(state.totalItems);
|
||||
}
|
||||
},
|
||||
itemsLoad: function () {
|
||||
if ('function' === typeof loadItemsCallback) {
|
||||
loadItemsCallback(items);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function loadNextItems(itemsLength) {
|
||||
let itemsToLoad, needExtraRequest;
|
||||
|
||||
itemsLength = !isNaN(itemsLength) ? itemsLength : config.pageItems;
|
||||
|
||||
if (waiting.pageItems && waiting.pageItems <= responseItems.length) {
|
||||
itemsToLoad = waiting.pageItems;
|
||||
needExtraRequest = false;
|
||||
waiting.pageItems = 0;
|
||||
} else {
|
||||
itemsToLoad = Math.min(itemsLength, responseItems.length);
|
||||
needExtraRequest = itemsLength > responseItems.length && !!state.nextRequestUrl;
|
||||
waiting.pageItems = needExtraRequest ? itemsLength - responseItems.length : 0;
|
||||
}
|
||||
|
||||
if (itemsToLoad) {
|
||||
let i = 0;
|
||||
while (i < itemsToLoad) {
|
||||
items.push(responseItems.shift());
|
||||
i += 1;
|
||||
}
|
||||
callbacks.itemsLoad();
|
||||
}
|
||||
|
||||
if (needExtraRequest) {
|
||||
runRequest();
|
||||
}
|
||||
}
|
||||
|
||||
function runFirstItemRequest() {
|
||||
function fn(response) {
|
||||
if (!!!response || !!!response.data) {
|
||||
} else {
|
||||
let data = response.data;
|
||||
let results = void 0 !== data.results ? data.results : data; // NOTE: The structure of response data in case of categories differs from the others.
|
||||
|
||||
if (results.length) {
|
||||
firstItemUrl = results[0].url;
|
||||
items.push(results[0]);
|
||||
}
|
||||
}
|
||||
|
||||
runRequest(true);
|
||||
}
|
||||
|
||||
getRequest(formatInnerLink(first_item_request_url, PageStore.get('config-site').url), false, fn);
|
||||
}
|
||||
|
||||
function runRequest(initialRequest) {
|
||||
waiting.requestResponse = true;
|
||||
|
||||
function fn(response) {
|
||||
waiting.requestResponse = false;
|
||||
|
||||
if (!!!response || !!!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = response.data;
|
||||
let results = void 0 !== data.results ? data.results : data; // NOTE: The structure of response data in case of categories differs from the others.
|
||||
|
||||
let i = 0;
|
||||
while (i < results.length && config.maxItems > responseItems.length) {
|
||||
if (null === firstItemUrl || firstItemUrl !== results[i].url) {
|
||||
responseItems.push(results[i]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
state.nextRequestUrl = !!data.next && config.maxItems > responseItems.length ? data.next : null;
|
||||
|
||||
if (initialRequest) {
|
||||
// In some cases, (total) 'count' field is missing, but probably doesn't need (eg. in recommended media).
|
||||
state.totalItems = !!data.count ? data.count : responseItems.length;
|
||||
state.totalItems = Math.min(config.maxItems, state.totalItems);
|
||||
|
||||
state.totalPages = Math.ceil(state.totalItems / config.pageItems);
|
||||
|
||||
callbacks.itemsCount();
|
||||
}
|
||||
|
||||
loadNextItems();
|
||||
}
|
||||
|
||||
getRequest(state.nextRequestUrl, false, fn);
|
||||
|
||||
state.nextRequestUrl = null;
|
||||
}
|
||||
|
||||
function loadItems(itemsLength) {
|
||||
if (!waiting.requestResponse && items.length < state.totalItems) {
|
||||
loadNextItems(itemsLength);
|
||||
}
|
||||
}
|
||||
|
||||
function totalPages() {
|
||||
return state.totalPages;
|
||||
}
|
||||
|
||||
function loadedAllItems() {
|
||||
return items.length === state.totalItems;
|
||||
}
|
||||
|
||||
function cancelAll() {
|
||||
itemsCountCallback = null;
|
||||
loadItemsCallback = null;
|
||||
}
|
||||
|
||||
if (void 0 !== first_item_request_url && null !== first_item_request_url) {
|
||||
runFirstItemRequest();
|
||||
} else {
|
||||
runRequest(true);
|
||||
}
|
||||
|
||||
return {
|
||||
loadItems,
|
||||
totalPages,
|
||||
loadedAllItems,
|
||||
cancelAll,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
export function ItemsStaticListHandler(itemsArray, itemsPerPage, maxItems, itemsCountCallback, loadItemsCallback) {
|
||||
const config = {
|
||||
maxItems: maxItems || 255,
|
||||
pageItems: itemsPerPage ? Math.min(maxItems, itemsPerPage) : 1,
|
||||
};
|
||||
|
||||
const state = {
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
};
|
||||
|
||||
let results = itemsArray;
|
||||
|
||||
const items = [];
|
||||
const responseItems = [];
|
||||
|
||||
const callbacks = {
|
||||
itemsCount: function () {
|
||||
if ('function' === typeof itemsCountCallback) {
|
||||
itemsCountCallback(state.totalItems);
|
||||
}
|
||||
},
|
||||
itemsLoad: function () {
|
||||
if ('function' === typeof loadItemsCallback) {
|
||||
loadItemsCallback(items);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function loadNextItems(itemsLength) {
|
||||
itemsLength = !isNaN(itemsLength) ? itemsLength : config.pageItems;
|
||||
|
||||
let itemsToLoad = Math.min(itemsLength, responseItems.length);
|
||||
|
||||
if (itemsToLoad) {
|
||||
let i = 0;
|
||||
while (i < itemsToLoad) {
|
||||
items.push(responseItems.shift());
|
||||
i += 1;
|
||||
}
|
||||
|
||||
callbacks.itemsLoad();
|
||||
}
|
||||
}
|
||||
|
||||
function loadItems(itemsLength) {
|
||||
if (items.length < state.totalItems) {
|
||||
loadNextItems(itemsLength);
|
||||
}
|
||||
}
|
||||
|
||||
function totalPages() {
|
||||
return state.totalPages;
|
||||
}
|
||||
|
||||
function loadedAllItems() {
|
||||
return items.length === state.totalItems;
|
||||
}
|
||||
|
||||
function cancelAll() {
|
||||
itemsCountCallback = null;
|
||||
loadItemsCallback = null;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (i < results.length && config.maxItems > responseItems.length) {
|
||||
responseItems.push(results[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
state.totalItems = Math.min(config.maxItems, results.length);
|
||||
state.totalPages = Math.ceil(state.totalItems / config.pageItems);
|
||||
|
||||
callbacks.itemsCount();
|
||||
|
||||
loadNextItems();
|
||||
|
||||
return {
|
||||
loadItems,
|
||||
totalPages,
|
||||
loadedAllItems,
|
||||
cancelAll,
|
||||
};
|
||||
}
|
||||
53
frontend/src/static/js/components/item-list/includes/itemLists/MediaItem.js
Executable file
53
frontend/src/static/js/components/item-list/includes/itemLists/MediaItem.js
Executable file
@@ -0,0 +1,53 @@
|
||||
import MediaItemPreviewer from './MediaItemPreviewer';
|
||||
|
||||
let mediaPreviewerInstance = null;
|
||||
|
||||
var CSS_selectors = {
|
||||
mediaItemPreviewer: '.item-img-preview',
|
||||
};
|
||||
|
||||
var DataAttributes = {
|
||||
mediaPreviewSrc: 'data-src',
|
||||
mediaPreviewExt: 'data-ext',
|
||||
};
|
||||
|
||||
export default class MediaItem {
|
||||
constructor(item) {
|
||||
if (!Node.prototype.isPrototypeOf(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.element = item;
|
||||
|
||||
this.previewer = {
|
||||
element: item.querySelector(CSS_selectors.mediaItemPreviewer),
|
||||
};
|
||||
|
||||
let tmp;
|
||||
|
||||
if (this.previewer.element) {
|
||||
tmp = this.previewer.element.getAttribute(DataAttributes.mediaPreviewSrc);
|
||||
|
||||
if (tmp) {
|
||||
this.previewer.src = tmp.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.previewer.src) {
|
||||
tmp = this.previewer.element.getAttribute(DataAttributes.mediaPreviewExt);
|
||||
|
||||
if (tmp) {
|
||||
this.previewer.extensions = tmp.trim().split(',');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.previewer.extensions) {
|
||||
mediaPreviewerInstance = mediaPreviewerInstance || new MediaItemPreviewer(this.previewer.extensions);
|
||||
mediaPreviewerInstance.elementEvents(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
element() {
|
||||
return this.element;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { PageStore } from '../../../../utils/stores/';
|
||||
import { cancelAnimationFrame, requestAnimationFrame, addClassname } from '../../../../utils/helpers/';
|
||||
|
||||
Array.isArray =
|
||||
Array.isArray ||
|
||||
function (arg) {
|
||||
'use strict';
|
||||
return Object.prototype.toString.call(arg) === '[object Array]';
|
||||
};
|
||||
|
||||
var hoverTimeoutID, requestAnimationFrameID;
|
||||
|
||||
var CSS_selectors = {
|
||||
mediaItemPreviewer: '.item-img-preview',
|
||||
};
|
||||
|
||||
var DataAttributes = {
|
||||
mediaPreviewSrc: 'data-src',
|
||||
};
|
||||
|
||||
export default class MediaItemPreviewer {
|
||||
constructor(extensions) {
|
||||
if (!Array.isArray(extensions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.extensions = {};
|
||||
|
||||
function onImageLoad(ins, evt) {
|
||||
requestAnimationFrameID = requestAnimationFrame(function () {
|
||||
if (ins.wrapperItem) {
|
||||
addClassname(ins.wrapperItem, 'on-hover-preview');
|
||||
requestAnimationFrameID = void 0;
|
||||
ins.wrapperItem = void 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fallback_ext = ['png', 'jpg', 'jpeg']; // NOTE: Keep extentions order.
|
||||
let i, x;
|
||||
|
||||
this.element = null;
|
||||
|
||||
if (-1 < extensions.indexOf('webp')) {
|
||||
x = document.createElement('source');
|
||||
x.type = 'image/webp';
|
||||
this.extensions.anim = this.extensions.anim || [];
|
||||
this.extensions.anim.push({ elem: x, type: 'webp' });
|
||||
if (1 === extensions.length) {
|
||||
this.extensions.fallback = { elem: document.createElement('img'), type: 'webp' };
|
||||
}
|
||||
}
|
||||
|
||||
if (-1 < extensions.indexOf('gif')) {
|
||||
x = document.createElement('source');
|
||||
x.type = 'image/gif';
|
||||
this.extensions.anim = this.extensions.anim || [];
|
||||
this.extensions.anim.push({ elem: x, type: 'gif' });
|
||||
this.extensions.fallback = { elem: document.createElement('img'), type: 'gif' };
|
||||
}
|
||||
|
||||
if (-1 < extensions.indexOf('jpg')) {
|
||||
x = document.createElement('source');
|
||||
x.type = 'image/jpg';
|
||||
this.extensions.anim = this.extensions.anim || [];
|
||||
this.extensions.anim.push({ elem: x, type: 'jpg' });
|
||||
this.extensions.fallback = { elem: document.createElement('img'), type: 'jpg' };
|
||||
}
|
||||
|
||||
if (-1 < extensions.indexOf('jpeg')) {
|
||||
x = document.createElement('source');
|
||||
x.type = 'image/jpeg';
|
||||
this.extensions.anim = this.extensions.anim || [];
|
||||
this.extensions.anim.push({ elem: x, type: 'jpeg' });
|
||||
this.extensions.fallback = { elem: document.createElement('img'), type: 'jpeg' };
|
||||
}
|
||||
|
||||
if (!this.extensions.fallback.elem) {
|
||||
i = 0;
|
||||
while (i < fallback_extensions.length) {
|
||||
if (-1 < extensions.indexOf(fallback_ext[i])) {
|
||||
this.extensions.fallback = { elem: document.createElement('img'), type: fallback_ext[i] };
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extensions.anim.length || this.extensions.fallback.elem) {
|
||||
this.element = document.createElement('picture');
|
||||
|
||||
if (this.extensions.anim.length) {
|
||||
i = 0;
|
||||
while (i < this.extensions.anim.length) {
|
||||
this.element.appendChild(this.extensions.anim[i].elem);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extensions.fallback.elem) {
|
||||
this.element.appendChild(this.extensions.fallback.elem);
|
||||
}
|
||||
|
||||
this.image = this.element.querySelector('img');
|
||||
this.image.addEventListener('load', onImageLoad.bind(null, this));
|
||||
}
|
||||
}
|
||||
|
||||
elementEvents(el) {
|
||||
el.addEventListener('mouseenter', this.onMediaItemMouseEnter.bind(null, this));
|
||||
el.addEventListener('mouseleave', this.onMediaItemMouseLeave.bind(null, this));
|
||||
}
|
||||
|
||||
newImage(src, width, height, item) {
|
||||
let i;
|
||||
|
||||
if (void 0 !== hoverTimeoutID) {
|
||||
clearTimeout(hoverTimeoutID);
|
||||
}
|
||||
|
||||
if (void 0 !== requestAnimationFrameID) {
|
||||
cancelAnimationFrame(requestAnimationFrameID);
|
||||
}
|
||||
|
||||
/*
|
||||
* Set source (src).
|
||||
*/
|
||||
|
||||
if (this.extensions.anim.length) {
|
||||
i = 0;
|
||||
while (i < this.extensions.anim.length) {
|
||||
this.extensions.anim[i].elem.setAttribute('srcset', src + '.' + this.extensions.anim[i].type);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if (this.extensions.fallback.elem) {
|
||||
this.extensions.fallback.elem.setAttribute('src', src + '.' + this.extensions.fallback.type);
|
||||
}
|
||||
|
||||
/*
|
||||
* Set dimensions (src).
|
||||
*/
|
||||
|
||||
if (this.extensions.fallback.elem) {
|
||||
this.extensions.fallback.elem.setAttribute('width', width + 'px');
|
||||
this.extensions.fallback.elem.setAttribute('height', height + 'px');
|
||||
}
|
||||
|
||||
/*
|
||||
* Append previewer.
|
||||
*/
|
||||
|
||||
item.querySelector(CSS_selectors.mediaItemPreviewer).appendChild(this.element);
|
||||
|
||||
/*
|
||||
* Set previewer's container element.
|
||||
*/
|
||||
|
||||
this.wrapperItem = item;
|
||||
}
|
||||
|
||||
onMediaItemMouseEnter(ins, evt) {
|
||||
var elem, src;
|
||||
|
||||
if (ins.image) {
|
||||
elem = evt.target.querySelector(CSS_selectors.mediaItemPreviewer);
|
||||
src =
|
||||
PageStore.get('config-site').url + '/' + elem.getAttribute(DataAttributes.mediaPreviewSrc).replace(/^\//g, '');
|
||||
|
||||
hoverTimeoutID = setTimeout(function () {
|
||||
ins.newImage(src, 1 + elem.offsetWidth, 1 + elem.offsetHeight, evt.target);
|
||||
}, 100); // NOTE: Avoid loading unnecessary media, when mouse is moving fast over dom items.
|
||||
}
|
||||
}
|
||||
|
||||
onMediaItemMouseLeave(ins, evt) {
|
||||
if (void 0 !== hoverTimeoutID) {
|
||||
clearTimeout(hoverTimeoutID);
|
||||
}
|
||||
|
||||
if (void 0 !== requestAnimationFrameID) {
|
||||
cancelAnimationFrame(requestAnimationFrameID);
|
||||
}
|
||||
|
||||
ins.wrapperItem = void 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import MediaItem from './MediaItem';
|
||||
import { hasClassname } from '../../../../utils/helpers/';
|
||||
|
||||
const _MediaItemsListData = {};
|
||||
|
||||
export default class MediaItemsList {
|
||||
constructor(listContainer, initialItems) {
|
||||
if (!Node.prototype.isPrototypeOf(listContainer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_MediaItemsListData[
|
||||
Object.defineProperty(this, 'id', { value: 'MediaItemsList_' + Object.keys(_MediaItemsListData).length }).id
|
||||
] = {};
|
||||
|
||||
this.items = [];
|
||||
this.container = listContainer;
|
||||
this.horizontalItems = hasClassname(this.container, 'items-list-hor');
|
||||
|
||||
this.appendItems(initialItems);
|
||||
}
|
||||
|
||||
dataObject() {
|
||||
return _MediaItemsListData;
|
||||
}
|
||||
|
||||
appendItems(items) {
|
||||
var i;
|
||||
if (NodeList.prototype.isPrototypeOf(items)) {
|
||||
i = 0;
|
||||
while (i < items.length) {
|
||||
this.items.push(new MediaItem(items[i]));
|
||||
i += 1;
|
||||
}
|
||||
} else if (Node.prototype.isPrototypeOf(items)) {
|
||||
this.items.push(new MediaItem(items));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import MediaItemsList from './MediaItemsList';
|
||||
|
||||
var CSS_selectors = {
|
||||
mediaItems: '.item',
|
||||
};
|
||||
|
||||
var mediaItemsListInstances = [];
|
||||
|
||||
export default function (lists) {
|
||||
if (!lists.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let items,
|
||||
i = 0;
|
||||
|
||||
while (i < lists.length) {
|
||||
items = lists[i].querySelectorAll(CSS_selectors.mediaItems);
|
||||
|
||||
if (items.length) {
|
||||
mediaItemsListInstances = mediaItemsListInstances || [];
|
||||
mediaItemsListInstances.push(new MediaItemsList(lists[i], items));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return mediaItemsListInstances;
|
||||
}
|
||||
7
frontend/src/static/js/components/item-list/index.js
Normal file
7
frontend/src/static/js/components/item-list/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './InlineSliderItemList.jsx';
|
||||
export * from './InlineSliderItemListAsync.jsx';
|
||||
export * from './ItemList.jsx';
|
||||
export * from './ItemListAsync.jsx';
|
||||
export * from './LazyLoadItemList.jsx';
|
||||
export * from './LazyLoadItemListAsync.jsx';
|
||||
export * from './PendingItemsList.jsx';
|
||||
28
frontend/src/static/js/components/list-item/Item.jsx
Normal file
28
frontend/src/static/js/components/list-item/Item.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
|
||||
export function Item(props) { }
|
||||
|
||||
Item.propTypes = {
|
||||
order: PositiveIntegerOrZero,
|
||||
title: PropTypes.string.isRequired,
|
||||
link: PropTypes.string.isRequired,
|
||||
singleLinkContent: PropTypes.bool.isRequired,
|
||||
description: PropTypes.string,
|
||||
meta_description: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
onMount: PropTypes.func,
|
||||
publish_date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
editLink: PropTypes.string,
|
||||
};
|
||||
|
||||
Item.defaultProps = {
|
||||
title: '',
|
||||
link: '#',
|
||||
singleLinkContent: false,
|
||||
description: '',
|
||||
meta_description: '',
|
||||
thumbnail: '',
|
||||
publish_date: 0,
|
||||
};
|
||||
590
frontend/src/static/js/components/list-item/Item.scss
Executable file
590
frontend/src/static/js/components/list-item/Item.scss
Executable file
@@ -0,0 +1,590 @@
|
||||
@import '../../../css/includes/_variables.scss';
|
||||
@import '../../../css/includes/_variables_dimensions.scss';
|
||||
|
||||
@import '../../../css/config/index.scss';
|
||||
|
||||
.item {
|
||||
vertical-align: top;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: var(--max-item-width, var(--default-max-item-width));
|
||||
margin-bottom: var(--item-margin-bottom-width, var(--default-item-margin-bottom-width));
|
||||
}
|
||||
|
||||
.item-thumb,
|
||||
a.item-thumb {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: auto;
|
||||
padding-bottom: 56.11%;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-color: var(--item-thumb-bg-color);
|
||||
}
|
||||
|
||||
.item-thumb.no-thumb {
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: 0;
|
||||
margin-top: -1rem;
|
||||
margin-left: -1rem;
|
||||
font-size: 2rem;
|
||||
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
font-family: 'Material Icons';
|
||||
text-decoration: none;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
// @link: https://github.com/google/material-design-icons/blob/master/iconfont/codepoints
|
||||
|
||||
.item.video-item & {
|
||||
&:before {
|
||||
content: '\E02C';
|
||||
content: '\E54D';
|
||||
content: '\E04B';
|
||||
}
|
||||
}
|
||||
|
||||
.item.image-item & {
|
||||
&:before {
|
||||
content: '\E3F4';
|
||||
content: '\E412';
|
||||
}
|
||||
}
|
||||
|
||||
.item.audio-item & {
|
||||
&:before {
|
||||
content: '\E3A1';
|
||||
}
|
||||
}
|
||||
|
||||
.item.pdf-item &,
|
||||
.item.attachment-item & {
|
||||
&:before {
|
||||
content: '\e415';
|
||||
content: '\e24d';
|
||||
}
|
||||
}
|
||||
|
||||
.item.playlist-item & {
|
||||
&:before {
|
||||
content: '\e43c';
|
||||
}
|
||||
}
|
||||
|
||||
.item.category-item & {
|
||||
&:before {
|
||||
content: '\e892';
|
||||
content: 'list_alt';
|
||||
}
|
||||
}
|
||||
|
||||
.item.tag-item & {
|
||||
&:before {
|
||||
content: '\e54e';
|
||||
}
|
||||
}
|
||||
|
||||
.item.other-item & {
|
||||
&:before {
|
||||
content: '\e2bc';
|
||||
content: '\e24d';
|
||||
}
|
||||
}
|
||||
|
||||
.item.member-item & {
|
||||
&:before {
|
||||
content: 'person';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: block;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.item-img-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 750ms;
|
||||
}
|
||||
|
||||
.item-duration,
|
||||
.item-type-icon {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
padding: 2px 4px;
|
||||
color: hsl(0, 0%, 100%);
|
||||
background-color: hsl(0, 0%, 6.7%);
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.item-duration {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 13.5px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.item-type-icon {
|
||||
> * {
|
||||
float: left;
|
||||
|
||||
&:before {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item.video-item & {
|
||||
&:before {
|
||||
content: '\E02C';
|
||||
content: '\E54D';
|
||||
content: '\E04B';
|
||||
}
|
||||
}
|
||||
|
||||
.item.audio-item & {
|
||||
&:before {
|
||||
content: '\E3A1';
|
||||
}
|
||||
}
|
||||
|
||||
.item.image-item & {
|
||||
&:before {
|
||||
content: '\E3F4';
|
||||
content: '\E412';
|
||||
}
|
||||
}
|
||||
|
||||
.item.pdf-item &,
|
||||
.item.attachment-item & {
|
||||
&:before {
|
||||
content: '\e24d';
|
||||
}
|
||||
}
|
||||
|
||||
.item.category-item & {
|
||||
&:before {
|
||||
content: '\e892';
|
||||
content: 'list_alt';
|
||||
}
|
||||
}
|
||||
|
||||
.item.tag-item & {
|
||||
&:before {
|
||||
content: '\e54e';
|
||||
}
|
||||
}
|
||||
|
||||
.item.other-item & {
|
||||
&:before {
|
||||
content: '\e001';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
clear: left;
|
||||
float: left;
|
||||
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 18px;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
color: var(--item-meta-text-color);
|
||||
|
||||
> * {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
a,
|
||||
a {
|
||||
color: var(--item-meta-link-text-color);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--item-meta-link-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-author {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.item-views {
|
||||
}
|
||||
|
||||
.item-views + .item-date {
|
||||
&:before {
|
||||
content: '•';
|
||||
content: '\2022';
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-description {
|
||||
$descr-font-size: 13px;
|
||||
$descr-max-lines: 2;
|
||||
$descr-line-height: 18px;
|
||||
|
||||
color: rgb(136, 136, 136);
|
||||
|
||||
font-size: $descr-font-size;
|
||||
line-height: 1em;
|
||||
width: 100%;
|
||||
float: left;
|
||||
margin: 10px 0 8px;
|
||||
// overflow: hidden;
|
||||
|
||||
/* Only for non-webkit */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $descr-max-lines;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
/* Fallback for non-webkit */
|
||||
max-height: $descr-max-lines * $descr-line-height;
|
||||
|
||||
div {
|
||||
@include multiline_texts_excerpt(
|
||||
$font-size: $descr-font-size,
|
||||
$line-height: $descr-line-height,
|
||||
$lines-to-show: $descr-max-lines,
|
||||
$bg-color: transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.item.on-hover-preview {
|
||||
&:hover {
|
||||
.item-img-preview {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
h3 {
|
||||
display: inline-block;
|
||||
clear: right;
|
||||
width: auto;
|
||||
position: relative;
|
||||
|
||||
/* Fallback for non-webkit */
|
||||
max-height: calc(var(--item-title-max-lines) * var(--item-title-line-height));
|
||||
|
||||
/* Only for non-webkit */
|
||||
/*display: -webkit-box;
|
||||
-webkit-line-clamp: var(--item-title-max-lines);
|
||||
-webkit-box-orient: vertical;*/
|
||||
|
||||
a {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: var(--item-title-line-height);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: var(--item-bg-color);
|
||||
|
||||
/* Fallback for non-webkit */
|
||||
display: block;
|
||||
max-height: calc(var(--item-title-max-lines) * var(--item-title-line-height));
|
||||
|
||||
/* Only for non-webkit */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: var(--item-title-max-lines);
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content-link {
|
||||
h3 {
|
||||
text-decoration: none;
|
||||
color: var(--item-title-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
line-height: var(--item-title-line-height);
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: var(--item-title-font-size);
|
||||
line-height: var(--item-title-line-height);
|
||||
margin-top: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.item-edit-link {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
padding: 8px 0;
|
||||
font-size: 0.928571429em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border-radius: 1px 1px 0 0;
|
||||
color: #fff;
|
||||
background-color: var(--brand-color, var(--default-brand-color));
|
||||
}
|
||||
|
||||
.playlist-item {
|
||||
.playlist-count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 92px;
|
||||
display: block;
|
||||
line-height: 1.25;
|
||||
color: rgba(#fff, 0.8);
|
||||
background-color: rgba(17, 17, 17, 0.8);
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 29px;
|
||||
margin: 1px 0 0 4px;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-hover-play-all {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgba(#fff, 0.8);
|
||||
background-color: rgba(17, 17, 17, 0.8);
|
||||
|
||||
letter-spacing: 0.007px;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-count,
|
||||
.playlist-hover-play-all {
|
||||
> * {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.playlist-hover-play-all {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-main {
|
||||
.item-meta {
|
||||
}
|
||||
|
||||
a.view-full-playlist {
|
||||
position: relative;
|
||||
float: left;
|
||||
clear: both;
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.view-full-playlist {
|
||||
color: var(--playlist-item-main-view-full-link-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--playlist-item-main-view-full-link-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hover-overlay-title {
|
||||
.item {
|
||||
.item-main,
|
||||
.item-content-link {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-content-link {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
h3 {
|
||||
z-index: +1;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:after,
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
content: '';
|
||||
display: block;
|
||||
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
transition-property: color, background-color;
|
||||
transition-duration: 0.2s;
|
||||
|
||||
&:before {
|
||||
opacity: 1;
|
||||
background: radial-gradient(circle, rgba(#000, 0.75) 0%, rgba(#4a4a4a, 0.75) 100%);
|
||||
}
|
||||
|
||||
&:after {
|
||||
opacity: 0;
|
||||
background: radial-gradient(circle, rgba(#fff, 0.75) 0%, rgba(#c6c6c6, 0.75) 100%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
h3 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&:before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
max-height: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 8px;
|
||||
|
||||
font-size: 1.5em;
|
||||
|
||||
span {
|
||||
max-height: 100% !important;
|
||||
line-height: 1.15;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item-meta,
|
||||
.item-description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
frontend/src/static/js/components/list-item/ItemHorizontal.scss
Executable file
117
frontend/src/static/js/components/list-item/ItemHorizontal.scss
Executable file
@@ -0,0 +1,117 @@
|
||||
@import '../../../css/config/index.scss';
|
||||
|
||||
@media (min-width: 390px) {
|
||||
.items-list-hor .item {
|
||||
max-width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.items-list-hor .item-content {
|
||||
padding-left: calc(218px - 4px);
|
||||
}
|
||||
|
||||
.items-list-hor .item-thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(218px - 4px);
|
||||
height: calc(0.5611 * calc(218px - 4px));
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.items-list-hor .item-main {
|
||||
min-height: calc(0.5611 * calc(218px - 4px));
|
||||
padding-left: var(--horizontal-item-margin-right-width, var(--default-horizontal-item-margin-right-width));
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1em;
|
||||
max-height: initial;
|
||||
|
||||
span {
|
||||
line-height: var(--horizontal-item-title-line-height);
|
||||
max-height: calc(var(--horizontal-item-title-max-lines) * var(--default-horizontal-item-title-line-height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.items-list-hor .item-author {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.items-list-hor .item-views {
|
||||
&:before {
|
||||
content: '•';
|
||||
content: '\2022';
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.items-list-hor .item-description {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 390px) and (max-width: 599px) {
|
||||
.items-list-hor {
|
||||
.items-list {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
}
|
||||
|
||||
.item-content {
|
||||
padding-left: 168px;
|
||||
}
|
||||
|
||||
.item-thumb,
|
||||
a.item-thumb {
|
||||
width: 168px;
|
||||
height: calc(0.5611 * 168px);
|
||||
}
|
||||
|
||||
.item-main {
|
||||
min-height: calc(0.5611 * 168px);
|
||||
}
|
||||
|
||||
.item-main h3 {
|
||||
line-height: 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.item-author {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-views {
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item-meta > .item-views + .item-date:before {
|
||||
content: '•';
|
||||
content: '\2022';
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.items-list-hor .item-date:before {
|
||||
content: '•';
|
||||
content: '\2022';
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.items-list-hor .item {
|
||||
margin-bottom: var(--horizontal-item-margin-bottom-width, var(--default-horizontal-item-margin-bottom-width));
|
||||
}
|
||||
}
|
||||
202
frontend/src/static/js/components/list-item/ItemVertical.scss
Executable file
202
frontend/src/static/js/components/list-item/ItemVertical.scss
Executable file
@@ -0,0 +1,202 @@
|
||||
@import '../../../css/config/index.scss';
|
||||
|
||||
.items-list-ver .feat-first-item {
|
||||
.items-list-wrap,
|
||||
.items-list {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.no-title {
|
||||
margin-top: var(--default-item-margin-bottom-width);
|
||||
}
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:first-child .item-player-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding-bottom: 56.11%;
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:first-child .item-player-wrapper-inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
background-color: var(--item-thumb-bg-color);
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
$item-width: 260px;
|
||||
$side-empty-space: 40px;
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 2 * $item-width ) )) {
|
||||
.items-list-ver.media-list-wrapper .media-list-row .item {
|
||||
display: inline-block;
|
||||
max-width: var(--item-width, var(--default-item-width));
|
||||
}
|
||||
|
||||
.items-list-ver.media-list-wrapper .media-list-row .item-content {
|
||||
margin-right: var(--item-margin-right-width, var(--default-item-margin-right-width));
|
||||
}
|
||||
|
||||
.items-list-ver.media-list-wrapper .media-list-row .item-main {
|
||||
h3 {
|
||||
margin: 0.5714285em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 2 * $item-width ) )) {
|
||||
.items-list-ver .feat-first-item .item:first-child {
|
||||
float: left;
|
||||
max-width: calc(3 * var(--item-width, var(--default-item-width)));
|
||||
}
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
$item-width: 218px;
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 4 * $item-width ) )) {
|
||||
.items-list-ver .feat-first-item .item:nth-child(4n + 4) {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:nth-child(2) {
|
||||
min-height: 232px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 5 * $item-width ) )) {
|
||||
.items-list-ver .feat-first-item .item:nth-child(4n + 4) {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:nth-child(5n + 6) {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:nth-child(2) {
|
||||
min-height: 0;
|
||||
margin-bottom: var(--default-item-margin-bottom-width);
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:nth-child(3) {
|
||||
min-height: 232px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 6 * $item-width ) )) {
|
||||
.items-list-ver .feat-first-item .item:nth-child(5n + 6) {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:nth-child(6n + 8) {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:nth-child(3) {
|
||||
min-height: 0;
|
||||
margin-bottom: var(--default-item-margin-bottom-width);
|
||||
}
|
||||
|
||||
.items-list-ver .feat-first-item .item:nth-child(4) {
|
||||
min-height: 232px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* #################################################################################################### */
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 4 * $item-width ) )) {
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(4n + 4),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(4n + 4) {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(2),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(2) {
|
||||
min-height: 0;
|
||||
margin-bottom: var(--default-item-margin-bottom-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 5 * $item-width ) )) {
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(4n + 4),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(4n + 4) {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(3),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(3) {
|
||||
min-height: 0;
|
||||
margin-bottom: var(--default-item-margin-bottom-width);
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(2),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(2) {
|
||||
min-height: 232px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 6 * $item-width ) )) {
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(4n + 4),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(4n + 4) {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(5n + 6),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(5n + 6) {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(2),
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(4),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(2),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(4) {
|
||||
min-height: 0;
|
||||
margin-bottom: var(--default-item-margin-bottom-width);
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(3),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(3) {
|
||||
min-height: 232px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ( ( 2 * $side-empty-space ) + ( 7 * $item-width ) )) {
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(5n + 6),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(5n + 6) {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(6n + 8),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(6n + 8) {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(3),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(3) {
|
||||
min-height: 0;
|
||||
margin-bottom: var(--default-item-margin-bottom-width);
|
||||
}
|
||||
|
||||
.sliding-sidebar .items-list-ver .feat-first-item .item:nth-child(4),
|
||||
.visible-sidebar .items-list-ver .feat-first-item .item:nth-child(4) {
|
||||
min-height: 232px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
}
|
||||
344
frontend/src/static/js/components/list-item/ListItem.jsx
Normal file
344
frontend/src/static/js/components/list-item/ListItem.jsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import React from 'react';
|
||||
import { LinksContext } from '../../utils/contexts/';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { MediaItemAudio as AudioItem } from './MediaItemAudio';
|
||||
import { MediaItemVideo as VideoItem } from './MediaItemVideo';
|
||||
import { MediaItem as ImageItem } from './MediaItem';
|
||||
import { MediaItem as PdfItem } from './MediaItem';
|
||||
import { MediaItem as AttachmentItem } from './MediaItem';
|
||||
import { PlaylistItem } from './PlaylistItem';
|
||||
import { TaxonomyItem } from './TaxonomyItem';
|
||||
import { UserItem } from './UserItem';
|
||||
|
||||
function extractPlaylistId() {
|
||||
let playlistId = null;
|
||||
|
||||
const getParamsString = window.location.search;
|
||||
|
||||
if ('' !== getParamsString) {
|
||||
let tmp = getParamsString.split('?');
|
||||
|
||||
if (2 === tmp.length) {
|
||||
tmp = tmp[1].split('&');
|
||||
|
||||
let x;
|
||||
|
||||
let i = 0;
|
||||
while (i < tmp.length) {
|
||||
x = tmp[i].split('=');
|
||||
|
||||
if ('pl' === x[0]) {
|
||||
if (2 === x.length) {
|
||||
playlistId = x[1];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return playlistId;
|
||||
}
|
||||
|
||||
function itemPageLink(props, item) {
|
||||
if (props.inCategoriesList) {
|
||||
return LinksContext._currentValue.search.category + item.title.replace(' ', '%20');
|
||||
}
|
||||
|
||||
if (props.inTagsList) {
|
||||
return LinksContext._currentValue.search.tag + item.title.replace(' ', '%20');
|
||||
}
|
||||
|
||||
const playlistId = extractPlaylistId();
|
||||
|
||||
if (props.inPlaylistView && playlistId) {
|
||||
return item.url + '&pl=' + playlistId;
|
||||
}
|
||||
|
||||
if (void 0 !== props.playlistId && null !== props.playlistId) {
|
||||
return item.url + '&pl=' + props.playlistId;
|
||||
}
|
||||
|
||||
return item.url;
|
||||
}
|
||||
|
||||
export function listItemProps(props, item, index) {
|
||||
const isArchiveItem = props.inCategoriesList || props.inTagsList;
|
||||
const isUserItem = !isArchiveItem && void 0 !== item.username;
|
||||
const isPlaylistItem =
|
||||
!isArchiveItem &&
|
||||
!isUserItem &&
|
||||
('playlist' === item.media_type || (void 0 !== item.url && -1 < item.url.indexOf('playlists'))); // TODO: Improve this.
|
||||
const isMediaItem = !isArchiveItem && !isUserItem && !isPlaylistItem;
|
||||
const isSearchItem = 'search-results' === PageStore.get('current-page'); // TODO: Improve this.
|
||||
|
||||
const url = {
|
||||
view: itemPageLink(props, item),
|
||||
edit: props.canEdit ? item.url.replace('view?m=', 'edit?m=') : null,
|
||||
};
|
||||
|
||||
if (window.MediaCMS.site.devEnv && -1 < url.view.indexOf('view?')) {
|
||||
url.view = '/media.html?' + url.view.split('view?')[1];
|
||||
}
|
||||
|
||||
const thumbnail = item.thumbnail_url || '';
|
||||
const previewThumbnail = item.preview_url || '';
|
||||
|
||||
let type, title, date, description, meta_description;
|
||||
|
||||
title =
|
||||
void 0 !== item.username && 'string' === typeof item.username
|
||||
? item.username
|
||||
: void 0 !== item.title && 'string' === typeof item.title
|
||||
? item.title
|
||||
: null;
|
||||
|
||||
date =
|
||||
void 0 !== item.date_added && 'string' === typeof item.date_added
|
||||
? item.date_added
|
||||
: void 0 !== item.add_date && 'string' === typeof item.add_date
|
||||
? item.add_date
|
||||
: null;
|
||||
|
||||
// description = props.preferSummary && 'string' === typeof props.summary ? props.summary.trim() : ( 'string' === typeof item.description ? item.description.trim() : null );
|
||||
// description = null === description ? description : description.replace(/(<([^>]+)>)/ig,"");
|
||||
|
||||
if (isUserItem) {
|
||||
type = 'user';
|
||||
} else if (isPlaylistItem) {
|
||||
type = 'playlist';
|
||||
} else if (isMediaItem) {
|
||||
type = item.media_type;
|
||||
}
|
||||
|
||||
const taxonomyPage = {
|
||||
current: false,
|
||||
type: null,
|
||||
};
|
||||
|
||||
const playlistPage = {
|
||||
current: props.inPlaylistPage,
|
||||
id: props.playlistId,
|
||||
hideOptions: props.hidePlaylistOptions || false,
|
||||
hideOrderNumber: props.hidePlaylistOrderNumber || false,
|
||||
};
|
||||
|
||||
const playlistPlayback = {
|
||||
current: props.inPlaylistView,
|
||||
id: props.playlistId,
|
||||
activeItem: props.playlistActiveItem || false,
|
||||
hideOrderNumber: props.hidePlaylistOrderNumber || false,
|
||||
};
|
||||
|
||||
if (isArchiveItem) {
|
||||
if (props.inCategoriesList) {
|
||||
taxonomyPage.type = 'categories';
|
||||
} else if (props.inTagsList) {
|
||||
taxonomyPage.type = 'tags';
|
||||
}
|
||||
|
||||
if (null !== taxonomyPage.type) {
|
||||
taxonomyPage.current = true;
|
||||
}
|
||||
}
|
||||
|
||||
const author = {
|
||||
name: item.author_name || item.user,
|
||||
url: item.author_profile ? item.author_profile.replace(' ', '%20') : null,
|
||||
};
|
||||
|
||||
const stats = {
|
||||
views: item.views || null,
|
||||
};
|
||||
|
||||
const hide = {
|
||||
allMeta: props.hideAllMeta || false,
|
||||
};
|
||||
|
||||
let args = {
|
||||
order: index + 1,
|
||||
type,
|
||||
title,
|
||||
date,
|
||||
url,
|
||||
author,
|
||||
stats,
|
||||
thumbnail,
|
||||
taxonomyPage,
|
||||
playlistPage,
|
||||
playlistPlayback,
|
||||
canEdit: null !== url.edit,
|
||||
singleLinkContent: props.singleLinkContent || false,
|
||||
hasMediaViewer: 0 === index && 'video' === item.media_type && !!props.firstItemViewer,
|
||||
hasMediaViewerDescr: false,
|
||||
};
|
||||
|
||||
args.hasMediaViewerDescr = args.hasMediaViewer && !!props.firstItemDescr;
|
||||
|
||||
if (!args.hasMediaViewerDescr) {
|
||||
description =
|
||||
props.preferSummary && 'string' === typeof props.summary
|
||||
? props.summary.trim()
|
||||
: 'string' === typeof item.description
|
||||
? item.description.trim()
|
||||
: null;
|
||||
description = null === description ? description : description.replace(/(<([^>]+)>)/gi, '');
|
||||
|
||||
if (isSearchItem || props.inCategoriesList || 'user' === type) {
|
||||
args.description = description;
|
||||
} else {
|
||||
args.meta_description = description;
|
||||
}
|
||||
} else {
|
||||
if (!!props.firstItemViewer) {
|
||||
description = 'string' === typeof props.summary ? props.summary.trim() : null;
|
||||
} else {
|
||||
description = 'string' === typeof item.description ? item.description.trim() : null;
|
||||
}
|
||||
|
||||
description = null === description ? description : description.replace(/(<([^>]+)>)/gi, '');
|
||||
|
||||
args.description = description;
|
||||
|
||||
// TODO: Improve this.
|
||||
if (props.summary) {
|
||||
meta_description = props.summary.trim();
|
||||
meta_description = null === meta_description ? meta_description : meta_description.replace(/(<([^>]+)>)/gi, '');
|
||||
args.meta_description = meta_description;
|
||||
}
|
||||
}
|
||||
|
||||
if ('video' === type) {
|
||||
args.previewThumbnail = previewThumbnail;
|
||||
}
|
||||
|
||||
if ('video' === type || 'audio' === type) {
|
||||
args.duration = item.duration;
|
||||
}
|
||||
|
||||
if ((isArchiveItem || isPlaylistItem) && !isNaN(item.media_count)) {
|
||||
args.media_count = parseInt(item.media_count, 10);
|
||||
}
|
||||
|
||||
if (isMediaItem) {
|
||||
hide.date = props.hideDate || false;
|
||||
hide.views = props.hideViews || false;
|
||||
hide.author = props.hideAuthor || false;
|
||||
}
|
||||
|
||||
args = { ...args, hide };
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export function ListItem(props) {
|
||||
let isMediaItem = false;
|
||||
|
||||
const args = {
|
||||
order: props.order,
|
||||
title: props.title,
|
||||
link: props.url.view,
|
||||
thumbnail: props.thumbnail,
|
||||
publish_date: props.date,
|
||||
singleLinkContent: props.singleLinkContent,
|
||||
hasMediaViewer: props.hasMediaViewer,
|
||||
hasMediaViewerDescr: props.hasMediaViewerDescr,
|
||||
};
|
||||
|
||||
switch (props.type) {
|
||||
case 'user':
|
||||
break;
|
||||
case 'playlist':
|
||||
break;
|
||||
case 'video':
|
||||
isMediaItem = true;
|
||||
args.duration = props.duration;
|
||||
args.preview_thumbnail = props.previewThumbnail;
|
||||
break;
|
||||
case 'audio':
|
||||
isMediaItem = true;
|
||||
args.duration = props.duration;
|
||||
break;
|
||||
case 'image':
|
||||
isMediaItem = true;
|
||||
break;
|
||||
case 'pdf':
|
||||
isMediaItem = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (void 0 !== props.description) {
|
||||
args.description = props.description;
|
||||
}
|
||||
|
||||
if (void 0 !== props.meta_description) {
|
||||
args.meta_description = props.meta_description;
|
||||
}
|
||||
|
||||
if ((props.taxonomyPage.current || 'playlist' === props.type) && !isNaN(props.media_count)) {
|
||||
args.media_count = props.media_count;
|
||||
}
|
||||
|
||||
args.hideAllMeta = props.hide.allMeta;
|
||||
|
||||
if (isMediaItem) {
|
||||
args.views = props.stats.views;
|
||||
|
||||
args.author_name = props.author.name;
|
||||
args.author_link = props.author.url;
|
||||
|
||||
args.hideDate = props.hide.date;
|
||||
args.hideViews = props.hide.views;
|
||||
args.hideAuthor = props.hide.author;
|
||||
}
|
||||
|
||||
if (props.playlistPage.current || props.playlistPlayback.current) {
|
||||
args.playlistOrder = props.order;
|
||||
|
||||
if (props.playlistPlayback.current) {
|
||||
args.playlist_id = props.playlistPlayback.id;
|
||||
args.playlistActiveItem = props.playlistPlayback.activeItem;
|
||||
args.hidePlaylistOrderNumber = props.playlistPlayback.hideOrderNumber;
|
||||
} else {
|
||||
args.playlist_id = props.playlistPage.id;
|
||||
args.hidePlaylistOptions = props.playlistPage.hideOptions;
|
||||
args.hidePlaylistOrderNumber = props.playlistPage.hideOrderNumber;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.canEdit) {
|
||||
args.editLink = props.url.edit;
|
||||
}
|
||||
|
||||
if (props.taxonomyPage.current) {
|
||||
switch (props.taxonomyPage.type) {
|
||||
case 'categories':
|
||||
return <TaxonomyItem {...args} type="category" />;
|
||||
case 'tags':
|
||||
return <TaxonomyItem {...args} type="tag" />;
|
||||
}
|
||||
}
|
||||
|
||||
switch (props.type) {
|
||||
case 'user':
|
||||
return <UserItem {...args} />;
|
||||
case 'playlist':
|
||||
if (window.MediaCMS.site.devEnv) {
|
||||
args.link = args.link.replace('/playlists/', 'playlist.html?pl=');
|
||||
}
|
||||
return <PlaylistItem {...args} />;
|
||||
case 'video':
|
||||
return <VideoItem {...args} />;
|
||||
case 'audio':
|
||||
return <AudioItem {...args} />;
|
||||
case 'image':
|
||||
return <ImageItem {...args} type="image" />;
|
||||
case 'pdf':
|
||||
return <PdfItem {...args} type="pdf" />;
|
||||
}
|
||||
|
||||
return <AttachmentItem {...args} type="attachment" />;
|
||||
}
|
||||
70
frontend/src/static/js/components/list-item/MediaItem.jsx
Normal file
70
frontend/src/static/js/components/list-item/MediaItem.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveInteger, PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { MediaItemThumbnailLink, itemClassname } from './includes/items/';
|
||||
import { Item } from './Item';
|
||||
|
||||
export function MediaItem(props) {
|
||||
const type = props.type;
|
||||
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
function thumbnailComponent() {
|
||||
return <MediaItemThumbnailLink src={thumbnailUrl} title={props.title} link={props.link} />;
|
||||
}
|
||||
|
||||
const containerClassname = itemClassname(
|
||||
'item ' + type + '-item',
|
||||
props.class_name.trim(),
|
||||
props.playlistOrder === props.playlistActiveItem
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClassname}>
|
||||
<div className="item-content">
|
||||
{editMediaComponent()}
|
||||
|
||||
{thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MediaItem.propTypes = {
|
||||
...Item.propTypes,
|
||||
type: PropTypes.string.isRequired,
|
||||
class_name: PropTypes.string,
|
||||
views: PositiveIntegerOrZero,
|
||||
hideViews: PropTypes.bool,
|
||||
hideDate: PropTypes.bool,
|
||||
hideAuthor: PropTypes.bool,
|
||||
author_name: PropTypes.string,
|
||||
author_link: PropTypes.string,
|
||||
playlistOrder: PositiveInteger,
|
||||
playlistActiveItem: PositiveIntegerOrZero,
|
||||
inPlaylistView: PropTypes.bool,
|
||||
hidePlaylistOrderNumber: PropTypes.bool,
|
||||
};
|
||||
|
||||
MediaItem.defaultProps = {
|
||||
...Item.defaultProps,
|
||||
class_name: '',
|
||||
views: 0,
|
||||
hideViews: false,
|
||||
hideDate: false,
|
||||
hideAuthor: false,
|
||||
author_name: '',
|
||||
author_link: '#',
|
||||
playlistOrder: 1,
|
||||
playlistActiveItem: 1,
|
||||
inPlaylistView: false,
|
||||
hidePlaylistOrderNumber: true,
|
||||
};
|
||||
106
frontend/src/static/js/components/list-item/MediaItemAudio.jsx
Normal file
106
frontend/src/static/js/components/list-item/MediaItemAudio.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { MediaDurationInfo } from '../../utils/classes/';
|
||||
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions';
|
||||
import { MediaItemDuration, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
|
||||
import { MediaItem } from './MediaItem';
|
||||
|
||||
export function MediaItemAudio(props) {
|
||||
const type = props.type;
|
||||
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
const _MediaDurationInfo = new MediaDurationInfo();
|
||||
|
||||
_MediaDurationInfo.update(props.duration);
|
||||
|
||||
const duration = _MediaDurationInfo.ariaLabel();
|
||||
const durationStr = _MediaDurationInfo.toString();
|
||||
const durationISO8601 = _MediaDurationInfo.ISO8601();
|
||||
|
||||
function thumbnailComponent() {
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
title: props.title,
|
||||
tabIndex: '-1',
|
||||
'aria-hidden': true,
|
||||
className: 'item-thumb' + (!thumbnailUrl ? ' no-thumb' : ''),
|
||||
style: !thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" },
|
||||
};
|
||||
|
||||
return (
|
||||
<a {...attr}>
|
||||
{props.inPlaylistView ? null : (
|
||||
<MediaItemDuration ariaLabel={duration} time={durationISO8601} text={durationStr} />
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function playlistOrderNumberComponent() {
|
||||
return props.hidePlaylistOrderNumber ? null : (
|
||||
<MediaItemPlaylistIndex
|
||||
index={props.playlistOrder}
|
||||
inPlayback={props.inPlaylistView}
|
||||
activeIndex={props.playlistActiveItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function playlistOptionsComponent() {
|
||||
let mediaId = props.link.split('=')[1];
|
||||
mediaId = mediaId.split('&')[0];
|
||||
return props.hidePlaylistOptions ? null : (
|
||||
<MediaPlaylistOptions key="options" media_id={mediaId} playlist_id={props.playlist_id} />
|
||||
);
|
||||
}
|
||||
|
||||
const containerClassname = itemClassname(
|
||||
'item ' + type + '-item',
|
||||
props.class_name.trim(),
|
||||
props.playlistOrder === props.playlistActiveItem
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClassname}>
|
||||
{playlistOrderNumberComponent()}
|
||||
|
||||
<div className="item-content">
|
||||
{editMediaComponent()}
|
||||
|
||||
{thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
|
||||
{playlistOptionsComponent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MediaItemAudio.propTypes = {
|
||||
...MediaItem.propTypes,
|
||||
type: PropTypes.string.isRequired,
|
||||
duration: PositiveIntegerOrZero,
|
||||
hidePlaylistOptions: PropTypes.bool,
|
||||
hasMediaViewer: PropTypes.bool,
|
||||
hasMediaViewerDescr: PropTypes.bool,
|
||||
playlist_id: PropTypes.string,
|
||||
};
|
||||
|
||||
MediaItemAudio.defaultProps = {
|
||||
...MediaItem.defaultProps,
|
||||
type: 'audio',
|
||||
duration: 0,
|
||||
hidePlaylistOptions: true,
|
||||
hasMediaViewer: false,
|
||||
hasMediaViewerDescr: false,
|
||||
};
|
||||
113
frontend/src/static/js/components/list-item/MediaItemVideo.jsx
Normal file
113
frontend/src/static/js/components/list-item/MediaItemVideo.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { MediaDurationInfo } from '../../utils/classes/';
|
||||
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions.jsx';
|
||||
import { MediaItemVideoPlayer, MediaItemDuration, MediaItemVideoPreviewer, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
|
||||
import { MediaItem } from './MediaItem';
|
||||
|
||||
export function MediaItemVideo(props) {
|
||||
const type = props.type;
|
||||
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
const _MediaDurationInfo = new MediaDurationInfo();
|
||||
|
||||
_MediaDurationInfo.update(props.duration);
|
||||
|
||||
const duration = _MediaDurationInfo.ariaLabel();
|
||||
const durationStr = _MediaDurationInfo.toString();
|
||||
const durationISO8601 = _MediaDurationInfo.ISO8601();
|
||||
|
||||
function videoViewerComponent() {
|
||||
return <MediaItemVideoPlayer mediaPageLink={props.link} />;
|
||||
}
|
||||
|
||||
function thumbnailComponent() {
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
title: props.title,
|
||||
tabIndex: '-1',
|
||||
'aria-hidden': true,
|
||||
className: 'item-thumb' + (!thumbnailUrl ? ' no-thumb' : ''),
|
||||
style: !thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" },
|
||||
};
|
||||
|
||||
return (
|
||||
<a {...attr}>
|
||||
{props.inPlaylistView ? null : (
|
||||
<MediaItemDuration ariaLabel={duration} time={durationISO8601} text={durationStr} />
|
||||
)}
|
||||
{props.inPlaylistView || props.inPlaylistPage ? null : (
|
||||
<MediaItemVideoPreviewer url={props.preview_thumbnail} />
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function playlistOrderNumberComponent() {
|
||||
return props.hidePlaylistOrderNumber ? null : (
|
||||
<MediaItemPlaylistIndex
|
||||
index={props.playlistOrder}
|
||||
inPlayback={props.inPlaylistView}
|
||||
activeIndex={props.playlistActiveItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function playlistOptionsComponent() {
|
||||
let mediaId = props.link.split('=')[1];
|
||||
mediaId = mediaId.split('&')[0];
|
||||
return props.hidePlaylistOptions ? null : (
|
||||
<MediaPlaylistOptions key="options" media_id={mediaId} playlist_id={props.playlist_id} />
|
||||
);
|
||||
}
|
||||
|
||||
const containerClassname = itemClassname(
|
||||
'item ' + type + '-item',
|
||||
props.class_name.trim(),
|
||||
props.playlistOrder === props.playlistActiveItem
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClassname}>
|
||||
{playlistOrderNumberComponent()}
|
||||
|
||||
<div className="item-content">
|
||||
{editMediaComponent()}
|
||||
|
||||
{props.hasMediaViewer ? videoViewerComponent() : thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
|
||||
{playlistOptionsComponent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MediaItemVideo.propTypes = {
|
||||
...MediaItem.propTypes,
|
||||
type: PropTypes.string.isRequired,
|
||||
duration: PositiveIntegerOrZero,
|
||||
hidePlaylistOptions: PropTypes.bool,
|
||||
hasMediaViewer: PropTypes.bool,
|
||||
hasMediaViewerDescr: PropTypes.bool,
|
||||
playlist_id: PropTypes.string,
|
||||
};
|
||||
|
||||
MediaItemVideo.defaultProps = {
|
||||
...MediaItem.defaultProps,
|
||||
type: 'video',
|
||||
duration: 0,
|
||||
hidePlaylistOptions: true,
|
||||
hasMediaViewer: false,
|
||||
hasMediaViewerDescr: false,
|
||||
};
|
||||
73
frontend/src/static/js/components/list-item/PlaylistItem.jsx
Executable file
73
frontend/src/static/js/components/list-item/PlaylistItem.jsx
Executable file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { format } from 'timeago.js';
|
||||
import { useItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { PlaylistItemMetaDate } from './includes/items/';
|
||||
import { Item } from './Item';
|
||||
|
||||
export function PlaylistItem(props) {
|
||||
const type = 'playlist';
|
||||
|
||||
const { titleComponent, thumbnailUrl, UnderThumbWrapper } = useItem({ ...props, type });
|
||||
|
||||
function metaComponents() {
|
||||
const publishDate = format(new Date(props.publish_date));
|
||||
const publishDateTime =
|
||||
'string' === typeof props.publish_date
|
||||
? Date.parse(props.publish_date)
|
||||
: Date.parse(new Date(props.publish_date));
|
||||
|
||||
return <PlaylistItemMetaDate dateTime={publishDateTime} text={'Created ' + publishDate} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="item playlist-item">
|
||||
<div className="item-content">
|
||||
<a
|
||||
className={'item-thumb' + (!thumbnailUrl ? ' no-thumb' : '')}
|
||||
href={props.link}
|
||||
title={props.title}
|
||||
tabIndex="-1"
|
||||
aria-hidden="true"
|
||||
style={!thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" }}
|
||||
>
|
||||
<div className="playlist-count">
|
||||
<div>
|
||||
<div>
|
||||
<span>{props.media_count}</span>
|
||||
<i className="material-icons">playlist_play</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="playlist-hover-play-all">
|
||||
<div>
|
||||
<div>
|
||||
<i className="material-icons">play_arrow</i>
|
||||
<span>PLAY ALL</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
<a href={props.link} title="" className="view-full-playlist">
|
||||
VIEW FULL PLAYLIST
|
||||
</a>
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PlaylistItem.propTypes = {
|
||||
...Item.propTypes,
|
||||
media_count: PositiveIntegerOrZero,
|
||||
};
|
||||
|
||||
PlaylistItem.defaultProps = {
|
||||
...Item.defaultProps,
|
||||
media_count: 0,
|
||||
};
|
||||
60
frontend/src/static/js/components/list-item/TaxonomyItem.jsx
Normal file
60
frontend/src/static/js/components/list-item/TaxonomyItem.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { TaxonomyItemMediaCount, itemClassname } from './includes/items/';
|
||||
import { Item } from './Item';
|
||||
|
||||
export function TaxonomyItem(props) {
|
||||
const type = props.type;
|
||||
|
||||
const { titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper } = useItem({ ...props, type });
|
||||
|
||||
function thumbnailComponent() {
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
title: props.title,
|
||||
tabIndex: '-1',
|
||||
'aria-hidden': true,
|
||||
className: 'item-thumb' + (!thumbnailUrl ? ' no-thumb' : ''),
|
||||
style: !thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" },
|
||||
};
|
||||
return <a {...attr}></a>;
|
||||
}
|
||||
|
||||
function metaComponents() {
|
||||
return props.hideAllMeta ? null : (
|
||||
<span className="item-meta">{<TaxonomyItemMediaCount count={props.media_count} />}</span>
|
||||
);
|
||||
}
|
||||
|
||||
const containerClassname = itemClassname('item ' + type + '-item', props.class_name.trim(), false);
|
||||
|
||||
return (
|
||||
<div className={containerClassname}>
|
||||
<div className="item-content">
|
||||
{thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TaxonomyItem.propTypes = {
|
||||
...Item.propTypes,
|
||||
type: PropTypes.string.isRequired,
|
||||
class_name: PropTypes.string,
|
||||
media_count: PositiveIntegerOrZero,
|
||||
};
|
||||
|
||||
TaxonomyItem.defaultProps = {
|
||||
...Item.defaultProps,
|
||||
class_name: '',
|
||||
media_count: 0,
|
||||
};
|
||||
44
frontend/src/static/js/components/list-item/UserItem.jsx
Executable file
44
frontend/src/static/js/components/list-item/UserItem.jsx
Executable file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { useItem } from '../../utils/hooks/';
|
||||
import { UserItemMemberSince, UserItemThumbnailLink } from './includes/items/';
|
||||
import { Item } from './Item';
|
||||
|
||||
export function UserItem(props) {
|
||||
const type = 'user';
|
||||
|
||||
const { titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper } = useItem({ ...props, type });
|
||||
|
||||
function metaComponents() {
|
||||
return props.hideAllMeta ? null : (
|
||||
<span className="item-meta">
|
||||
<UserItemMemberSince date={props.publish_date} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function thumbnailComponent() {
|
||||
return <UserItemThumbnailLink src={thumbnailUrl} title={props.title} link={props.link} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="item member-item">
|
||||
<div className="item-content">
|
||||
{thumbnailComponent()}
|
||||
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserItem.propTypes = {
|
||||
...Item.propTypes,
|
||||
};
|
||||
|
||||
UserItem.defaultProps = {
|
||||
...Item.defaultProps,
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
import React from 'react';
|
||||
import { format } from 'timeago.js';
|
||||
import { formatViewsNumber, imageExtension } from '../../../../utils/helpers/';
|
||||
import { VideoPlayerByPageLink } from '../../../video-player/VideoPlayerByPageLink';
|
||||
|
||||
export function ItemDescription(props) {
|
||||
return '' === props.description ? null : (
|
||||
<div className="item-description">
|
||||
<div>{props.description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ItemMain(props) {
|
||||
return <div className="item-main">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function ItemMainInLink(props) {
|
||||
return (
|
||||
<ItemMain>
|
||||
<a className="item-content-link" href={props.link} title={props.title}>
|
||||
{props.children}
|
||||
</a>
|
||||
</ItemMain>
|
||||
);
|
||||
}
|
||||
|
||||
export function ItemTitle(props) {
|
||||
return '' === props.title ? null : (
|
||||
<h3>
|
||||
<span aria-label={props.ariaLabel}>{props.title}</span>
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
export function ItemTitleLink(props) {
|
||||
return '' === props.title ? null : (
|
||||
<h3>
|
||||
<a href={props.link} title={props.title}>
|
||||
<span aria-label={props.ariaLabel}>{props.title}</span>
|
||||
</a>
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserItemMemberSince(props) {
|
||||
return <time key="member-since">Member for {format(new Date(props.date)).replace(' ago', '')}</time>;
|
||||
}
|
||||
|
||||
export function TaxonomyItemMediaCount(props) {
|
||||
return (
|
||||
<span key="item-media-count" className="item-media-count">
|
||||
{' ' + props.count} media
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlaylistItemMetaDate(props) {
|
||||
return (
|
||||
<span className="item-meta">
|
||||
<span className="playlist-date">
|
||||
<time dateTime={props.dateTime}>{props.text}</time>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemEditLink(props) {
|
||||
let link = props.link;
|
||||
|
||||
if (link && window.MediaCMS.site.devEnv) {
|
||||
link = '/edit-media.html';
|
||||
}
|
||||
|
||||
return !link ? null : (
|
||||
<a href={link} title="Edit media" className="item-edit-link">
|
||||
EDIT MEDIA
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemThumbnailLink(props) {
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
title: props.title,
|
||||
tabIndex: '-1',
|
||||
'aria-hidden': true,
|
||||
className: 'item-thumb' + (!props.src ? ' no-thumb' : ''),
|
||||
style: !props.src ? null : { backgroundImage: "url('" + props.src + "')" },
|
||||
};
|
||||
|
||||
return (
|
||||
<a {...attr}>
|
||||
{!props.src ? null : (
|
||||
<div key="item-type-icon" className="item-type-icon">
|
||||
<div></div>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserItemThumbnailLink(props) {
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
title: props.title,
|
||||
tabIndex: '-1',
|
||||
'aria-hidden': true,
|
||||
className: 'item-thumb' + (!props.src ? ' no-thumb' : ''),
|
||||
style: !props.src ? null : { backgroundImage: "url('" + props.src + "')" },
|
||||
};
|
||||
|
||||
return <a {...attr}></a>;
|
||||
}
|
||||
|
||||
export function MediaItemAuthor(props) {
|
||||
return '' === props.name ? null : (
|
||||
<span className="item-author">
|
||||
<span>{props.name}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemAuthorLink(props) {
|
||||
return '' === props.name ? null : (
|
||||
<span className="item-author">
|
||||
<a href={props.link} title={props.name}>
|
||||
<span>{props.name}</span>
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemMetaViews(props) {
|
||||
return (
|
||||
<span className="item-views">{formatViewsNumber(props.views) + ' ' + (1 >= props.views ? 'view' : 'views')}</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemMetaDate(props) {
|
||||
return (
|
||||
<span className="item-date">
|
||||
<time dateTime={props.dateTime} content={props.time}>
|
||||
{props.text}
|
||||
</time>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemDuration(props) {
|
||||
return (
|
||||
<span className="item-duration">
|
||||
<span aria-label={props.ariaLabel} content={props.time}>
|
||||
{props.text}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemVideoPreviewer(props) {
|
||||
if ('' === props.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const src = props.url.split('.').slice(0, -1).join('.');
|
||||
const ext = imageExtension(props.url);
|
||||
|
||||
return <span className="item-img-preview" data-src={src} data-ext={ext}></span>;
|
||||
}
|
||||
|
||||
export function MediaItemVideoPlayer(props) {
|
||||
return (
|
||||
<div className="item-player-wrapper">
|
||||
<div className="item-player-wrapper-inner">
|
||||
<VideoPlayerByPageLink pageLink={props.mediaPageLink} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaItemPlaylistIndex(props) {
|
||||
return (
|
||||
<div className="item-order-number">
|
||||
<div>
|
||||
<div data-order={props.index} data-id={props.media_id}>
|
||||
{props.inPlayback && props.index === props.activeIndex ? (
|
||||
<i className="material-icons">play_arrow</i>
|
||||
) : (
|
||||
props.index
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './includes';
|
||||
export * from './itemClassname';
|
||||
@@ -0,0 +1,13 @@
|
||||
export function itemClassname(defaultClassname, inheritedClassname, isActiveInPlaylistPlayback) {
|
||||
let classname = defaultClassname;
|
||||
|
||||
if ('' !== inheritedClassname) {
|
||||
classname += ' ' + inheritedClassname;
|
||||
}
|
||||
|
||||
if (isActiveInPlaylistPlayback) {
|
||||
classname += ' pl-active-item';
|
||||
}
|
||||
|
||||
return classname;
|
||||
}
|
||||
8
frontend/src/static/js/components/list-item/index.js
Normal file
8
frontend/src/static/js/components/list-item/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './Item.jsx';
|
||||
export * from './ListItem.jsx';
|
||||
export * from './MediaItem.jsx';
|
||||
export * from './MediaItemAudio.jsx';
|
||||
export * from './MediaItemVideo.jsx';
|
||||
export * from './PlaylistItem.jsx';
|
||||
export * from './TaxonomyItem.jsx';
|
||||
export * from './UserItem.jsx';
|
||||
@@ -0,0 +1,194 @@
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { usePopup } from '../../../utils/hooks/';
|
||||
import { PageStore } from '../../../utils/stores/';
|
||||
import { PopupMain } from '../../_shared';
|
||||
import { ManageItemDate } from './ManageMediaItem';
|
||||
|
||||
function ManageItemCommentAuthor(props) {
|
||||
if (void 0 !== props.name && void 0 !== props.url) {
|
||||
return (
|
||||
<a href={props.url} title={props.name}>
|
||||
{props.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (void 0 !== props.name) {
|
||||
return props.name;
|
||||
}
|
||||
|
||||
if (void 0 !== props.url) {
|
||||
return props.url;
|
||||
}
|
||||
|
||||
return <i className="non-available">N/A</i>;
|
||||
}
|
||||
|
||||
function ManageItemCommentActions(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
const [isOpenPopup, setIsOpenPopup] = useState(false);
|
||||
|
||||
function onPopupShow() {
|
||||
setIsOpenPopup(true);
|
||||
}
|
||||
|
||||
function onPopupHide() {
|
||||
setIsOpenPopup(false);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
popupContentRef.current.tryToHide();
|
||||
if ('function' === typeof props.onCancel) {
|
||||
props.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function onProceed() {
|
||||
popupContentRef.current.tryToHide();
|
||||
if ('function' === typeof props.onProceed) {
|
||||
props.onProceed();
|
||||
}
|
||||
}
|
||||
|
||||
const positionState = { updating: false, pending: 0 };
|
||||
|
||||
const onWindowResize = useCallback(function () {
|
||||
if (positionState.updating) {
|
||||
positionState.pending = positionState.pending + 1;
|
||||
} else {
|
||||
positionState.updating = true;
|
||||
|
||||
const popupElem = props.containerRef.current.querySelector('.popup');
|
||||
|
||||
if (popupElem) {
|
||||
const containerClientRect = props.containerRef.current.getBoundingClientRect();
|
||||
|
||||
popupElem.style.position = 'fixed';
|
||||
popupElem.style.left = containerClientRect.x + 'px';
|
||||
|
||||
if (document.body.offsetHeight < 32 + popupElem.offsetHeight + window.scrollY + containerClientRect.top) {
|
||||
popupElem.style.top = containerClientRect.y - popupElem.offsetHeight + 'px';
|
||||
} else {
|
||||
popupElem.style.top = containerClientRect.y + containerClientRect.height + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
positionState.updating = false;
|
||||
|
||||
if (positionState.pending) {
|
||||
positionState.pending = 0;
|
||||
onWindowResize();
|
||||
}
|
||||
}, 8);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenPopup) {
|
||||
PageStore.on('window_scroll', onWindowResize);
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
onWindowResize();
|
||||
} else {
|
||||
PageStore.removeListener('window_scroll', onWindowResize);
|
||||
PageStore.removeListener('window_resize', onWindowResize);
|
||||
}
|
||||
}, [isOpenPopup]);
|
||||
|
||||
return (
|
||||
<div ref={props.containerRef} className="actions">
|
||||
{void 0 === props.media_url ? null : (
|
||||
<span>
|
||||
<a href={props.media_url}>View media</a>
|
||||
</span>
|
||||
)}
|
||||
{void 0 === props.media_url || props.hideDeleteAction ? null : <span className="seperator">|</span>}
|
||||
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button title="Delete comment">Delete</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef} showCallback={onPopupShow} hideCallback={onPopupHide}>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Comment removal</span>
|
||||
<span className="popup-message-main">You're willing to remove comment?</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-profile-removal" onClick={onCancel}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-profile-removal" onClick={onProceed}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ManageCommentsItem(props) {
|
||||
const actionsContainerRef = useRef(null);
|
||||
|
||||
const [selected, setSelected] = useState(false);
|
||||
|
||||
function onRowCheck() {
|
||||
setSelected(!selected);
|
||||
}
|
||||
|
||||
function onClickProceed() {
|
||||
if ('function' === typeof props.onProceedRemoval) {
|
||||
props.onProceedRemoval(props.uid);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if ('function' === typeof props.onCheckRow) {
|
||||
props.onCheckRow(props.uid, selected);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(props.selectedRow);
|
||||
}, [props.selectedRow]);
|
||||
|
||||
return (
|
||||
<div className="item manage-item manage-comments-item">
|
||||
<div className="mi-checkbox">
|
||||
<input type="checkbox" checked={selected} onChange={onRowCheck} />
|
||||
</div>
|
||||
<div className="mi-author">
|
||||
<ManageItemCommentAuthor name={props.author_name} url={props.author_url} />
|
||||
</div>
|
||||
<div className="mi-comment">
|
||||
{void 0 === props.text ? <i className="non-available">N/A</i> : props.text}
|
||||
{void 0 === props.text || (void 0 === props.media_url && props.hideDeleteAction) ? null : (
|
||||
<ManageItemCommentActions
|
||||
containerRef={actionsContainerRef}
|
||||
title={props.title}
|
||||
onProceed={onClickProceed}
|
||||
media_url={props.media_url}
|
||||
hideDeleteAction={props.hideDeleteAction}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mi-added">
|
||||
<ManageItemDate date={props.add_date} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageCommentsItem.propTypes = {
|
||||
author_name: PropTypes.string,
|
||||
author_url: PropTypes.string,
|
||||
author_thumbnail_url: PropTypes.string,
|
||||
add_date: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
selectedRow: PropTypes.bool.isRequired,
|
||||
hideDeleteAction: PropTypes.bool.isRequired,
|
||||
uid: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useManagementTableHeader } from '../../../utils/hooks/';
|
||||
import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon';
|
||||
|
||||
export function ManageCommentsItemHeader(props) {
|
||||
const [sort, order, isSelected, sortByColumn, checkAll] = useManagementTableHeader({ ...props, type: 'comments' });
|
||||
|
||||
return (
|
||||
<div className="item manage-item manage-item-header manage-comments-item">
|
||||
<div className="mi-checkbox">
|
||||
<input type="checkbox" checked={isSelected} onChange={checkAll} />
|
||||
</div>
|
||||
<div className="mi-author">Author</div>
|
||||
<div
|
||||
id="text"
|
||||
onClick={sortByColumn}
|
||||
className={'mi-comment mi-col-sort' + ('text' === sort ? ('asc' === order ? ' asc' : ' desc') : '')}
|
||||
>
|
||||
Comment
|
||||
<div className="mi-col-sort-icons">
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_up" />
|
||||
</span>
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_down" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="add_date"
|
||||
onClick={sortByColumn}
|
||||
className={'mi-added mi-col-sort' + ('add_date' === sort ? ('asc' === order ? ' asc' : ' desc') : '')}
|
||||
>
|
||||
Date added
|
||||
<div className="mi-col-sort-icons">
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_up" />
|
||||
</span>
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_down" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageCommentsItemHeader.propTypes = {
|
||||
sort: PropTypes.string.isRequired,
|
||||
order: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
onClickColumnSort: PropTypes.func,
|
||||
onCheckAllRows: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,256 @@
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { usePopup } from '../../../utils/hooks/usePopup';
|
||||
import { formatManagementTableDate } from '../../../utils/helpers/';
|
||||
import { PageStore } from '../../../utils/stores/';
|
||||
import { PopupMain } from '../../_shared';
|
||||
import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon';
|
||||
|
||||
function ManageItemTitle(props) {
|
||||
if (void 0 !== props.title && void 0 !== props.url) {
|
||||
return (
|
||||
<a href={props.url} title={props.title}>
|
||||
{props.title}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (void 0 !== props.title) {
|
||||
return props.title;
|
||||
}
|
||||
|
||||
if (void 0 !== props.url) {
|
||||
return props.url;
|
||||
}
|
||||
|
||||
return <i className="non-available">N/A</i>;
|
||||
}
|
||||
|
||||
export function ManageItemDate(props) {
|
||||
if (void 0 !== props.date) {
|
||||
return formatManagementTableDate(new Date(Date.parse(props.date)));
|
||||
}
|
||||
|
||||
return <i className="non-available">N/A</i>;
|
||||
}
|
||||
|
||||
function ManageItemMediaAuthor(props) {
|
||||
if (void 0 !== props.name && void 0 !== props.url) {
|
||||
return (
|
||||
<a href={props.url} title={props.name}>
|
||||
{props.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (void 0 !== props.name) {
|
||||
return props.name;
|
||||
}
|
||||
|
||||
if (void 0 !== props.url) {
|
||||
return props.url;
|
||||
}
|
||||
|
||||
return <i className="non-available">N/A</i>;
|
||||
}
|
||||
|
||||
function ManageItemMediaActions(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
const [isOpenPopup, setIsOpenPopup] = useState(false);
|
||||
|
||||
function onPopupShow() {
|
||||
setIsOpenPopup(true);
|
||||
}
|
||||
|
||||
function onPopupHide() {
|
||||
setIsOpenPopup(false);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
popupContentRef.current.tryToHide();
|
||||
if ('function' === typeof props.onCancel) {
|
||||
props.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function onProceed() {
|
||||
popupContentRef.current.tryToHide();
|
||||
if ('function' === typeof props.onProceed) {
|
||||
props.onProceed();
|
||||
}
|
||||
}
|
||||
|
||||
const positionState = { updating: false, pending: 0 };
|
||||
|
||||
const onWindowResize = useCallback(function () {
|
||||
if (positionState.updating) {
|
||||
positionState.pending = positionState.pending + 1;
|
||||
} else {
|
||||
positionState.updating = true;
|
||||
|
||||
const popupElem = props.containerRef.current.querySelector('.popup');
|
||||
|
||||
if (popupElem) {
|
||||
const containerClientRect = props.containerRef.current.getBoundingClientRect();
|
||||
|
||||
popupElem.style.position = 'fixed';
|
||||
popupElem.style.left = containerClientRect.x + 'px';
|
||||
|
||||
if (document.body.offsetHeight < 32 + popupElem.offsetHeight + window.scrollY + containerClientRect.top) {
|
||||
popupElem.style.top = containerClientRect.y - popupElem.offsetHeight + 'px';
|
||||
} else {
|
||||
popupElem.style.top = containerClientRect.y + containerClientRect.height + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
positionState.updating = false;
|
||||
|
||||
if (positionState.pending) {
|
||||
positionState.pending = 0;
|
||||
onWindowResize();
|
||||
}
|
||||
}, 8);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenPopup) {
|
||||
PageStore.on('window_scroll', onWindowResize);
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
onWindowResize();
|
||||
} else {
|
||||
PageStore.removeListener('window_scroll', onWindowResize);
|
||||
PageStore.removeListener('window_resize', onWindowResize);
|
||||
}
|
||||
}, [isOpenPopup]);
|
||||
|
||||
return (
|
||||
<div ref={props.containerRef} className="actions">
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button title={'Delete' + (void 0 !== props.title ? ' "' + props.title + '"' : '')}>Delete</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef} showCallback={onPopupShow} hideCallback={onPopupHide}>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Media removal</span>
|
||||
<span className="popup-message-main">
|
||||
{"You're willing to remove media" + (void 0 !== props.title ? ' "' + props.title + '"' : '')}?
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-profile-removal" onClick={onCancel}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-profile-removal" onClick={onProceed}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ManageMediaItem(props) {
|
||||
const actionsContainerRef = useRef(null);
|
||||
|
||||
const [selected, setSelected] = useState(false);
|
||||
|
||||
function onRowCheck() {
|
||||
setSelected(!selected);
|
||||
}
|
||||
|
||||
function onClickProceed() {
|
||||
if ('function' === typeof props.onProceedRemoval) {
|
||||
props.onProceedRemoval(props.token);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if ('function' === typeof props.onCheckRow) {
|
||||
props.onCheckRow(props.token, selected);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(props.selectedRow);
|
||||
}, [props.selectedRow]);
|
||||
|
||||
return (
|
||||
<div className="item manage-item manage-media-item">
|
||||
<div className="mi-checkbox">
|
||||
<input type="checkbox" checked={selected} onChange={onRowCheck} />
|
||||
</div>
|
||||
<div className="mi-title">
|
||||
<ManageItemTitle title={props.title} url={props.url} />
|
||||
{props.hideDeleteAction ? null : (
|
||||
<ManageItemMediaActions containerRef={actionsContainerRef} title={props.title} onProceed={onClickProceed} />
|
||||
)}
|
||||
</div>
|
||||
<div className="mi-added">
|
||||
<ManageItemDate date={props.add_date} />
|
||||
</div>
|
||||
<div className="mi-author">
|
||||
<ManageItemMediaAuthor name={props.author_name} url={props.author_url} />
|
||||
</div>
|
||||
<div className="mi-type">
|
||||
{void 0 === props.media_type ? <i className="non-available">N/A</i> : props.media_type}
|
||||
</div>
|
||||
<div className="mi-encoding">
|
||||
{void 0 === props.encoding_status ? <i className="non-available">N/A</i> : props.encoding_status}
|
||||
</div>
|
||||
<div className="mi-state">{void 0 === props.state ? <i className="non-available">N/A</i> : props.state}</div>
|
||||
<div className="mi-reviewed">
|
||||
{void 0 === props.is_reviewed ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : props.is_reviewed ? (
|
||||
<MaterialIcon type="check_circle" />
|
||||
) : (
|
||||
<MaterialIcon type="cancel" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mi-featured">
|
||||
{void 0 === props.featured ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : props.featured ? (
|
||||
<MaterialIcon type="check_circle" />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
<div className="mi-reported">
|
||||
{void 0 === props.reported_times ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : 0 === props.reported_times ? (
|
||||
<span>-</span>
|
||||
) : (
|
||||
<span className="reported-number">
|
||||
{props.reported_times} {'time' + (1 < props.reported_times ? 's' : '')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageMediaItem.propTypes = {
|
||||
thumbnail_url: PropTypes.string,
|
||||
token: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
author_name: PropTypes.string,
|
||||
author_url: PropTypes.string,
|
||||
add_date: PropTypes.string,
|
||||
media_type: PropTypes.string,
|
||||
encoding_status: PropTypes.string,
|
||||
state: PropTypes.string,
|
||||
is_reviewed: PropTypes.bool,
|
||||
featured: PropTypes.bool,
|
||||
reported_times: PropTypes.number,
|
||||
onCheckRow: PropTypes.func,
|
||||
selectedRow: PropTypes.bool.isRequired,
|
||||
hideDeleteAction: PropTypes.bool.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useManagementTableHeader } from '../../../utils/hooks/';
|
||||
import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon.jsx';
|
||||
|
||||
export function ManageMediaItemHeader(props) {
|
||||
const [sort, order, isSelected, sortByColumn, checkAll] = useManagementTableHeader({ ...props, type: 'media' });
|
||||
|
||||
return (
|
||||
<div className="item manage-item manage-item-header manage-media-item">
|
||||
<div className="mi-checkbox">
|
||||
<input type="checkbox" checked={isSelected} onChange={checkAll} />
|
||||
</div>
|
||||
<div
|
||||
id="title"
|
||||
onClick={sortByColumn}
|
||||
className={'mi-title mi-col-sort' + ('title' === sort ? ('asc' === order ? ' asc' : ' desc') : '')}
|
||||
>
|
||||
Title
|
||||
<div className="mi-col-sort-icons">
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_up" />
|
||||
</span>
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_down" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="add_date"
|
||||
onClick={sortByColumn}
|
||||
className={'mi-added mi-col-sort' + ('add_date' === sort ? ('asc' === order ? ' asc' : ' desc') : '')}
|
||||
>
|
||||
Date added
|
||||
<div className="mi-col-sort-icons">
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_up" />
|
||||
</span>
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_down" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mi-author">Author</div>
|
||||
<div className="mi-type">Media type</div>
|
||||
<div className="mi-encoding">Encoding status</div>
|
||||
<div className="mi-state">State</div>
|
||||
<div className="mi-reviewed">Reviewed</div>
|
||||
<div className="mi-featured">Featured</div>
|
||||
<div className="mi-reported">Reported</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageMediaItemHeader.propTypes = {
|
||||
sort: PropTypes.string.isRequired,
|
||||
order: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
onClickColumnSort: PropTypes.func,
|
||||
onCheckAllRows: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { usePopup } from '../../../utils/hooks/';
|
||||
import { PageStore } from '../../../utils/stores/';
|
||||
import { PopupMain } from '../../_shared';
|
||||
import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon.jsx';
|
||||
import { ManageItemDate } from './ManageMediaItem';
|
||||
|
||||
function ManageItemName(props) {
|
||||
if (void 0 !== props.url) {
|
||||
if (null !== props.name && '' !== props.name) {
|
||||
return (
|
||||
<a href={props.url} title={props.name}>
|
||||
{props.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if (null !== props.name && '' !== props.name) {
|
||||
return props.name;
|
||||
}
|
||||
|
||||
return <i className="non-available">N/A</i>;
|
||||
}
|
||||
|
||||
function ManageItemUsername(props) {
|
||||
if (void 0 !== props.url) {
|
||||
if (null !== props.username && '' !== props.username) {
|
||||
return (
|
||||
<a href={props.url} title={props.username}>
|
||||
{props.username}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if (null !== props.username && '' !== props.username) {
|
||||
return props.username;
|
||||
}
|
||||
|
||||
return <i className="non-available">N/A</i>;
|
||||
}
|
||||
|
||||
function ManageItemCommentActions(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
const [isOpenPopup, setIsOpenPopup] = useState(false);
|
||||
|
||||
function onPopupShow() {
|
||||
setIsOpenPopup(true);
|
||||
}
|
||||
|
||||
function onPopupHide() {
|
||||
setIsOpenPopup(false);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
popupContentRef.current.tryToHide();
|
||||
if ('function' === typeof props.onCancel) {
|
||||
props.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function onProceed() {
|
||||
popupContentRef.current.tryToHide();
|
||||
if ('function' === typeof props.onProceed) {
|
||||
props.onProceed();
|
||||
}
|
||||
}
|
||||
|
||||
const positionState = { updating: false, pending: 0 };
|
||||
|
||||
const onWindowResize = useCallback(function () {
|
||||
if (positionState.updating) {
|
||||
positionState.pending = positionState.pending + 1;
|
||||
} else {
|
||||
positionState.updating = true;
|
||||
|
||||
const popupElem = props.containerRef.current.querySelector('.popup');
|
||||
|
||||
if (popupElem) {
|
||||
const containerClientRect = props.containerRef.current.getBoundingClientRect();
|
||||
|
||||
popupElem.style.position = 'fixed';
|
||||
popupElem.style.left = containerClientRect.x + 'px';
|
||||
|
||||
if (document.body.offsetHeight < 32 + popupElem.offsetHeight + window.scrollY + containerClientRect.top) {
|
||||
popupElem.style.top = containerClientRect.y - popupElem.offsetHeight + 'px';
|
||||
} else {
|
||||
popupElem.style.top = containerClientRect.y + containerClientRect.height + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
positionState.updating = false;
|
||||
|
||||
if (positionState.pending) {
|
||||
positionState.pending = 0;
|
||||
onWindowResize();
|
||||
}
|
||||
}, 8);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenPopup) {
|
||||
PageStore.on('window_scroll', onWindowResize);
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
onWindowResize();
|
||||
} else {
|
||||
PageStore.removeListener('window_scroll', onWindowResize);
|
||||
PageStore.removeListener('window_resize', onWindowResize);
|
||||
}
|
||||
}, [isOpenPopup]);
|
||||
|
||||
return (
|
||||
<div ref={props.containerRef} className="actions">
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button title={'Delete "' + props.name + '"'}>Delete</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef} showCallback={onPopupShow} hideCallback={onPopupHide}>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Member removal</span>
|
||||
<span className="popup-message-main">{'You\'re willing to remove member "' + props.name + '"'}?</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-profile-removal" onClick={onCancel}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-profile-removal" onClick={onProceed}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ManageUsersItem(props) {
|
||||
const actionsContainerRef = useRef(null);
|
||||
|
||||
const [selected, setSelected] = useState(false);
|
||||
|
||||
function onRowCheck() {
|
||||
setSelected(!selected);
|
||||
}
|
||||
|
||||
function onClickProceed() {
|
||||
if ('function' === typeof props.onProceedRemoval) {
|
||||
props.onProceedRemoval(props.username);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if ('function' === typeof props.onCheckRow) {
|
||||
props.onCheckRow(props.username, selected);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(props.selectedRow);
|
||||
}, [props.selectedRow]);
|
||||
|
||||
return (
|
||||
<div className="item manage-item manage-users-item">
|
||||
<div className="mi-checkbox">
|
||||
<input type="checkbox" checked={selected} onChange={onRowCheck} />
|
||||
</div>
|
||||
<div className="mi-name">
|
||||
<ManageItemName name={props.name} url={props.url} />
|
||||
<ManageItemCommentActions
|
||||
containerRef={actionsContainerRef}
|
||||
name={props.name || props.username}
|
||||
onProceed={onClickProceed}
|
||||
/>
|
||||
</div>
|
||||
<div className="mi-username">
|
||||
<ManageItemUsername username={props.username} url={props.url} />
|
||||
</div>
|
||||
<div className="mi-added">
|
||||
<ManageItemDate date={props.add_date} />
|
||||
</div>
|
||||
{props.has_roles ? (
|
||||
<div className="mi-role">
|
||||
{void 0 === props.roles ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : props.roles.length ? (
|
||||
props.roles.join('\n')
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{props.has_verified ? (
|
||||
<div className="mi-verified">
|
||||
{void 0 === props.is_verified ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : props.is_verified ? (
|
||||
<MaterialIcon type="check_circle" />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{props.has_trusted ? (
|
||||
<div className="mi-trusted">
|
||||
{void 0 === props.is_trusted ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : props.is_trusted ? (
|
||||
<MaterialIcon type="check_circle" />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mi-featured">
|
||||
{void 0 === props.is_featured ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : props.is_featured ? (
|
||||
<MaterialIcon type="check_circle" />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageUsersItem.propTypes = {
|
||||
thumbnail_url: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
add_date: PropTypes.string,
|
||||
is_featured: PropTypes.bool,
|
||||
onCheckRow: PropTypes.func,
|
||||
selectedRow: PropTypes.bool.isRequired,
|
||||
hideDeleteAction: PropTypes.bool.isRequired,
|
||||
has_roles: PropTypes.bool,
|
||||
has_verified: PropTypes.bool,
|
||||
has_trusted: PropTypes.bool,
|
||||
roles: PropTypes.array,
|
||||
is_verified: PropTypes.bool,
|
||||
is_trusted: PropTypes.bool,
|
||||
};
|
||||
|
||||
ManageUsersItem.defaultProps = {
|
||||
has_roles: false,
|
||||
has_verified: false,
|
||||
has_trusted: false,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useManagementTableHeader } from '../../../utils/hooks/';
|
||||
import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon';
|
||||
|
||||
export function ManageUsersItemHeader(props) {
|
||||
const [sort, order, isSelected, sortByColumn, checkAll] = useManagementTableHeader({ ...props, type: 'users' });
|
||||
|
||||
return (
|
||||
<div className="item manage-item manage-item-header manage-users-item">
|
||||
<div className="mi-checkbox">
|
||||
<input type="checkbox" checked={isSelected} onChange={checkAll} />
|
||||
</div>
|
||||
<div
|
||||
id="name"
|
||||
onClick={sortByColumn}
|
||||
className={'mi-name mi-col-sort' + ('name' === sort ? ('asc' === order ? ' asc' : ' desc') : '')}
|
||||
>
|
||||
Name
|
||||
<div className="mi-col-sort-icons">
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_up" />
|
||||
</span>
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_down" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mi-username">Username</div>
|
||||
<div
|
||||
id="add_date"
|
||||
onClick={sortByColumn}
|
||||
className={'mi-added mi-col-sort' + ('add_date' === sort ? ('asc' === order ? ' asc' : ' desc') : '')}
|
||||
>
|
||||
Date added
|
||||
<div className="mi-col-sort-icons">
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_up" />
|
||||
</span>
|
||||
<span>
|
||||
<MaterialIcon type="arrow_drop_down" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{props.has_roles ? <div className="mi-role">Role</div> : null}
|
||||
{props.has_verified ? <div className="mi-verified">Verified</div> : null}
|
||||
{props.has_trusted ? <div className="mi-trusted">Trusted</div> : null}
|
||||
<div className="mi-featured">Featured</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageUsersItemHeader.propTypes = {
|
||||
sort: PropTypes.string.isRequired,
|
||||
order: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
onClickColumnSort: PropTypes.func,
|
||||
onCheckAllRows: PropTypes.func,
|
||||
has_roles: PropTypes.bool,
|
||||
has_verified: PropTypes.bool,
|
||||
has_trusted: PropTypes.bool,
|
||||
};
|
||||
|
||||
ManageUsersItemHeader.defaultProps = {
|
||||
has_roles: false,
|
||||
has_verified: false,
|
||||
has_trusted: false,
|
||||
};
|
||||
201
frontend/src/static/js/components/management-table/ManageItemList-filters.scss
Executable file
201
frontend/src/static/js/components/management-table/ManageItemList-filters.scss
Executable file
@@ -0,0 +1,201 @@
|
||||
@use "sass:math";
|
||||
@import '../../../css/includes/_variables.scss';
|
||||
@import '../../../css/includes/_variables_dimensions.scss';
|
||||
|
||||
@import '../../../css/config/index.scss';
|
||||
|
||||
.mi-filters-row {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
transition-property: all;
|
||||
transition-duration: 0.2s;
|
||||
|
||||
&.hidden {
|
||||
height: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mi-filters-row-inner {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 24px;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px;
|
||||
border-color: var(--sidebar-nav-border-color);
|
||||
|
||||
.mi-filter {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
width: 50%;
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
padding-left: 0;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&:nth-child(2n + 2) {
|
||||
padding-left: 16px;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
width: math.div(1,3) * 100%;
|
||||
|
||||
&:nth-child(3n + 1) {
|
||||
padding-left: 0;
|
||||
padding-right: 21px;
|
||||
}
|
||||
|
||||
&:nth-child(3n + 2) {
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
}
|
||||
|
||||
&:nth-child(3n + 3) {
|
||||
padding-left: 21px;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
width: 20%;
|
||||
|
||||
&:nth-child(3n + 1),
|
||||
&:nth-child(3n + 2),
|
||||
&:nth-child(3n + 3) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&:nth-child(5n + 1) {
|
||||
padding-left: 0;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
&:nth-child(5n + 2) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&:nth-child(5n + 3) {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
&:nth-child(5n + 4) {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
&:nth-child(5n + 5) {
|
||||
padding-left: 32px;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mi-filter-title {
|
||||
padding: 4px 0 16px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.007px;
|
||||
margin-bottom: 4px;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px;
|
||||
border-color: var(--sidebar-nav-border-color);
|
||||
}
|
||||
|
||||
.mi-filter-options {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
padding: 3px 6px 4px 0;
|
||||
// line-height:16px;
|
||||
line-height: 1.5;
|
||||
text-align: initial;
|
||||
color: var(--header-circle-button-color);
|
||||
border: 0;
|
||||
background: none;
|
||||
opacity: 0.85;
|
||||
|
||||
.dark_theme & {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: 1px 0 0;
|
||||
margin: 0 0 0 4px;
|
||||
font-size: 1em;
|
||||
line-height: 1.45;
|
||||
}
|
||||
}
|
||||
|
||||
&.active button,
|
||||
button:hover {
|
||||
color: inherit;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mi-filters-toggle {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 0;
|
||||
|
||||
button {
|
||||
vertical-align: middle;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 2px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.007px;
|
||||
color: var(--header-circle-button-color);
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
opacity: 0.85;
|
||||
|
||||
.dark_theme & {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
color: inherit;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
margin-top: -2px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.filter-button-label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filter-button-label-text {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import urlParse from 'url-parse';
|
||||
import { deleteRequest, csrfToken } from '../../../utils/helpers/';
|
||||
import { usePopup } from '../../../utils/hooks/';
|
||||
import { PopupMain } from '../../_shared';
|
||||
import { PendingItemsList } from '../../item-list/PendingItemsList.jsx';
|
||||
import { renderManageItems } from './includes/functions';
|
||||
import initManageItemsList from './includes/initManageItemsList';
|
||||
import { ManageItemsListHandler } from './includes/ManageItemsListHandler';
|
||||
|
||||
import './ManageItemList.scss';
|
||||
|
||||
function useManageItemList(props, itemsListRef) {
|
||||
let previousItemsLength = 0;
|
||||
|
||||
let itemsListInstance = null;
|
||||
|
||||
const [items, setItems] = useState([]);
|
||||
|
||||
const [countedItems, setCountedItems] = useState(false);
|
||||
const [listHandler, setListHandler] = useState(null);
|
||||
|
||||
function onItemsLoad(itemsArray) {
|
||||
setItems([...itemsArray]);
|
||||
}
|
||||
|
||||
function onItemsCount(totalItems) {
|
||||
setCountedItems(true);
|
||||
if (void 0 !== props.itemsCountCallback) {
|
||||
props.itemsCountCallback(totalItems);
|
||||
}
|
||||
}
|
||||
|
||||
function addListItems() {
|
||||
if (previousItemsLength < items.length) {
|
||||
if (null === itemsListInstance) {
|
||||
itemsListInstance = initManageItemsList([itemsListRef.current])[0];
|
||||
}
|
||||
|
||||
// FIXME: Should get item elements from children components.
|
||||
const itemsElem = itemsListRef.current.querySelectorAll('.item');
|
||||
|
||||
if (!itemsElem || !itemsElem.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let i = previousItemsLength;
|
||||
|
||||
while (i < items.length) {
|
||||
itemsListInstance.appendItems(itemsElem[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
previousItemsLength = items.length;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (void 0 !== props.itemsLoadCallback) {
|
||||
props.itemsLoadCallback();
|
||||
}
|
||||
}, [items]);
|
||||
|
||||
return [items, countedItems, listHandler, setListHandler, onItemsLoad, onItemsCount, addListItems];
|
||||
}
|
||||
|
||||
function useManageItemListSync(props) {
|
||||
const itemsListRef = useRef(null);
|
||||
const itemsListWrapperRef = useRef(null);
|
||||
|
||||
const [items, countedItems, listHandler, setListHandler, onItemsLoad, onItemsCount, addListItems] = useManageItemList(
|
||||
{ ...props, itemsCountCallback },
|
||||
itemsListRef
|
||||
);
|
||||
|
||||
const [totalItems, setTotalItems] = useState(null);
|
||||
|
||||
let classname = {
|
||||
list: 'manage-items-list',
|
||||
listOuter: 'items-list-outer' + ('string' === typeof props.className ? ' ' + props.className.trim() : ''),
|
||||
};
|
||||
|
||||
function onClickLoadMore() {
|
||||
listHandler.loadItems();
|
||||
}
|
||||
|
||||
function itemsCountCallback(itemsSumm) {
|
||||
setTotalItems(itemsSumm);
|
||||
}
|
||||
|
||||
function afterItemsLoad() {}
|
||||
|
||||
function renderBeforeListWrap() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderAfterListWrap() {
|
||||
if (!listHandler) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 1 > listHandler.totalPages() || listHandler.loadedAllItems() ? null : (
|
||||
<button className="load-more" onClick={onClickLoadMore}>
|
||||
SHOW MORE
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
addListItems();
|
||||
afterItemsLoad();
|
||||
}, [items]);
|
||||
|
||||
return [
|
||||
countedItems,
|
||||
totalItems,
|
||||
items,
|
||||
listHandler,
|
||||
setListHandler,
|
||||
classname,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
renderBeforeListWrap,
|
||||
renderAfterListWrap,
|
||||
];
|
||||
}
|
||||
|
||||
function pageUrlQuery(baseQuery, pageNumber) {
|
||||
let queryParams = [];
|
||||
let pos = 0;
|
||||
|
||||
if ('' !== baseQuery) {
|
||||
queryParams = baseQuery.split('?')[1].split('&');
|
||||
|
||||
let param;
|
||||
|
||||
let i = 0;
|
||||
|
||||
while (i < queryParams.length) {
|
||||
param = queryParams[i].split('=');
|
||||
|
||||
if ('page' === param[0]) {
|
||||
pos = i;
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
queryParams[pos] = 'page=' + pageNumber;
|
||||
|
||||
return '?' + queryParams.join('&');
|
||||
}
|
||||
|
||||
function pageUrl(parsedUrl, query) {
|
||||
return parsedUrl.set('query', query).href;
|
||||
}
|
||||
|
||||
function BulkActions(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
const [selectedBulkAction, setSelectedBulkAction] = useState('');
|
||||
const [selectedItemsSize, setSelectedItemsSize] = useState(props.selectedItemsSize);
|
||||
|
||||
function onBulkActionSelect(ev) {
|
||||
setSelectedBulkAction(ev.currentTarget.value);
|
||||
}
|
||||
|
||||
function onClickProceed() {
|
||||
if ('function' === typeof props.onProceedRemoval) {
|
||||
props.onProceedRemoval();
|
||||
}
|
||||
|
||||
popupContentRef.current.tryToHide();
|
||||
}
|
||||
|
||||
function onClickCancel() {
|
||||
popupContentRef.current.tryToHide();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedItemsSize(props.selectedItemsSize);
|
||||
}, [props.selectedItemsSize]);
|
||||
|
||||
return (
|
||||
<div className="manage-items-bulk-action">
|
||||
<select value={selectedBulkAction} onChange={onBulkActionSelect}>
|
||||
<option value="">Bulk actions</option>
|
||||
<option value="delete">Delete selected</option>
|
||||
</select>
|
||||
|
||||
{!selectedItemsSize || !selectedBulkAction ? null : (
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button>Apply</button>
|
||||
</PopupTrigger>
|
||||
)}
|
||||
|
||||
<PopupContent contentRef={popupContentRef}>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Bulk removal</span>
|
||||
<span className="popup-message-main">You're willing to remove selected items permanently?</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-profile-removal" onClick={onClickCancel}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-profile-removal" onClick={onClickProceed}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ManageItemsOptions(props) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<BulkActions selectedItemsSize={props.items.length} onProceedRemoval={props.onProceedRemoval} />
|
||||
{1 === props.pagesSize ? null : (
|
||||
<div className="manage-items-pagination">
|
||||
<PaginationButtons
|
||||
totalItems={props.totalItems}
|
||||
pageItems={props.pageItems}
|
||||
onPageButtonClick={props.onPageButtonClick}
|
||||
query={props.query}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationButtons(props) {
|
||||
const buttons = [];
|
||||
|
||||
let i;
|
||||
|
||||
let maxPagin = 11;
|
||||
|
||||
const newPagesNumber = {
|
||||
last: Math.ceil(props.totalItems / props.pageItems),
|
||||
current: 1,
|
||||
};
|
||||
|
||||
if ('' !== props.query) {
|
||||
const queryParams = props.query.split('?')[1].split('&');
|
||||
|
||||
let param;
|
||||
|
||||
let i = 0;
|
||||
while (i < queryParams.length) {
|
||||
param = queryParams[i].split('=');
|
||||
if ('page' === param[0]) {
|
||||
newPagesNumber.current = parseInt(param[1], 10);
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const paginButtonsData = paginationButtonsList(maxPagin, newPagesNumber);
|
||||
|
||||
i = 0;
|
||||
while (i < paginButtonsData.length) {
|
||||
if ('button' === paginButtonsData[i].type) {
|
||||
buttons.push(
|
||||
<button
|
||||
key={i + '[button]'}
|
||||
onClick={props.onPageButtonClick}
|
||||
page={paginButtonsData[i].number}
|
||||
className={newPagesNumber.current === paginButtonsData[i].number ? 'active' : ''}
|
||||
>
|
||||
{paginButtonsData[i].number}
|
||||
</button>
|
||||
);
|
||||
} else if ('dots' === paginButtonsData[i].type) {
|
||||
buttons.push(
|
||||
<span key={i + '[dots]'} className="pagination-dots">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function paginationButtonsList(maxPagin, pagesNumber) {
|
||||
if (3 > maxPagin) {
|
||||
maxPagin = 3;
|
||||
}
|
||||
|
||||
let i;
|
||||
|
||||
let maxCurr;
|
||||
let maxEdge = 1;
|
||||
|
||||
if (maxPagin >= pagesNumber.last) {
|
||||
maxPagin = pagesNumber.last;
|
||||
maxCurr = pagesNumber.last;
|
||||
maxEdge = 0;
|
||||
} else {
|
||||
if (5 < maxPagin) {
|
||||
if (7 >= maxPagin) {
|
||||
maxEdge = 2;
|
||||
} else {
|
||||
maxEdge = Math.floor(maxPagin / 4);
|
||||
}
|
||||
}
|
||||
|
||||
maxCurr = maxPagin - 2 * maxEdge;
|
||||
}
|
||||
|
||||
const currentArr = [];
|
||||
const firstArr = [];
|
||||
const lastArr = [];
|
||||
|
||||
if (pagesNumber.current <= maxCurr + maxEdge - pagesNumber.current) {
|
||||
i = 1;
|
||||
|
||||
while (i <= maxCurr + maxEdge) {
|
||||
currentArr.push(i);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
i = pagesNumber.last - maxPagin + currentArr.length + 1;
|
||||
|
||||
while (i <= pagesNumber.last) {
|
||||
lastArr.push(i);
|
||||
i += 1;
|
||||
}
|
||||
} else if (pagesNumber.current > pagesNumber.last - (maxCurr + maxEdge - 1)) {
|
||||
i = pagesNumber.last - (maxCurr + maxEdge - 1);
|
||||
|
||||
while (i <= pagesNumber.last) {
|
||||
currentArr.push(i);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
i = 1;
|
||||
while (i <= maxPagin - currentArr.length) {
|
||||
firstArr.push(i);
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
currentArr.push(pagesNumber.current);
|
||||
|
||||
i = 1;
|
||||
while (maxCurr > currentArr.length) {
|
||||
currentArr.push(pagesNumber.current + i);
|
||||
|
||||
if (maxCurr === currentArr.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentArr.unshift(pagesNumber.current - i);
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
i = 1;
|
||||
while (i <= maxEdge) {
|
||||
firstArr.push(i);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
i = pagesNumber.last - (maxPagin - (firstArr.length + currentArr.length) - 1);
|
||||
while (i <= pagesNumber.last) {
|
||||
lastArr.push(i);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const ret = [];
|
||||
|
||||
i = 0;
|
||||
while (i < firstArr.length) {
|
||||
ret.push({
|
||||
type: 'button',
|
||||
number: firstArr[i],
|
||||
});
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (firstArr.length && currentArr.length && firstArr[firstArr.length - 1] + 1 < currentArr[0]) {
|
||||
ret.push({
|
||||
type: 'dots',
|
||||
});
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < currentArr.length) {
|
||||
ret.push({
|
||||
type: 'button',
|
||||
number: currentArr[i],
|
||||
});
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (currentArr.length && lastArr.length && currentArr[currentArr.length - 1] + 1 < lastArr[0]) {
|
||||
ret.push({
|
||||
type: 'dots',
|
||||
});
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < lastArr.length) {
|
||||
ret.push({
|
||||
type: 'button',
|
||||
number: lastArr[i],
|
||||
});
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function ManageItemList(props) {
|
||||
const [
|
||||
countedItems,
|
||||
totalItems,
|
||||
items,
|
||||
listHandler,
|
||||
setListHandler,
|
||||
classname,
|
||||
itemsListWrapperRef,
|
||||
itemsListRef,
|
||||
onItemsCount,
|
||||
onItemsLoad,
|
||||
] = useManageItemListSync(props);
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [selectedAllItems, setSelectedAllItems] = useState(false);
|
||||
|
||||
const [parsedRequestUrl, setParsedRequestUrl] = useState(null);
|
||||
const [parsedRequestUrlQuery, setParsedRequestUrlQuery] = useState(null);
|
||||
|
||||
function onPageButtonClick(ev) {
|
||||
const clickedPageUrl = pageUrl(
|
||||
parsedRequestUrl,
|
||||
pageUrlQuery(parsedRequestUrlQuery, ev.currentTarget.getAttribute('page'))
|
||||
);
|
||||
|
||||
if ('function' === typeof props.onPageChange) {
|
||||
props.onPageChange(clickedPageUrl, ev.currentTarget.getAttribute('page'));
|
||||
}
|
||||
}
|
||||
|
||||
function onBulkItemsRemoval() {
|
||||
deleteSelectedItems();
|
||||
}
|
||||
|
||||
function onAllRowsCheck(selectedAllRows, tableType) {
|
||||
const newSelected = [];
|
||||
|
||||
if (selectedAllRows) {
|
||||
if (items.length !== selectedItems.length) {
|
||||
let entry;
|
||||
|
||||
if ('media' === tableType) {
|
||||
for (entry of items) {
|
||||
newSelected.push(entry.friendly_token);
|
||||
}
|
||||
} else if ('users' === tableType) {
|
||||
for (entry of items) {
|
||||
newSelected.push(entry.username);
|
||||
}
|
||||
} else if ('comments' === tableType) {
|
||||
for (entry of items) {
|
||||
newSelected.push(entry.uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedItems(newSelected);
|
||||
setSelectedAllItems(newSelected.length === items.length);
|
||||
}
|
||||
|
||||
function onRowCheck(token, isSelected) {
|
||||
if (void 0 !== token) {
|
||||
let newSelected;
|
||||
|
||||
if (-1 === selectedItems.indexOf(token)) {
|
||||
if (isSelected) {
|
||||
newSelected = [...selectedItems, token];
|
||||
|
||||
setSelectedItems(newSelected);
|
||||
setSelectedAllItems(newSelected.length === items.length);
|
||||
}
|
||||
} else {
|
||||
if (!isSelected) {
|
||||
newSelected = [];
|
||||
|
||||
let entry;
|
||||
for (entry of selectedItems) {
|
||||
if (token !== entry) {
|
||||
newSelected.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedItems(newSelected);
|
||||
setSelectedAllItems(newSelected.length === items.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeBulkMediaResponse(response) {
|
||||
if (response && 204 === response.status) {
|
||||
setSelectedItems([]);
|
||||
setSelectedAllItems(false);
|
||||
|
||||
if ('function' === typeof props.onRowsDelete) {
|
||||
props.onRowsDelete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeBulkMediaFail() {
|
||||
if ('function' === typeof props.onRowsDeleteFail) {
|
||||
props.onRowsDeleteFail(true);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteItem(token, isManageComments) {
|
||||
deleteRequest(
|
||||
props.requestUrl.split('?')[0] + ('comments' === props.manageType ? '?comment_ids=' : '?tokens=') + token,
|
||||
{
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken(),
|
||||
},
|
||||
tokens: token,
|
||||
},
|
||||
false,
|
||||
removeMediaResponse,
|
||||
removeMediaFail
|
||||
);
|
||||
}
|
||||
|
||||
function deleteSelectedItems() {
|
||||
deleteRequest(
|
||||
props.requestUrl.split('?')[0] +
|
||||
('comments' === props.manageType ? '?comment_ids=' : '?tokens=') +
|
||||
selectedItems.join(','),
|
||||
{
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken(),
|
||||
},
|
||||
},
|
||||
false,
|
||||
removeBulkMediaResponse,
|
||||
removeBulkMediaFail
|
||||
);
|
||||
}
|
||||
|
||||
function removeMediaResponse(response) {
|
||||
if (response && 204 === response.status) {
|
||||
props.onRowsDelete(false);
|
||||
}
|
||||
}
|
||||
|
||||
function removeMediaFail() {
|
||||
props.onRowsDeleteFail(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (parsedRequestUrl) {
|
||||
setParsedRequestUrlQuery(parsedRequestUrl.query);
|
||||
}
|
||||
}, [parsedRequestUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
setParsedRequestUrl(urlParse(props.requestUrl));
|
||||
}, [props.requestUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
setListHandler(
|
||||
new ManageItemsListHandler(props.pageItems, props.maxItems, props.requestUrl, onItemsCount, onItemsLoad)
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
<div className={classname.listOuter}>
|
||||
<ManageItemsOptions
|
||||
totalItems={totalItems}
|
||||
pageItems={props.pageItems}
|
||||
onPageButtonClick={onPageButtonClick}
|
||||
query={parsedRequestUrlQuery || ''}
|
||||
className="manage-items-options"
|
||||
items={selectedItems}
|
||||
pagesSize={listHandler.totalPages()}
|
||||
onProceedRemoval={onBulkItemsRemoval}
|
||||
/>
|
||||
|
||||
<div ref={itemsListWrapperRef} className="items-list-wrap">
|
||||
<div ref={itemsListRef} className={classname.list}>
|
||||
{renderManageItems(items, {
|
||||
...props,
|
||||
onAllRowsCheck: onAllRowsCheck,
|
||||
onRowCheck: onRowCheck,
|
||||
selectedItems: selectedItems,
|
||||
selectedAllItems: selectedAllItems,
|
||||
onDelete: deleteItem,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ManageItemsOptions
|
||||
totalItems={totalItems}
|
||||
pageItems={props.pageItems}
|
||||
onPageButtonClick={onPageButtonClick}
|
||||
query={parsedRequestUrlQuery || ''}
|
||||
className="manage-items-options popup-on-top"
|
||||
items={selectedItems}
|
||||
pagesSize={listHandler.totalPages()}
|
||||
onProceedRemoval={onBulkItemsRemoval}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageItemList.defaultProps = {
|
||||
itemsCountCallback: PropTypes.func,
|
||||
maxItems: PropTypes.number.isRequired,
|
||||
pageItems: PropTypes.number.isRequired,
|
||||
requestUrl: PropTypes.string.isRequired,
|
||||
onPageChange: PropTypes.func,
|
||||
onRowsDelete: PropTypes.func,
|
||||
onRowsDeleteFail: PropTypes.func,
|
||||
pageItems: 24,
|
||||
};
|
||||
|
||||
ManageItemList.defaultProps = {
|
||||
maxItems: 99999,
|
||||
pageItems: 24,
|
||||
requestUrl: null,
|
||||
};
|
||||
@@ -0,0 +1,666 @@
|
||||
@use "sass:math";
|
||||
@import '../../../../css/includes/_variables.scss';
|
||||
@import '../../../../css/includes/_variables_dimensions.scss';
|
||||
|
||||
@import '../../../../css/config/index.scss';
|
||||
|
||||
#page-manage-media,
|
||||
#page-manage-users,
|
||||
#page-manage-comments {
|
||||
.media-list-wrapper {
|
||||
padding: 0 16px;
|
||||
|
||||
@media (min-width: 710px) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
max-width: calc(48px + calc(var(--default-item-width) * var(--default-max-row-items)));
|
||||
}
|
||||
|
||||
.manage-items-list {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.items-list-outer {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.items-list-wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-list-header {
|
||||
display: block;
|
||||
padding: 12px 0;
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
display: inline-block;
|
||||
margin: 12px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
margin: 10px 16px;
|
||||
text-decoration: none;
|
||||
color: var(--media-list-header-title-link-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manage-items-list {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
word-break: break-word;
|
||||
|
||||
border-radius: 1px;
|
||||
box-shadow: 0px 4px 8px 0 rgba(17, 17, 17, 0.06);
|
||||
overflow: auto;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-list-header {
|
||||
display: block;
|
||||
padding: 12px 0;
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
display: inline-block;
|
||||
margin: 12px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
margin: 10px 16px;
|
||||
text-decoration: none;
|
||||
color: var(--media-list-header-title-link-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manage-item,
|
||||
.item.manage-item {
|
||||
position: relative;
|
||||
display: table;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px;
|
||||
border-color: #f0f0f0;
|
||||
|
||||
background-color: var(--user-action-form-inner-bg-color);
|
||||
|
||||
.dark_theme & {
|
||||
border-color: #2d2d2d;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.dark_theme & {
|
||||
background-color: #202020;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
box-shadow: 0px 1px 2px 0 rgba(#000, 0.12);
|
||||
|
||||
.dark_theme & {
|
||||
background-color: #181818;
|
||||
box-shadow: 0px 1px 2px 0 rgba(#000, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
display: table-cell;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
|
||||
.dark_theme & {
|
||||
border-color: #2d2d2d;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> * {
|
||||
border-color: #eaeaea;
|
||||
|
||||
.dark_theme & {
|
||||
border-color: #181818;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.material-icons[data-icon='check_circle'],
|
||||
.material-icons[data-icon='check_circle_outline'] {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.material-icons[data-icon='cancel'],
|
||||
.material-icons[data-icon='highlight_off'],
|
||||
.reported-number {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.reported-number {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.non-available {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
min-width: 98px;
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mi-title,
|
||||
.mi-name {
|
||||
.actions {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
|
||||
button {
|
||||
font-size: 12px;
|
||||
color: var(--danger-color);
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
font-size: initial;
|
||||
font-weight: initial;
|
||||
}
|
||||
|
||||
.popup-message-bottom {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
color: var(--popup-msg-main-text-color);
|
||||
|
||||
&.proceed-profile-removal {
|
||||
float: right;
|
||||
color: var(--default-theme-color);
|
||||
}
|
||||
|
||||
&.cancel-profile-removal {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.manage-media-item {
|
||||
> * {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mi-title,
|
||||
.mi-author {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
text-align: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mi-type,
|
||||
.mi-encoding,
|
||||
.mi-state {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.mi-checkbox {
|
||||
min-width: 48px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.mi-title {
|
||||
min-width: 240px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mi-author {
|
||||
min-width: 184px;
|
||||
}
|
||||
|
||||
.mi-added {
|
||||
min-width: 168px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.mi-type {
|
||||
}
|
||||
|
||||
.mi-encoding {
|
||||
min-width: 136px;
|
||||
}
|
||||
|
||||
.mi-state,
|
||||
.mi-reviewed,
|
||||
.mi-featured,
|
||||
.mi-reported {
|
||||
min-width: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
&.manage-users-item {
|
||||
> * {
|
||||
width: math.div(1,8) * 100%;
|
||||
}
|
||||
|
||||
.mi-added,
|
||||
.mi-role,
|
||||
.mi-featured,
|
||||
.mi-verified,
|
||||
.mi-trusted,
|
||||
.mi-checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mi-name,
|
||||
.mi-username {
|
||||
min-width: 240px;
|
||||
min-width: 200px;
|
||||
width: 50%;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mi-checkbox {
|
||||
min-width: 48px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.mi-added {
|
||||
min-width: 168px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.manage-comments-item {
|
||||
> * {
|
||||
width: 16%;
|
||||
}
|
||||
|
||||
.mi-title,
|
||||
.mi-comment,
|
||||
.mi-author {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.mi-comment,
|
||||
.mi-added {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mi-author {
|
||||
min-width: 160px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mi-comment {
|
||||
min-width: 240px;
|
||||
|
||||
.actions {
|
||||
margin: 0.5em 0 0;
|
||||
font-size: 0.92857em;
|
||||
// font-weight:500;
|
||||
|
||||
.seperator {
|
||||
margin: 0 4px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
button {
|
||||
color: var(--danger-color);
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
font-size: initial;
|
||||
font-weight: initial;
|
||||
}
|
||||
|
||||
.popup-message-bottom {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
color: var(--popup-msg-main-text-color);
|
||||
|
||||
&.proceed-profile-removal {
|
||||
float: right;
|
||||
color: var(--default-theme-color);
|
||||
}
|
||||
|
||||
&.cancel-profile-removal {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mi-added {
|
||||
min-width: 192px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mi-checkbox {
|
||||
min-width: 48px;
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.manage-item-header {
|
||||
.mi-comment {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.manage-item-header {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.007px;
|
||||
background-color: #e3e3e3;
|
||||
|
||||
.dark_theme & {
|
||||
background-color: #151515;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
> * {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-right: 0;
|
||||
text-transform: uppercase !important;
|
||||
}
|
||||
|
||||
> .mi-col-sort {
|
||||
.mi-col-sort-icons {
|
||||
position: relative;
|
||||
display: inline;
|
||||
vertical-align: top;
|
||||
background-color: yellow;
|
||||
|
||||
.material-icons {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 0 0 0 1px;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> * {
|
||||
opacity: 0.25;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
&:first-child {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
.mi-col-sort-icons {
|
||||
> * {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.desc {
|
||||
.mi-col-sort-icons {
|
||||
> *:last-child {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.asc {
|
||||
.mi-col-sort-icons {
|
||||
> *:first-child {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manage-items-options {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100%;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.manage-items-bulk-action {
|
||||
position: relative;
|
||||
width: auto;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
|
||||
> select {
|
||||
margin-right: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-color: var(--input-bg-color);
|
||||
background-color: var(--user-action-form-inner-bg-color);
|
||||
box-shadow: 0px 1px 4px 0 rgba(17, 17, 17, 0.06);
|
||||
}
|
||||
|
||||
> button {
|
||||
padding: 0;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--default-theme-color);
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--user-action-form-inner-bg-color);
|
||||
|
||||
.popup-on-top & {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.popup-message-bottom {
|
||||
float: left;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
width: auto;
|
||||
float: left;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
font-size: 1em;
|
||||
color: var(--popup-msg-main-text-color);
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
&.proceed-profile-removal {
|
||||
float: right;
|
||||
color: var(--default-theme-color);
|
||||
}
|
||||
|
||||
&.cancel-profile-removal {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manage-items-pagination {
|
||||
position: relative;
|
||||
width: auto;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
|
||||
float: right;
|
||||
display: inline-block;
|
||||
|
||||
button,
|
||||
.pagination-dots {
|
||||
padding: 0;
|
||||
margin: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
background: var(--user-action-form-inner-bg-color);
|
||||
border-radius: 1px;
|
||||
box-shadow: 0px 1px 4px 0 rgba(17, 17, 17, 0.06);
|
||||
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #f0f0f0;
|
||||
|
||||
.dark_theme & {
|
||||
border-color: #2d2d2d;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
font-weight: 500;
|
||||
color: var(--default-theme-color);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--user-action-form-inner-bg-color);
|
||||
|
||||
.dark_theme & {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
background-color: var(--default-theme-color);
|
||||
border-color: var(--default-theme-color);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-dots {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { PageStore } from '../../../../utils/stores/';
|
||||
import { formatInnerLink, getRequest } from '../../../../utils/helpers/';
|
||||
|
||||
export function ManageItemsListHandler(itemsPerPage, maxItems, request_url, itemsCountCallback, loadItemsCallback) {
|
||||
const config = {
|
||||
maxItems: maxItems || 255,
|
||||
pageItems: itemsPerPage ? Math.min(maxItems, itemsPerPage) : 1,
|
||||
};
|
||||
|
||||
const state = {
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
nextRequestUrl: formatInnerLink(request_url, PageStore.get('config-site').url),
|
||||
};
|
||||
|
||||
const waiting = {
|
||||
pageItems: 0,
|
||||
requestResponse: false,
|
||||
};
|
||||
|
||||
let firstItemUrl = null;
|
||||
|
||||
const items = [];
|
||||
const responseItems = [];
|
||||
|
||||
const callbacks = {
|
||||
itemsCount: function () {
|
||||
if ('function' === typeof itemsCountCallback) {
|
||||
itemsCountCallback(state.totalItems);
|
||||
}
|
||||
},
|
||||
itemsLoad: function () {
|
||||
if ('function' === typeof loadItemsCallback) {
|
||||
loadItemsCallback(items);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function loadNextItems(itemsLength) {
|
||||
let itemsToLoad, needExtraRequest;
|
||||
|
||||
itemsLength = !isNaN(itemsLength) ? itemsLength : config.pageItems;
|
||||
|
||||
if (waiting.pageItems && waiting.pageItems <= responseItems.length) {
|
||||
itemsToLoad = waiting.pageItems;
|
||||
needExtraRequest = false;
|
||||
waiting.pageItems = 0;
|
||||
} else {
|
||||
itemsToLoad = Math.min(itemsLength, responseItems.length);
|
||||
needExtraRequest = itemsLength > responseItems.length && !!state.nextRequestUrl;
|
||||
waiting.pageItems = needExtraRequest ? itemsLength - responseItems.length : 0;
|
||||
}
|
||||
|
||||
if (itemsToLoad) {
|
||||
let i = 0;
|
||||
while (i < itemsToLoad) {
|
||||
items.push(responseItems.shift());
|
||||
i += 1;
|
||||
}
|
||||
callbacks.itemsLoad();
|
||||
}
|
||||
|
||||
if (needExtraRequest) {
|
||||
runRequest();
|
||||
}
|
||||
}
|
||||
|
||||
function runRequest(initialRequest) {
|
||||
waiting.requestResponse = true;
|
||||
|
||||
function fn(response) {
|
||||
waiting.requestResponse = false;
|
||||
|
||||
if (!!!response || !!!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = response.data;
|
||||
let results = void 0 !== data.results ? data.results : data; // NOTE: The structure of response data in the case of categories differs from the others.
|
||||
|
||||
// console.log( firstItemUrl );
|
||||
|
||||
let i = 0;
|
||||
while (i < results.length && config.maxItems > responseItems.length) {
|
||||
if (null === firstItemUrl || firstItemUrl !== results[i].url) {
|
||||
responseItems.push(results[i]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
state.nextRequestUrl = !!data.next && config.maxItems > responseItems.length ? data.next : null;
|
||||
|
||||
if (initialRequest) {
|
||||
// In some cases, (total) 'count' field is missing, but probably doesn't need (eg. in recommended media).
|
||||
state.totalItems = !!data.count ? data.count : responseItems.length;
|
||||
state.totalItems = Math.min(config.maxItems, state.totalItems);
|
||||
|
||||
state.totalPages = Math.ceil(state.totalItems / config.pageItems);
|
||||
|
||||
callbacks.itemsCount();
|
||||
}
|
||||
|
||||
loadNextItems();
|
||||
}
|
||||
|
||||
getRequest(state.nextRequestUrl, false, fn);
|
||||
|
||||
state.nextRequestUrl = null;
|
||||
}
|
||||
|
||||
function loadItems(itemsLength) {
|
||||
if (!waiting.requestResponse && items.length < state.totalItems) {
|
||||
loadNextItems(itemsLength);
|
||||
}
|
||||
}
|
||||
|
||||
function totalPages() {
|
||||
return state.totalPages;
|
||||
}
|
||||
|
||||
function loadedAllItems() {
|
||||
return items.length === state.totalItems;
|
||||
}
|
||||
|
||||
runRequest(true);
|
||||
|
||||
return {
|
||||
loadItems,
|
||||
totalPages,
|
||||
loadedAllItems,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export default class MediaItem {
|
||||
constructor(item) {
|
||||
if (!Node.prototype.isPrototypeOf(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.element = item;
|
||||
}
|
||||
|
||||
element() {
|
||||
return this.element;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import ManageMediaItem from './ManageMediaItem';
|
||||
|
||||
const _ManageMediaItemsListData = {};
|
||||
|
||||
export default class MediaItemsList {
|
||||
constructor(listContainer, initialItems) {
|
||||
if (!Node.prototype.isPrototypeOf(listContainer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_ManageMediaItemsListData[
|
||||
Object.defineProperty(this, 'id', {
|
||||
value: 'ManageMediaItemsList_' + Object.keys(_ManageMediaItemsListData).length,
|
||||
}).id
|
||||
] = {};
|
||||
|
||||
this.items = [];
|
||||
this.container = listContainer;
|
||||
|
||||
this.appendItems(initialItems);
|
||||
}
|
||||
|
||||
dataObject() {
|
||||
return _ManageMediaItemsListData;
|
||||
}
|
||||
|
||||
appendItems(items) {
|
||||
var i;
|
||||
if (NodeList.prototype.isPrototypeOf(items)) {
|
||||
i = 0;
|
||||
while (i < items.length) {
|
||||
this.items.push(new ManageMediaItem(items[i]));
|
||||
i += 1;
|
||||
}
|
||||
} else if (Node.prototype.isPrototypeOf(items)) {
|
||||
this.items.push(new ManageMediaItem(items));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import React from 'react';
|
||||
import { ManageMediaItem } from '../../ManageItem/ManageMediaItem';
|
||||
import { ManageUsersItem } from '../../ManageItem/ManageUsersItem';
|
||||
import { ManageCommentsItem } from '../../ManageItem/ManageCommentsItem';
|
||||
import { ManageMediaItemHeader } from '../../ManageItem/ManageMediaItemHeader';
|
||||
import { ManageUsersItemHeader } from '../../ManageItem/ManageUsersItemHeader';
|
||||
import { ManageCommentsItemHeader } from '../../ManageItem/ManageCommentsItemHeader';
|
||||
|
||||
function useManageItem(props) {
|
||||
const itemData = props.item;
|
||||
|
||||
const itemProps = {
|
||||
order: props.order,
|
||||
onCheckRow: props.onCheckRow,
|
||||
selectedRow: props.selectedRow,
|
||||
onProceedRemoval: props.onProceedRemoval,
|
||||
hideDeleteAction: props.hideDeleteAction,
|
||||
};
|
||||
|
||||
return [itemData, itemProps];
|
||||
}
|
||||
|
||||
function ListManageMediaItem(props) {
|
||||
const [itemData, itemProps] = useManageItem(props);
|
||||
|
||||
const args = {
|
||||
...itemProps,
|
||||
thumbnail_url: itemData.thumbnail_url,
|
||||
title: itemData.title,
|
||||
url: itemData.url.replace(' ', '%20'),
|
||||
author_name: itemData.author_name,
|
||||
author_url: itemData.author_profile,
|
||||
add_date: itemData.add_date,
|
||||
media_type: itemData.media_type,
|
||||
encoding_status: itemData.encoding_status,
|
||||
state: itemData.state,
|
||||
is_reviewed: itemData.is_reviewed,
|
||||
featured: itemData.featured,
|
||||
reported_times: itemData.reported_times,
|
||||
token: itemData.friendly_token,
|
||||
};
|
||||
|
||||
return <ManageMediaItem {...args} />;
|
||||
}
|
||||
|
||||
function ListManageUserItem(props) {
|
||||
const [itemData, itemProps] = useManageItem(props);
|
||||
|
||||
const roles = [];
|
||||
|
||||
if (void 0 !== itemData.is_editor && itemData.is_editor) {
|
||||
roles.push('Editor');
|
||||
}
|
||||
|
||||
if (void 0 !== itemData.is_manager && itemData.is_manager) {
|
||||
roles.push('Manager');
|
||||
}
|
||||
|
||||
const args = {
|
||||
...itemProps,
|
||||
thumbnail_url: itemData.thumbnail_url,
|
||||
name: itemData.name,
|
||||
url: itemData.url.replace(' ', '%20'),
|
||||
username: itemData.username,
|
||||
add_date: itemData.date_added,
|
||||
is_featured: itemData.is_featured,
|
||||
roles: roles,
|
||||
is_verified: true === itemData.email_is_verified,
|
||||
is_trusted: true === itemData.advancedUser,
|
||||
has_roles: void 0 !== itemData.is_editor || void 0 !== itemData.is_manager,
|
||||
has_verified: void 0 !== itemData.email_is_verified,
|
||||
has_trusted: void 0 !== itemData.advancedUser,
|
||||
};
|
||||
|
||||
return <ManageUsersItem {...args} />;
|
||||
}
|
||||
|
||||
function ListManageCommentItem(props) {
|
||||
const [itemData, itemProps] = useManageItem(props);
|
||||
|
||||
const args = {
|
||||
...itemProps,
|
||||
media_url: void 0 !== itemData.media_url ? itemData.media_url.replace(' ', '%20') : void 0,
|
||||
author_name: itemData.author_name,
|
||||
author_url: itemData.author_profile,
|
||||
author_thumbnail_url: itemData.author_thumbnail_url,
|
||||
add_date: itemData.add_date,
|
||||
text: itemData.text,
|
||||
uid: itemData.uid,
|
||||
};
|
||||
|
||||
return <ManageCommentsItem {...args} />;
|
||||
}
|
||||
|
||||
function ListManageItem(props) {
|
||||
const args = {
|
||||
item: props.item,
|
||||
order: props.order,
|
||||
hideDeleteAction: false,
|
||||
onCheckRow: props.onCheckRow,
|
||||
onProceedRemoval: props.onProceedRemoval,
|
||||
};
|
||||
|
||||
if ('media' === props.type) {
|
||||
return <ListManageMediaItem {...args} selectedRow={-1 < props.selectedItems.indexOf(props.item.friendly_token)} />;
|
||||
}
|
||||
|
||||
if ('users' === props.type) {
|
||||
return <ListManageUserItem {...args} selectedRow={-1 < props.selectedItems.indexOf(props.item.username)} />;
|
||||
}
|
||||
|
||||
if ('comments' === props.type) {
|
||||
return <ListManageCommentItem {...args} selectedRow={-1 < props.selectedItems.indexOf(props.item.uid)} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ListManageItemHeader(props) {
|
||||
const args = {
|
||||
sort: props.sort,
|
||||
order: props.order,
|
||||
selected: props.selected,
|
||||
onCheckAllRows: props.onCheckAllRows,
|
||||
onClickColumnSort: props.onClickColumnSort,
|
||||
};
|
||||
|
||||
if ('media' === props.type) {
|
||||
return <ManageMediaItemHeader {...args} />;
|
||||
}
|
||||
|
||||
if ('users' === props.type) {
|
||||
args.has_roles =
|
||||
props.items.length && (void 0 !== props.items[0].is_editor || void 0 !== props.items[0].is_manager);
|
||||
args.has_verified = props.items.length && void 0 !== props.items[0].email_is_verified;
|
||||
args.has_trusted = props.items.length && void 0 !== props.items[0].advancedUser;
|
||||
return <ManageUsersItemHeader {...args} />;
|
||||
}
|
||||
|
||||
if ('comments' === props.type) {
|
||||
return <ManageCommentsItemHeader {...args} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function renderManageItems(items, props) {
|
||||
return [
|
||||
<ListManageItemHeader
|
||||
key={0}
|
||||
type={props.manageType}
|
||||
items={items}
|
||||
sort={props.sortBy}
|
||||
order={props.ordering}
|
||||
selected={props.selectedAllItems}
|
||||
onCheckAllRows={props.onAllRowsCheck}
|
||||
onClickColumnSort={props.onClickColumnSort}
|
||||
/>,
|
||||
...items.map((item, index) => (
|
||||
<ListManageItem
|
||||
key={index + 1}
|
||||
order={index + 1}
|
||||
item={item}
|
||||
type={props.manageType}
|
||||
onCheckRow={props.onRowCheck}
|
||||
onProceedRemoval={props.onDelete}
|
||||
selectedItems={props.selectedItems}
|
||||
/>
|
||||
)),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import ManageMediaItemsList from './ManageMediaItemsList';
|
||||
|
||||
var CSS_selectors = {
|
||||
mediaItems: '.item',
|
||||
};
|
||||
|
||||
var ManageMediaItemsListInstances = [];
|
||||
|
||||
export default function (lists) {
|
||||
if (!lists.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let items,
|
||||
i = 0;
|
||||
|
||||
while (i < lists.length) {
|
||||
items = lists[i].querySelectorAll(CSS_selectors.mediaItems);
|
||||
|
||||
if (items.length) {
|
||||
ManageMediaItemsListInstances = ManageMediaItemsListInstances || [];
|
||||
ManageMediaItemsListInstances.push(new ManageMediaItemsList(lists[i], items));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return ManageMediaItemsListInstances;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { FilterOptions } from '../_shared';
|
||||
|
||||
import './ManageItemList-filters.scss';
|
||||
|
||||
const filters = {
|
||||
state: [
|
||||
{ id: 'all', title: 'All' },
|
||||
{ id: 'public', title: 'Public' },
|
||||
{ id: 'private', title: 'Private' },
|
||||
{ id: 'unlisted', title: 'Unlisted' },
|
||||
],
|
||||
media_type: [
|
||||
{ id: 'all', title: 'All' },
|
||||
{ id: 'video', title: 'Video' },
|
||||
{ id: 'audio', title: 'Audio' },
|
||||
{ id: 'image', title: 'Image' },
|
||||
{ id: 'pdf', title: 'Pdf' },
|
||||
],
|
||||
encoding_status: [
|
||||
{ id: 'all', title: 'All' },
|
||||
{ id: 'success', title: 'Success' },
|
||||
{ id: 'running', title: 'Running' },
|
||||
{ id: 'pending', title: 'Pending' },
|
||||
{ id: 'fail', title: 'Fail' },
|
||||
],
|
||||
reviewed: [
|
||||
{ id: 'all', title: 'All' },
|
||||
{ id: 'true', title: 'Yes' },
|
||||
{ id: 'false', title: 'No' },
|
||||
],
|
||||
featured: [
|
||||
{ id: 'all', title: 'All' },
|
||||
{ id: 'true', title: 'Yes' },
|
||||
{ id: 'false', title: 'No' },
|
||||
],
|
||||
};
|
||||
|
||||
export function ManageMediaFilters(props) {
|
||||
const [isHidden, setIsHidden] = useState(props.hidden);
|
||||
|
||||
const [state, setState] = useState('all');
|
||||
const [mediaType, setMediaType] = useState('all');
|
||||
const [encodingStatus, setEncodingStatus] = useState('all');
|
||||
const [isFeatured, setIsFeatured] = useState('all');
|
||||
const [isReviewed, setIsReviewed] = useState('all');
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const innerContainerRef = useRef(null);
|
||||
|
||||
function onWindowResize() {
|
||||
if (!isHidden) {
|
||||
containerRef.current.style.height = 24 + innerContainerRef.current.offsetHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onFilterSelect(ev) {
|
||||
const args = {
|
||||
state: state,
|
||||
media_type: mediaType,
|
||||
encoding_status: encodingStatus,
|
||||
featured: isFeatured,
|
||||
is_reviewed: isReviewed,
|
||||
};
|
||||
|
||||
switch (ev.currentTarget.getAttribute('filter')) {
|
||||
case 'state':
|
||||
args.state = ev.currentTarget.getAttribute('value');
|
||||
props.onFiltersUpdate(args);
|
||||
setState(args.state);
|
||||
break;
|
||||
case 'media_type':
|
||||
args.media_type = ev.currentTarget.getAttribute('value');
|
||||
props.onFiltersUpdate(args);
|
||||
setMediaType(args.media_type);
|
||||
break;
|
||||
case 'encoding_status':
|
||||
args.encoding_status = ev.currentTarget.getAttribute('value');
|
||||
props.onFiltersUpdate(args);
|
||||
setEncodingStatus(args.encoding_status);
|
||||
break;
|
||||
case 'featured':
|
||||
args.featured = ev.currentTarget.getAttribute('value');
|
||||
props.onFiltersUpdate(args);
|
||||
setIsFeatured(args.featured);
|
||||
break;
|
||||
case 'reviewed':
|
||||
args.is_reviewed = ev.currentTarget.getAttribute('value');
|
||||
props.onFiltersUpdate(args);
|
||||
setIsReviewed(args.is_reviewed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsHidden(props.hidden);
|
||||
onWindowResize();
|
||||
}, [props.hidden]);
|
||||
|
||||
useEffect(() => {
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
return () => PageStore.removeListener('window_resize', onWindowResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={'mi-filters-row' + (isHidden ? ' hidden' : '')}>
|
||||
<div ref={innerContainerRef} className="mi-filters-row-inner">
|
||||
<div className="mi-filter">
|
||||
<div className="mi-filter-title">STATE</div>
|
||||
<div className="mi-filter-options">
|
||||
<FilterOptions id={'state'} options={filters.state} selected={state} onSelect={onFilterSelect} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mi-filter">
|
||||
<div className="mi-filter-title">MEDIA TYPE</div>
|
||||
<div className="mi-filter-options">
|
||||
<FilterOptions
|
||||
id={'media_type'}
|
||||
options={filters.media_type}
|
||||
selected={mediaType}
|
||||
onSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mi-filter">
|
||||
<div className="mi-filter-title">ENCODING STATUS</div>
|
||||
<div className="mi-filter-options">
|
||||
<FilterOptions
|
||||
id={'encoding_status'}
|
||||
options={filters.encoding_status}
|
||||
selected={encodingStatus}
|
||||
onSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mi-filter">
|
||||
<div className="mi-filter-title">REVIEWED</div>
|
||||
<div className="mi-filter-options">
|
||||
<FilterOptions id={'reviewed'} options={filters.reviewed} selected={isReviewed} onSelect={onFilterSelect} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mi-filter">
|
||||
<div className="mi-filter-title">FEATURED</div>
|
||||
<div className="mi-filter-options">
|
||||
<FilterOptions id={'featured'} options={filters.featured} selected={isFeatured} onSelect={onFilterSelect} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageMediaFilters.propTypes = {
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
ManageMediaFilters.defaultProps = {
|
||||
hidden: false,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { FilterOptions } from '../_shared';
|
||||
|
||||
import './ManageItemList-filters.scss';
|
||||
|
||||
const filters = {
|
||||
role: [
|
||||
{ id: 'all', title: 'All' },
|
||||
{ id: 'editor', title: 'Editor' },
|
||||
{ id: 'manager', title: 'Manager' },
|
||||
],
|
||||
};
|
||||
|
||||
export function ManageUsersFilters(props) {
|
||||
const [isHidden, setIsHidden] = useState(props.hidden);
|
||||
|
||||
const [role, setFilterRole] = useState('all');
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const innerContainerRef = useRef(null);
|
||||
|
||||
function onWindowResize() {
|
||||
if (!isHidden) {
|
||||
containerRef.current.style.height = 24 + innerContainerRef.current.offsetHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onFilterSelect(ev) {
|
||||
const args = {
|
||||
role: role,
|
||||
};
|
||||
|
||||
switch (ev.currentTarget.getAttribute('filter')) {
|
||||
case 'role':
|
||||
args.role = ev.currentTarget.getAttribute('value');
|
||||
props.onFiltersUpdate(args);
|
||||
setFilterRole(args.role);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsHidden(props.hidden);
|
||||
onWindowResize();
|
||||
}, [props.hidden]);
|
||||
|
||||
useEffect(() => {
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
return () => PageStore.removeListener('window_resize', onWindowResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={'mi-filters-row' + (isHidden ? ' hidden' : '')}>
|
||||
<div ref={innerContainerRef} className="mi-filters-row-inner">
|
||||
<div className="mi-filter">
|
||||
<div className="mi-filter-title">ROLE</div>
|
||||
<div className="mi-filter-options">
|
||||
<FilterOptions id={'role'} options={filters.role} selected={role} onSelect={onFilterSelect} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ManageUsersFilters.propTypes = {
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
ManageUsersFilters.defaultProps = {
|
||||
hidden: false,
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TextsContext } from '../../utils/contexts/';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import { formatViewsNumber } from '../../utils/helpers/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, MaterialIcon } from '../_shared/';
|
||||
|
||||
export function MediaDislikeIcon() {
|
||||
const [dislikedMedia, setDislikedMedia] = useState(MediaPageStore.get('user-liked-media'));
|
||||
const [dislikesCounter, setDislikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-likes'), false));
|
||||
|
||||
function updateStateValues() {
|
||||
setDislikedMedia(MediaPageStore.get('user-disliked-media'));
|
||||
setDislikesCounter(formatViewsNumber(MediaPageStore.get('media-dislikes'), false));
|
||||
}
|
||||
|
||||
function onCompleteMediaDislike() {
|
||||
updateStateValues();
|
||||
PageActions.addNotification(TextsContext._currentValue.messages.addToDisliked, 'mediaDislike');
|
||||
}
|
||||
|
||||
function onCompleteMediaDislikeCancel() {
|
||||
updateStateValues();
|
||||
PageActions.addNotification(TextsContext._currentValue.messages.removeFromDisliked, 'cancelMediaDislike');
|
||||
}
|
||||
|
||||
function onFailMediaDislikeRequest() {
|
||||
PageActions.addNotification('Action failed', 'mediaDislikeRequestFail');
|
||||
}
|
||||
|
||||
function toggleDislike(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
MediaPageActions[dislikedMedia ? 'undislikeMedia' : 'dislikeMedia']();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
MediaPageStore.on('disliked_media', onCompleteMediaDislike);
|
||||
MediaPageStore.on('undisliked_media', onCompleteMediaDislikeCancel);
|
||||
MediaPageStore.on('disliked_media_failed_request', onFailMediaDislikeRequest);
|
||||
return () => {
|
||||
MediaPageStore.removeListener('disliked_media', onCompleteMediaDislike);
|
||||
MediaPageStore.removeListener('undisliked_media', onCompleteMediaDislikeCancel);
|
||||
MediaPageStore.removeListener('disliked_media_failed_request', onFailMediaDislikeRequest);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="like">
|
||||
<button onClick={toggleDislike}>
|
||||
<CircleIconButton type="span">
|
||||
<MaterialIcon type="thumb_down" />
|
||||
</CircleIconButton>
|
||||
<span className="dislikes-counter">{dislikesCounter}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TextsContext } from '../../utils/contexts/';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import { formatViewsNumber } from '../../utils/helpers/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, MaterialIcon } from '../_shared/';
|
||||
|
||||
export function MediaLikeIcon() {
|
||||
const [likedMedia, setLikedMedia] = useState(MediaPageStore.get('user-liked-media'));
|
||||
const [likesCounter, setLikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-likes'), false));
|
||||
|
||||
function updateStateValues() {
|
||||
setLikedMedia(MediaPageStore.get('user-liked-media'));
|
||||
setLikesCounter(formatViewsNumber(MediaPageStore.get('media-likes'), false));
|
||||
}
|
||||
|
||||
function onCompleteMediaLike() {
|
||||
updateStateValues();
|
||||
PageActions.addNotification(TextsContext._currentValue.addToLiked, 'likedMedia');
|
||||
}
|
||||
|
||||
function onCompleteMediaLikeCancel() {
|
||||
updateStateValues();
|
||||
PageActions.addNotification(TextsContext._currentValue.removeFromLiked, 'unlikedMedia');
|
||||
}
|
||||
|
||||
function onFailMediaLikeRequest() {
|
||||
PageActions.addNotification('Action failed', 'likedMediaRequestFail');
|
||||
}
|
||||
|
||||
function toggleLike(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
MediaPageActions[likedMedia ? 'unlikeMedia' : 'likeMedia']();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
MediaPageStore.on('liked_media', onCompleteMediaLike);
|
||||
MediaPageStore.on('unliked_media', onCompleteMediaLikeCancel);
|
||||
MediaPageStore.on('liked_media_failed_request', onFailMediaLikeRequest);
|
||||
return () => {
|
||||
MediaPageStore.removeListener('liked_media', onCompleteMediaLike);
|
||||
MediaPageStore.removeListener('unliked_media', onCompleteMediaLikeCancel);
|
||||
MediaPageStore.removeListener('liked_media_failed_request', onFailMediaLikeRequest);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="like">
|
||||
<button onClick={toggleLike}>
|
||||
<CircleIconButton type="span">
|
||||
<MaterialIcon type="thumb_up" />
|
||||
</CircleIconButton>
|
||||
<span className="likes-counter">{likesCounter}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { formatInnerLink } from '../../utils/helpers/';
|
||||
import { usePopup, useUser } from '../../utils/hooks/';
|
||||
import { SiteContext } from '../../utils/contexts/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import { CircleIconButton, MaterialIcon, NavigationContentApp, NavigationMenuList, PopupMain } from '../_shared/';
|
||||
import { ReportForm } from '../report-form/ReportForm';
|
||||
|
||||
function downloadOptions(mediaData, allowDownload) {
|
||||
const site = SiteContext._currentValue;
|
||||
|
||||
const encodingsInfo = mediaData.encodings_info;
|
||||
|
||||
const options = {};
|
||||
|
||||
let k, g;
|
||||
|
||||
for (k in encodingsInfo) {
|
||||
if (encodingsInfo.hasOwnProperty(k)) {
|
||||
if (Object.keys(encodingsInfo[k]).length) {
|
||||
for (g in encodingsInfo[k]) {
|
||||
if (encodingsInfo[k].hasOwnProperty(g)) {
|
||||
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress) {
|
||||
options[encodingsInfo[k][g].title] = {
|
||||
text: k + ' - ' + g.toUpperCase() + ' (' + encodingsInfo[k][g].size + ')',
|
||||
link: formatInnerLink(encodingsInfo[k][g].url, site.url),
|
||||
linkAttr: {
|
||||
target: '_blank',
|
||||
download: mediaData.title + '_' + k + '_' + g.toUpperCase(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options.original_media_url = {
|
||||
text: 'Original file (' + mediaData.size + ')',
|
||||
link: formatInnerLink(mediaData.original_media_url, site.url),
|
||||
linkAttr: {
|
||||
target: '_blank',
|
||||
download: mediaData.title,
|
||||
},
|
||||
};
|
||||
|
||||
return Object.values(options);
|
||||
}
|
||||
|
||||
function optionsItems(userCan, mediaData, allowDownload, downloadLink, mediaReported) {
|
||||
|
||||
const items = [];
|
||||
|
||||
const mediaType = mediaData.media_type;
|
||||
const mediaIsVideo = 'video' === mediaType;
|
||||
const mediaReportedTimes = mediaData.reported_times;
|
||||
|
||||
if (allowDownload && userCan.downloadMedia) {
|
||||
if (!mediaIsVideo) {
|
||||
if (downloadLink) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
link: downloadLink,
|
||||
text: 'Download',
|
||||
icon: 'arrow_downward',
|
||||
itemAttr: {
|
||||
className: 'visible-only-in-small',
|
||||
},
|
||||
linkAttr: {
|
||||
target: '_blank',
|
||||
download: mediaData.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
itemType: 'open-subpage',
|
||||
text: 'Download',
|
||||
icon: 'arrow_downward',
|
||||
itemAttr: {
|
||||
className: 'visible-only-in-small',
|
||||
},
|
||||
buttonAttr: {
|
||||
className: 'change-page',
|
||||
'data-page-id': 'videoDownloadOptions',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaIsVideo && userCan.editMedia) {
|
||||
items.push({
|
||||
itemType: 'open-subpage',
|
||||
text: 'Status info',
|
||||
icon: 'info',
|
||||
buttonAttr: {
|
||||
className: 'change-page',
|
||||
'data-page-id': 'mediaStatusInfo',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (userCan.reportMedia) {
|
||||
if (mediaReported) {
|
||||
items.push({
|
||||
itemType: 'div',
|
||||
text: 'Reported',
|
||||
icon: 'flag',
|
||||
divAttr: {
|
||||
className: 'reported-label loggedin-media-reported',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
itemType: 'open-subpage',
|
||||
text: 'Report',
|
||||
icon: 'flag',
|
||||
buttonAttr: {
|
||||
className: 'change-page' + (mediaReportedTimes ? ' loggedin-media-reported' : ''),
|
||||
'data-page-id': 'loggedInReportMedia',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function getPopupPages(userCan, mediaData, allowDownload, downloadLink, mediaReported, submitReportForm, cancelReportForm) {
|
||||
|
||||
const mediaUrl = mediaData.url;
|
||||
const mediaType = mediaData.media_type;
|
||||
const mediaState = mediaData.state || 'N/A';
|
||||
const mediaEncodingStatus = mediaData.encoding_status || 'N/A';
|
||||
const mediaReportedTimes = mediaData.reported_times;
|
||||
const mediaIsReviewed = mediaData.is_reviewed;
|
||||
|
||||
const mediaIsVideo = 'video' === mediaType;
|
||||
|
||||
const navItems = optionsItems(userCan, mediaData, allowDownload, downloadLink, mediaReported);
|
||||
|
||||
const pages = {};
|
||||
|
||||
if (navItems.length) {
|
||||
pages.main = (
|
||||
<div className="main-options">
|
||||
<PopupMain>
|
||||
<NavigationMenuList items={navItems} />
|
||||
</PopupMain>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (userCan.reportMedia) {
|
||||
pages.loggedInReportMedia = mediaReported ? null : (
|
||||
<div className="popup-fullscreen">
|
||||
<PopupMain>
|
||||
<span className="popup-fullscreen-overlay"></span>
|
||||
<div>
|
||||
<ReportForm mediaUrl={mediaUrl} submitReportForm={submitReportForm} cancelReportForm={cancelReportForm} />
|
||||
</div>
|
||||
</PopupMain>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (userCan.editMedia) {
|
||||
pages.mediaStatusInfo = (
|
||||
<div className="main-options">
|
||||
<PopupMain>
|
||||
<ul className="media-status-info">
|
||||
<li>
|
||||
Media type: <span>{mediaType}</span>
|
||||
</li>
|
||||
<li>
|
||||
State: <span>{mediaState}</span>
|
||||
</li>
|
||||
<li>
|
||||
Review state: <span>{mediaIsReviewed ? 'Is reviewed' : 'Pending review'}</span>
|
||||
</li>
|
||||
{mediaIsVideo ? (
|
||||
<li>
|
||||
Encoding Status: <span>{mediaEncodingStatus}</span>
|
||||
</li>
|
||||
) : null}
|
||||
{mediaReportedTimes ? (
|
||||
<li className="reports">
|
||||
Reports: <span>{mediaReportedTimes}</span>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</PopupMain>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (allowDownload && userCan.downloadMedia && mediaIsVideo) {
|
||||
pages.videoDownloadOptions = (
|
||||
<div className="video-download-options">
|
||||
<PopupMain>
|
||||
<NavigationMenuList items={downloadOptions(mediaData, allowDownload)} />
|
||||
</PopupMain>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
const defaultContainerClassname = 'more-options active-options';
|
||||
|
||||
export function MediaMoreOptionsIcon(props) {
|
||||
const { userCan } = useUser();
|
||||
|
||||
const site = SiteContext._currentValue;
|
||||
|
||||
const downloadLink = formatInnerLink(MediaPageStore.get('media-original-url'), site.url);
|
||||
const mediaData = MediaPageStore.get('media-data');
|
||||
const mediaIsVideo = 'video' === mediaData.media_type;
|
||||
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [reported, setReported] = useState(false);
|
||||
const [popupPages, setPopupPages] = useState({});
|
||||
const [popupCurrentPage, setPopupCurrentPage] = useState('main');
|
||||
const [containerClassname, setContainerClassname] = useState(defaultContainerClassname);
|
||||
|
||||
function submitReportForm(reportDescription) {
|
||||
MediaPageActions.reportMedia(reportDescription);
|
||||
}
|
||||
function cancelReportFormSubmission() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
function onPopupPageChange(newPage) {
|
||||
setPopupCurrentPage(newPage);
|
||||
}
|
||||
function onPopupHide() {
|
||||
setPopupCurrentPage('main');
|
||||
}
|
||||
|
||||
function onCompleteMediaReport() {
|
||||
popupContentRef.current.tryToHide();
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(function () {
|
||||
PageActions.addNotification('Media Reported', 'reportedMedia');
|
||||
setReported(true);
|
||||
MediaPageStore.removeListener('reported_media', onCompleteMediaReport);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!reported) {
|
||||
if (visible) {
|
||||
MediaPageStore.on('reported_media', onCompleteMediaReport);
|
||||
} else {
|
||||
MediaPageStore.removeListener('reported_media', onCompleteMediaReport);
|
||||
}
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(Object.keys(popupPages).length && props.allowDownload && userCan.downloadMedia);
|
||||
}, [popupPages]);
|
||||
|
||||
useEffect(() => {
|
||||
let classname = defaultContainerClassname;
|
||||
if (props.allowDownload && userCan.downloadMedia && 'videoDownloadOptions' === popupCurrentPage) {
|
||||
classname += ' video-downloads';
|
||||
}
|
||||
if (
|
||||
1 === Object.keys(popupPages).length &&
|
||||
props.allowDownload &&
|
||||
userCan.downloadMedia &&
|
||||
(mediaIsVideo || downloadLink)
|
||||
) {
|
||||
classname += ' visible-only-in-small';
|
||||
}
|
||||
setContainerClassname(classname);
|
||||
}, [popupCurrentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
setPopupPages(
|
||||
getPopupPages(
|
||||
userCan,
|
||||
mediaData,
|
||||
props.allowDownload,
|
||||
downloadLink,
|
||||
reported,
|
||||
submitReportForm,
|
||||
cancelReportFormSubmission
|
||||
)
|
||||
);
|
||||
}, [reported]);
|
||||
|
||||
useEffect(() => {
|
||||
setPopupPages(
|
||||
getPopupPages(
|
||||
userCan,
|
||||
mediaData,
|
||||
props.allowDownload,
|
||||
downloadLink,
|
||||
reported,
|
||||
submitReportForm,
|
||||
cancelReportFormSubmission
|
||||
)
|
||||
);
|
||||
return () => {
|
||||
if (visible && !reported) {
|
||||
MediaPageStore.removeListener('reported_media', onCompleteMediaReport);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !visible ? null : (
|
||||
<div className={containerClassname}>
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<span>
|
||||
<CircleIconButton type="button">
|
||||
<MaterialIcon type="more_horiz" />
|
||||
</CircleIconButton>
|
||||
</span>
|
||||
</PopupTrigger>
|
||||
|
||||
<div className={'nav-page-' + popupCurrentPage}>
|
||||
<PopupContent contentRef={popupContentRef} hideCallback={onPopupHide}>
|
||||
<NavigationContentApp
|
||||
pageChangeCallback={onPopupPageChange}
|
||||
initPage={popupCurrentPage}
|
||||
focusFirstItemOnPageChange={false}
|
||||
pages={popupPages}
|
||||
pageChangeSelector={'.change-page'}
|
||||
pageIdSelectorAttr={'data-page-id'}
|
||||
/>
|
||||
</PopupContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MediaMoreOptionsIcon.propTypes = {
|
||||
allowDownload: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
MediaMoreOptionsIcon.defaultProps = {
|
||||
allowDownload: false,
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
import { usePopup } from '../../utils/hooks/';
|
||||
import { CircleIconButton, MaterialIcon, NavigationContentApp, PopupMain } from '../_shared/';
|
||||
import { PlaylistsSelection } from '../playlists-selection/PlaylistsSelection';
|
||||
|
||||
function mediaSavePopupPages(onTriggerPopupClose) {
|
||||
return {
|
||||
selectPlaylist: (
|
||||
<div className="popup-fullscreen">
|
||||
<PopupMain>
|
||||
<span className="popup-fullscreen-overlay"></span>
|
||||
<PlaylistsSelection triggerPopupClose={onTriggerPopupClose} />
|
||||
</PopupMain>
|
||||
</div>
|
||||
),
|
||||
createPlaylist: (
|
||||
<div className="popup-fullscreen">
|
||||
<PopupMain>
|
||||
<span className="popup-fullscreen-overlay"></span>
|
||||
</PopupMain>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function MediaSaveButton(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
const [popupCurrentPage, setPopupCurrentPage] = useState('selectPlaylist');
|
||||
|
||||
function triggerPopupClose() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="save">
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button>
|
||||
<CircleIconButton type="span">
|
||||
<MaterialIcon type="playlist_add" />
|
||||
</CircleIconButton>
|
||||
<span>SAVE</span>
|
||||
</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef}>
|
||||
<NavigationContentApp
|
||||
initPage={popupCurrentPage}
|
||||
pageChangeSelector={'.change-page'}
|
||||
pageIdSelectorAttr={'data-page-id'}
|
||||
pages={mediaSavePopupPages(triggerPopupClose)}
|
||||
focusFirstItemOnPageChange={false}
|
||||
pageChangeCallback={setPopupCurrentPage}
|
||||
/>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useState } from 'react';
|
||||
import { usePopup } from '../../utils/hooks/';
|
||||
import { CircleIconButton, MaterialIcon, NavigationContentApp, PopupMain } from '../_shared/';
|
||||
import { MediaShareEmbed } from './MediaShareEmbed';
|
||||
import { MediaShareOptions } from './MediaShareOptions';
|
||||
|
||||
function mediaSharePopupPages() {
|
||||
return {
|
||||
shareOptions: (
|
||||
<div className="popup-fullscreen">
|
||||
<PopupMain>
|
||||
<span className="popup-fullscreen-overlay"></span>
|
||||
<MediaShareOptions />
|
||||
</PopupMain>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function videoSharePopupPages(onTriggerPopupClose) {
|
||||
return {
|
||||
...mediaSharePopupPages(),
|
||||
shareEmbed: (
|
||||
<div className="popup-fullscreen share-embed-popup">
|
||||
<PopupMain>
|
||||
<span className="popup-fullscreen-overlay"></span>
|
||||
<MediaShareEmbed triggerPopupClose={onTriggerPopupClose} />
|
||||
</PopupMain>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function MediaShareButton(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
const [popupCurrentPage, setPopupCurrentPage] = useState('shareOptions');
|
||||
|
||||
function triggerPopupClose() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
|
||||
function onPopupPageChange(newPage) {
|
||||
setPopupCurrentPage(newPage);
|
||||
}
|
||||
function onPopupHide() {
|
||||
setPopupCurrentPage('shareOptions');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="share">
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button>
|
||||
<CircleIconButton type="span">
|
||||
<MaterialIcon type="share" />
|
||||
</CircleIconButton>
|
||||
<span>SHARE</span>
|
||||
</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef} hideCallback={onPopupHide}>
|
||||
<NavigationContentApp
|
||||
initPage={popupCurrentPage}
|
||||
pageChangeSelector={'.change-page'}
|
||||
pageIdSelectorAttr={'data-page-id'}
|
||||
pages={props.isVideo ? videoSharePopupPages(triggerPopupClose) : mediaSharePopupPages()}
|
||||
focusFirstItemOnPageChange={false}
|
||||
pageChangeCallback={onPopupPageChange}
|
||||
/>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import React, { useRef, useState, useEffect, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LinksContext } from '../../utils/contexts/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/';
|
||||
import VideoViewer from '../media-viewer/VideoViewer';
|
||||
|
||||
export function MediaShareEmbed(props) {
|
||||
const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions;
|
||||
|
||||
const links = useContext(LinksContext);
|
||||
|
||||
const aspectRatioValueRef = useRef(null);
|
||||
const onRightRef = useRef(null);
|
||||
const onRightTopRef = useRef(null);
|
||||
const onRightMiddleRef = useRef(null);
|
||||
const onRightBottomRef = useRef(null);
|
||||
|
||||
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56);
|
||||
const [keepAspectRatio, setKeepAspectRatio] = useState(false);
|
||||
const [aspectRatio, setAspectRatio] = useState('16:9');
|
||||
const [embedWidthValue, setEmbedWidthValue] = useState(embedVideoDimensions.width);
|
||||
const [embedWidthUnit, setEmbedWidthUnit] = useState(embedVideoDimensions.widthUnit);
|
||||
const [embedHeightValue, setEmbedHeightValue] = useState(embedVideoDimensions.height);
|
||||
const [embedHeightUnit, setEmbedHeightUnit] = useState(embedVideoDimensions.heightUnit);
|
||||
const [rightMiddlePositionTop, setRightMiddlePositionTop] = useState(60);
|
||||
const [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60);
|
||||
const [unitOptions, setUnitOptions] = useState([
|
||||
{ key: 'px', label: 'px' },
|
||||
{ key: 'percent', label: '%' },
|
||||
]);
|
||||
|
||||
function onClickCopyMediaLink() {
|
||||
MediaPageActions.copyEmbedMediaCode(onRightMiddleRef.current.querySelector('textarea'));
|
||||
}
|
||||
|
||||
function onClickEmbedShareExit() {
|
||||
if (void 0 !== props.triggerPopupClose) {
|
||||
props.triggerPopupClose();
|
||||
}
|
||||
}
|
||||
|
||||
function onEmbedWidthValueChange(newVal) {
|
||||
newVal = '' === newVal ? 0 : newVal;
|
||||
|
||||
const arr = aspectRatio.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
|
||||
setEmbedWidthValue(newVal);
|
||||
setEmbedHeightValue(keepAspectRatio ? parseInt((newVal * y) / x, 10) : embedHeightValue);
|
||||
}
|
||||
|
||||
function onEmbedWidthUnitChange(newVal) {
|
||||
setEmbedWidthUnit(newVal);
|
||||
}
|
||||
|
||||
function onEmbedHeightValueChange(newVal) {
|
||||
newVal = '' === newVal ? 0 : newVal;
|
||||
|
||||
const arr = aspectRatio.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
|
||||
setEmbedHeightValue(newVal);
|
||||
setEmbedWidthValue(keepAspectRatio ? parseInt((newVal * x) / y, 10) : embedWidthValue);
|
||||
}
|
||||
|
||||
function onEmbedHeightUnitChange(newVal) {
|
||||
setEmbedHeightUnit(newVal);
|
||||
}
|
||||
|
||||
function onKeepAspectRatioChange() {
|
||||
const newVal = !keepAspectRatio;
|
||||
|
||||
const arr = aspectRatio.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
|
||||
setKeepAspectRatio(newVal);
|
||||
setEmbedWidthUnit(newVal ? 'px' : embedWidthUnit);
|
||||
setEmbedHeightUnit(newVal ? 'px' : embedHeightUnit);
|
||||
setEmbedHeightValue(newVal ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
|
||||
setUnitOptions(
|
||||
newVal
|
||||
? [{ key: 'px', label: 'px' }]
|
||||
: [
|
||||
{ key: 'px', label: 'px' },
|
||||
{ key: 'percent', label: '%' },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function onAspectRatioChange() {
|
||||
const newVal = aspectRatioValueRef.current.value;
|
||||
|
||||
const arr = newVal.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
|
||||
setAspectRatio(newVal);
|
||||
setEmbedHeightValue(keepAspectRatio ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
setMaxHeight(window.innerHeight - 144 + 56);
|
||||
setRightMiddlePositionTop(onRightTopRef.current.offsetHeight);
|
||||
setRightMiddlePositionBottom(onRightBottomRef.current.offsetHeight);
|
||||
}
|
||||
|
||||
function onCompleteCopyMediaLink() {
|
||||
setTimeout(function () {
|
||||
PageActions.addNotification('Embed media code copied to clipboard', 'clipboardEmbedMediaCodeCopy');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMaxHeight(window.innerHeight - 144 + 56);
|
||||
setRightMiddlePositionTop(onRightTopRef.current.offsetHeight);
|
||||
setRightMiddlePositionBottom(onRightBottomRef.current.offsetHeight);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
MediaPageStore.on('copied_embed_media_code', onCompleteCopyMediaLink);
|
||||
return () => {
|
||||
PageStore.removeListener('window_resize', onWindowResize);
|
||||
MediaPageStore.removeListener('copied_embed_media_code', onCompleteCopyMediaLink);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}>
|
||||
<div className="share-embed-inner">
|
||||
<div className="on-left">
|
||||
<div className="media-embed-wrap">
|
||||
<VideoViewer data={MediaPageStore.get('media-data')} inEmbed={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={onRightRef} className="on-right">
|
||||
<div ref={onRightTopRef} className="on-right-top">
|
||||
<div className="on-right-top-inner">
|
||||
<span className="ttl">Embed Video</span>
|
||||
<CircleIconButton type="button" onClick={onClickEmbedShareExit}>
|
||||
<MaterialIcon type="close" />
|
||||
</CircleIconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={onRightMiddleRef}
|
||||
className="on-right-middle"
|
||||
style={{ top: rightMiddlePositionTop + 'px', bottom: rightMiddlePositionBottom + 'px' }}
|
||||
>
|
||||
<textarea
|
||||
readOnly
|
||||
value={
|
||||
'<iframe width="' +
|
||||
('percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue) +
|
||||
'" height="' +
|
||||
('percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue) +
|
||||
'" src="' +
|
||||
links.embed +
|
||||
MediaPageStore.get('media-id') +
|
||||
'" frameborder="0" allowfullscreen></iframe>'
|
||||
}
|
||||
></textarea>
|
||||
|
||||
<div className="iframe-config">
|
||||
<div className="iframe-config-options-title">Embed options</div>
|
||||
|
||||
<div className="iframe-config-option">
|
||||
{/*<div className="option-title">
|
||||
<span>Dimensions</span>
|
||||
</div>*/}
|
||||
|
||||
<div className="option-content">
|
||||
<div className="ratio-options">
|
||||
<div className="options-group">
|
||||
<label style={{ minHeight: '36px' }}>
|
||||
<input type="checkbox" checked={keepAspectRatio} onChange={onKeepAspectRatioChange} />
|
||||
Keep aspect ratio
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!keepAspectRatio ? null : (
|
||||
<div className="options-group">
|
||||
<select ref={aspectRatioValueRef} onChange={onAspectRatioChange} value={aspectRatio}>
|
||||
<optgroup label="Horizontal orientation">
|
||||
<option value="16:9">16:9</option>
|
||||
<option value="4:3">4:3</option>
|
||||
<option value="3:2">3:2</option>
|
||||
</optgroup>
|
||||
<optgroup label="Vertical orientation">
|
||||
<option value="9:16">9:16</option>
|
||||
<option value="3:4">3:4</option>
|
||||
<option value="2:3">2:3</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedWidthValueChange}
|
||||
unitCallback={onEmbedWidthUnitChange}
|
||||
label={'Width'}
|
||||
defaultValue={parseInt(embedWidthValue, 10)}
|
||||
defaultUnit={embedWidthUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedHeightValueChange}
|
||||
unitCallback={onEmbedHeightUnitChange}
|
||||
label={'Height'}
|
||||
defaultValue={parseInt(embedHeightValue, 10)}
|
||||
defaultUnit={embedHeightUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={onRightBottomRef} className="on-right-bottom">
|
||||
<button onClick={onClickCopyMediaLink}>COPY</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MediaShareEmbed.propTypes = {
|
||||
triggerPopupClose: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,282 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { ShareOptionsContext } from '../../utils/contexts/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import ItemsInlineSlider from '../item-list/includes/itemLists/ItemsInlineSlider';
|
||||
import { CircleIconButton } from '../_shared/';
|
||||
|
||||
function shareOptionsList() {
|
||||
const socialMedia = ShareOptionsContext._currentValue;
|
||||
const mediaUrl = MediaPageStore.get('media-url');
|
||||
const mediaTitle = MediaPageStore.get('media-data').title;
|
||||
|
||||
const ret = {};
|
||||
|
||||
let i = 0;
|
||||
|
||||
while (i < socialMedia.length) {
|
||||
switch (socialMedia[i]) {
|
||||
case 'embed':
|
||||
if ('video' === MediaPageStore.get('media-data').media_type) {
|
||||
ret[socialMedia[i]] = {};
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'Email',
|
||||
shareUrl: 'mailto:?body=' + mediaUrl,
|
||||
};
|
||||
break;
|
||||
case 'fb':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'Facebook',
|
||||
shareUrl: 'https://www.facebook.com/sharer.php?u=' + mediaUrl,
|
||||
};
|
||||
break;
|
||||
case 'tw':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'Twitter',
|
||||
shareUrl: 'https://twitter.com/intent/tweet?url=' + mediaUrl,
|
||||
};
|
||||
break;
|
||||
case 'reddit':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'reddit',
|
||||
shareUrl: 'https://reddit.com/submit?url=' + mediaUrl + '&title=' + mediaTitle,
|
||||
};
|
||||
break;
|
||||
case 'tumblr':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'Tumblr',
|
||||
shareUrl: 'https://www.tumblr.com/widgets/share/tool?canonicalUrl=' + mediaUrl + '&title=' + mediaTitle,
|
||||
};
|
||||
break;
|
||||
case 'pinterest':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'Pinterest',
|
||||
shareUrl: 'http://pinterest.com/pin/create/link/?url=' + mediaUrl,
|
||||
};
|
||||
break;
|
||||
case 'vk':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'ВКонтакте',
|
||||
shareUrl: 'http://vk.com/share.php?url=' + mediaUrl + '&title=' + mediaTitle,
|
||||
};
|
||||
break;
|
||||
case 'linkedin':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'LinkedIn',
|
||||
shareUrl: 'https://www.linkedin.com/shareArticle?mini=true&url=' + mediaUrl,
|
||||
};
|
||||
break;
|
||||
case 'mix':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'Mix',
|
||||
shareUrl: 'https://mix.com/add?url=' + mediaUrl,
|
||||
};
|
||||
break;
|
||||
case 'whatsapp':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'WhatsApp',
|
||||
shareUrl: 'whatsapp://send?text=' + mediaUrl,
|
||||
};
|
||||
break;
|
||||
case 'telegram':
|
||||
ret[socialMedia[i]] = {
|
||||
title: 'Telegram',
|
||||
shareUrl: 'https://t.me/share/url?url=' + mediaUrl + '&text=' + mediaTitle,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function ShareOptions() {
|
||||
const shareOptions = shareOptionsList();
|
||||
|
||||
const compList = [];
|
||||
|
||||
for (let k in shareOptions) {
|
||||
if (shareOptions.hasOwnProperty(k)) {
|
||||
if (k === 'embed') {
|
||||
compList.push(
|
||||
<div key={'share-' + k} className={'sh-option share-' + k + '-opt'}>
|
||||
<button className="sh-option change-page" data-page-id="shareEmbed">
|
||||
<span>
|
||||
<i className="material-icons">code</i>
|
||||
</span>
|
||||
<span>Embed</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else if (k === 'whatsapp') {
|
||||
compList.push(
|
||||
<div key={'share-' + k} className={'sh-option share-' + k}>
|
||||
<a
|
||||
href={shareOptions[k].shareUrl}
|
||||
title=""
|
||||
target="_blank"
|
||||
data-action="share/whatsapp/share"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span></span>
|
||||
<span>{shareOptions[k].title}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else if (k === 'email') {
|
||||
compList.push(
|
||||
<div key="share-email" className="sh-option share-email">
|
||||
<a href={shareOptions[k].shareUrl} title="">
|
||||
<span>
|
||||
<i className="material-icons">email</i>
|
||||
</span>
|
||||
<span>{shareOptions[k].title}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
compList.push(
|
||||
<div key={'share-' + k} className={'sh-option share-' + k}>
|
||||
<a href={shareOptions[k].shareUrl} title="" target="_blank" rel="noreferrer">
|
||||
<span></span>
|
||||
<span>{shareOptions[k].title}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return compList;
|
||||
}
|
||||
|
||||
function NextSlideButton({ onClick }) {
|
||||
return (
|
||||
<span className="next-slide">
|
||||
<CircleIconButton buttonShadow={true} onClick={onClick}>
|
||||
<i className="material-icons">keyboard_arrow_right</i>
|
||||
</CircleIconButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousSlideButton({ onClick }) {
|
||||
return (
|
||||
<span className="previous-slide">
|
||||
<CircleIconButton buttonShadow={true} onClick={onClick}>
|
||||
<i className="material-icons">keyboard_arrow_left</i>
|
||||
</CircleIconButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function updateDimensions() {
|
||||
return {
|
||||
maxFormContentHeight: window.innerHeight - (56 + 4 * 24 + 44),
|
||||
maxPopupWidth: 518 > window.innerWidth - 2 * 40 ? window.innerWidth - 2 * 40 : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function MediaShareOptions(props) {
|
||||
const containerRef = useRef(null);
|
||||
const shareOptionsInnerRef = useRef(null);
|
||||
|
||||
const [inlineSlider, setInlineSlider] = useState(null);
|
||||
const [sliderButtonsVisible, setSliderButtonsVisible] = useState({ prev: false, next: false });
|
||||
|
||||
const [dimensions, setDimensions] = useState(updateDimensions());
|
||||
const [shareOptions] = useState(ShareOptions());
|
||||
|
||||
function onWindowResize() {
|
||||
setDimensions(updateDimensions());
|
||||
}
|
||||
|
||||
function onClickCopyMediaLink() {
|
||||
MediaPageActions.copyShareLink(containerRef.current.querySelector('.copy-field input'));
|
||||
}
|
||||
|
||||
function onCompleteCopyMediaLink() {
|
||||
// FIXME: Without delay throws conflict error [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(function () {
|
||||
PageActions.addNotification('Link copied to clipboard', 'clipboardLinkCopy');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function updateSlider() {
|
||||
inlineSlider.scrollToCurrentSlide();
|
||||
updateSliderButtonsView();
|
||||
}
|
||||
|
||||
function updateSliderButtonsView() {
|
||||
setSliderButtonsVisible({
|
||||
prev: inlineSlider.hasPreviousSlide(),
|
||||
next: inlineSlider.hasNextSlide(),
|
||||
});
|
||||
}
|
||||
|
||||
function nextSlide() {
|
||||
inlineSlider.nextSlide();
|
||||
updateSlider();
|
||||
}
|
||||
|
||||
function prevSlide() {
|
||||
inlineSlider.previousSlide();
|
||||
updateSlider();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setInlineSlider(new ItemsInlineSlider(shareOptionsInnerRef.current, '.sh-option'));
|
||||
}, [shareOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inlineSlider) {
|
||||
inlineSlider.updateDataStateOnResize(shareOptions.length, true, true);
|
||||
updateSlider();
|
||||
}
|
||||
}, [dimensions, inlineSlider]);
|
||||
|
||||
useEffect(() => {
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
MediaPageStore.on('copied_media_link', onCompleteCopyMediaLink);
|
||||
|
||||
return () => {
|
||||
PageStore.removeListener('window_resize', onWindowResize);
|
||||
MediaPageStore.removeListener('copied_media_link', onCompleteCopyMediaLink);
|
||||
setInlineSlider(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={null !== dimensions.maxPopupWidth ? { maxWidth: dimensions.maxPopupWidth + 'px' } : null}
|
||||
>
|
||||
<div
|
||||
className="scrollable-content"
|
||||
style={null !== dimensions.maxFormContentHeight ? { maxHeight: dimensions.maxFormContentHeight + 'px' } : null}
|
||||
>
|
||||
<div className="share-popup-title">Share media</div>
|
||||
{shareOptions.length ? (
|
||||
<div className="share-options">
|
||||
{sliderButtonsVisible.prev ? <PreviousSlideButton onClick={prevSlide} /> : null}
|
||||
<div ref={shareOptionsInnerRef} className="share-options-inner">
|
||||
{shareOptions}
|
||||
</div>
|
||||
{sliderButtonsVisible.next ? <NextSlideButton onClick={nextSlide} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="copy-field">
|
||||
<div>
|
||||
<input type="text" readOnly value={MediaPageStore.get('media-url')} />
|
||||
<button onClick={onClickCopyMediaLink}>COPY</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CircleIconButton, MaterialIcon } from '../_shared/';
|
||||
|
||||
export function OtherMediaDownloadLink(props) {
|
||||
return (
|
||||
<div className="download hidden-only-in-small">
|
||||
<a href={props.link} target="_blank" download={props.title} title="Download" rel="noreferrer">
|
||||
<CircleIconButton type="span">
|
||||
<MaterialIcon type="arrow_downward" />
|
||||
</CircleIconButton>
|
||||
<span>DOWNLOAD</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
OtherMediaDownloadLink.propTypes = {
|
||||
link: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useState } from 'react';
|
||||
import { usePopup } from '../../utils/hooks/';
|
||||
import { SiteContext } from '../../utils/contexts/';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import { formatInnerLink } from '../../utils/helpers/';
|
||||
import { CircleIconButton, MaterialIcon, NavigationContentApp, NavigationMenuList, PopupMain } from '../_shared/';
|
||||
|
||||
function downloadOptionsList() {
|
||||
const media_data = MediaPageStore.get('media-data');
|
||||
|
||||
const title = media_data.title;
|
||||
const encodings_info = media_data.encodings_info;
|
||||
|
||||
const optionsList = {};
|
||||
|
||||
let k, g;
|
||||
for (k in encodings_info) {
|
||||
if (encodings_info.hasOwnProperty(k)) {
|
||||
if (Object.keys(encodings_info[k]).length) {
|
||||
for (g in encodings_info[k]) {
|
||||
if (encodings_info[k].hasOwnProperty(g)) {
|
||||
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress) {
|
||||
optionsList[encodings_info[k][g].title] = {
|
||||
text: k + ' - ' + g.toUpperCase() + ' (' + encodings_info[k][g].size + ')',
|
||||
link: formatInnerLink(encodings_info[k][g].url, SiteContext._currentValue.url),
|
||||
linkAttr: {
|
||||
target: '_blank',
|
||||
download: media_data.title + '_' + k + '_' + g.toUpperCase(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
optionsList.original_media_url = {
|
||||
text: 'Original file (' + media_data.size + ')',
|
||||
link: formatInnerLink(media_data.original_media_url, SiteContext._currentValue.url),
|
||||
linkAttr: {
|
||||
target: '_blank',
|
||||
download: media_data.title,
|
||||
},
|
||||
};
|
||||
|
||||
return Object.values(optionsList);
|
||||
}
|
||||
|
||||
function downloadOptionsPages() {
|
||||
return {
|
||||
main: (
|
||||
<div className="main-options">
|
||||
<PopupMain>
|
||||
<NavigationMenuList items={downloadOptionsList()} />
|
||||
</PopupMain>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function VideoMediaDownloadLink(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
const [downloadOptionsCurrentPage, setDownloadOptionsCurrentPage] = useState('main');
|
||||
|
||||
return (
|
||||
<div className="video-downloads hidden-only-in-small">
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button>
|
||||
<CircleIconButton type="span">
|
||||
<MaterialIcon type="arrow_downward" />
|
||||
</CircleIconButton>
|
||||
<span>DOWNLOAD</span>
|
||||
</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<div className={'nav-page-' + downloadOptionsCurrentPage}>
|
||||
<PopupContent contentRef={popupContentRef}>
|
||||
<NavigationContentApp
|
||||
pageChangeCallback={null}
|
||||
initPage="main"
|
||||
focusFirstItemOnPageChange={false}
|
||||
pages={downloadOptionsPages()}
|
||||
pageChangeSelector={'.change-page'}
|
||||
pageIdSelectorAttr={'data-page-id'}
|
||||
/>
|
||||
</PopupContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
frontend/src/static/js/components/media-actions/index.js
Normal file
9
frontend/src/static/js/components/media-actions/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './MediaDislikeIcon.jsx';
|
||||
export * from './MediaLikeIcon.jsx';
|
||||
export * from './MediaMoreOptionsIcon.jsx';
|
||||
export * from './MediaSaveButton.jsx';
|
||||
export * from './MediaShareButton.jsx';
|
||||
export * from './MediaShareEmbed.jsx';
|
||||
export * from './MediaShareOptions.jsx';
|
||||
export * from './OtherMediaDownloadLink.jsx';
|
||||
export * from './VideoMediaDownloadLink.jsx';
|
||||
71
frontend/src/static/js/components/media-page/AutoPlay.jsx
Normal file
71
frontend/src/static/js/components/media-page/AutoPlay.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PageActions } from '../../utils/actions/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { ItemList } from '../item-list/ItemList';
|
||||
|
||||
function autoPlayMedia() {
|
||||
const dt = MediaPageStore.get('media-data');
|
||||
return dt && dt.related_media && dt.related_media.length ? dt.related_media[0] : null;
|
||||
}
|
||||
|
||||
export function AutoPlay(props) {
|
||||
const [media, setMedia] = useState(autoPlayMedia());
|
||||
const [enabledAutoPlay, setEnabledAutoPlay] = useState(PageStore.get('media-auto-play'));
|
||||
|
||||
function onKeyPress(ev) {
|
||||
if (0 === ev.keyCode) {
|
||||
PageActions.toggleMediaAutoPlay();
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateMediaAutoPlay() {
|
||||
setEnabledAutoPlay(PageStore.get('media-auto-play'));
|
||||
}
|
||||
|
||||
function onMediaDataLoad() {
|
||||
setMedia(autoPlayMedia());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
MediaPageStore.on('loaded_media_data', onMediaDataLoad);
|
||||
PageStore.on('switched_media_auto_play', onUpdateMediaAutoPlay);
|
||||
return () => {
|
||||
MediaPageStore.removeListener('loaded_media_data', onMediaDataLoad);
|
||||
PageStore.removeListener('switched_media_auto_play', onUpdateMediaAutoPlay);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !media ? null : (
|
||||
<div className="auto-play">
|
||||
<div className="auto-play-header">
|
||||
<div className="next-label">Up next</div>
|
||||
<div className="auto-play-option">
|
||||
<label className="checkbox-label right-selectbox" tabIndex={0} onKeyPress={onKeyPress}>
|
||||
AUTOPLAY
|
||||
<span className="checkbox-switcher-wrap">
|
||||
<span className="checkbox-switcher">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
checked={enabledAutoPlay}
|
||||
onChange={PageActions.toggleMediaAutoPlay}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<ItemList
|
||||
className="items-list-hor"
|
||||
items={[media]}
|
||||
pageItems={1}
|
||||
maxItems={1}
|
||||
singleLinkContent={true}
|
||||
horizontalItemsOrientation={true}
|
||||
hideDate={true}
|
||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||
hideAuthor={!PageStore.get('config-media-item').displayAuthor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1946
frontend/src/static/js/components/media-page/MediaPage.scss
Executable file
1946
frontend/src/static/js/components/media-page/MediaPage.scss
Executable file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { ItemList } from '../item-list/ItemList';
|
||||
|
||||
export function PlaylistPlaybackMedia(props) {
|
||||
return (
|
||||
<ItemList
|
||||
className={'items-list-hor'}
|
||||
pageItems={9999}
|
||||
maxItems={9999}
|
||||
items={props.items}
|
||||
hideDate={true}
|
||||
hideViews={true}
|
||||
hidePlaylistOrderNumber={false}
|
||||
horizontalItemsOrientation={true}
|
||||
inPlaylistView={true}
|
||||
singleLinkContent={true}
|
||||
playlistActiveItem={props.playlistActiveItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PlaylistPlaybackMedia.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
playlistActiveItem: PositiveIntegerOrZero,
|
||||
};
|
||||
|
||||
PlaylistPlaybackMedia.defaultProps = {
|
||||
playlistActiveItem: 1,
|
||||
};
|
||||
171
frontend/src/static/js/components/media-page/PlaylistView.js
Normal file
171
frontend/src/static/js/components/media-page/PlaylistView.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LinksContext } from '../../utils/contexts/';
|
||||
import { PlaylistViewStore } from '../../utils/stores/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { PageActions, PlaylistViewActions } from '../../utils/actions/';
|
||||
import { CircleIconButton } from '../_shared/';
|
||||
import { PlaylistPlaybackMedia } from './PlaylistPlaybackMedia';
|
||||
|
||||
import './PlaylistView.scss';
|
||||
|
||||
export default class PlaylistView extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
expanded: true,
|
||||
loopRepeat: PlaylistViewStore.get('enabled-loop'),
|
||||
shuffle: PlaylistViewStore.get('enabled-shuffle'),
|
||||
savedPlaylist: PlaylistViewStore.get('saved-playlist-loop'),
|
||||
title: props.playlistData.title,
|
||||
link: props.playlistData.url,
|
||||
authorName: props.playlistData.user,
|
||||
authorLink: LinksContext._currentValue.home + '/user/' + props.playlistData.user,
|
||||
activeItem: props.activeItem,
|
||||
totalMedia: props.playlistData.media_count,
|
||||
items: props.playlistData.playlist_media,
|
||||
};
|
||||
|
||||
this.onHeaderClick = this.onHeaderClick.bind(this);
|
||||
this.onLoopClick = this.onLoopClick.bind(this);
|
||||
this.onShuffleClick = this.onShuffleClick.bind(this);
|
||||
this.onSaveClick = this.onSaveClick.bind(this);
|
||||
this.onLoopRepeatUpdate = this.onLoopRepeatUpdate.bind(this);
|
||||
this.onShuffleUpdate = this.onShuffleUpdate.bind(this);
|
||||
this.onPlaylistSaveUpdate = this.onPlaylistSaveUpdate.bind(this);
|
||||
|
||||
PlaylistViewStore.on('loop-repeat-updated', this.onLoopRepeatUpdate);
|
||||
PlaylistViewStore.on('shuffle-updated', this.onShuffleUpdate);
|
||||
PlaylistViewStore.on('saved-updated', this.onPlaylistSaveUpdate);
|
||||
}
|
||||
|
||||
onHeaderClick(ev) {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
}
|
||||
|
||||
onLoopClick() {
|
||||
PlaylistViewActions.toggleLoop();
|
||||
}
|
||||
|
||||
onShuffleClick() {
|
||||
PlaylistViewActions.toggleShuffle();
|
||||
}
|
||||
|
||||
onSaveClick() {
|
||||
PlaylistViewActions.toggleSave();
|
||||
}
|
||||
|
||||
onShuffleUpdate() {
|
||||
this.setState(
|
||||
{
|
||||
shuffle: PlaylistViewStore.get('enabled-shuffle'),
|
||||
},
|
||||
() => {
|
||||
if (this.state.shuffle) {
|
||||
PageActions.addNotification('Playlist shuffle is on', 'shuffle-on');
|
||||
} else {
|
||||
PageActions.addNotification('Playlist shuffle is off', 'shuffle-off');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onLoopRepeatUpdate() {
|
||||
this.setState(
|
||||
{
|
||||
loopRepeat: PlaylistViewStore.get('enabled-loop'),
|
||||
},
|
||||
() => {
|
||||
if (this.state.loopRepeat) {
|
||||
PageActions.addNotification('Playlist loop is on', 'loop-on');
|
||||
} else {
|
||||
PageActions.addNotification('Playlist loop is off', 'loop-off');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onPlaylistSaveUpdate() {
|
||||
this.setState(
|
||||
{
|
||||
savedPlaylist: PlaylistViewStore.get('saved-playlist'),
|
||||
},
|
||||
() => {
|
||||
if (this.state.savedPlaylist) {
|
||||
PageActions.addNotification('Added to playlists library', 'added-to-playlists-lib');
|
||||
} else {
|
||||
PageActions.addNotification('Removed from playlists library', 'removed-from-playlists-lib');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="playlist-view-wrap">
|
||||
<div className={'playlist-view' + (!this.state.expanded ? '' : ' playlist-expanded-view')}>
|
||||
<div className="playlist-header">
|
||||
<div className="playlist-title">
|
||||
<a href={this.state.link} title={this.state.title}>
|
||||
{this.state.title}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="playlist-meta">
|
||||
{/*'public' === PlaylistViewStore.get('visibility') ? null :
|
||||
<div className="playlist-status">
|
||||
<span>{ PlaylistViewStore.get('visibility-icon') }</span>
|
||||
<div>{ PlaylistViewStore.get('visibility') }</div>
|
||||
</div>*/}
|
||||
<span>
|
||||
<a href={this.state.authorLink} title={this.state.authorName}>
|
||||
{this.state.authorName}
|
||||
</a>
|
||||
</span>
|
||||
-
|
||||
<span className="counter">
|
||||
{this.state.activeItem} / {this.state.totalMedia}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<CircleIconButton className="toggle-playlist-view" onClick={this.onHeaderClick}>
|
||||
{this.state.expanded ? (
|
||||
<i className="material-icons">keyboard_arrow_up</i>
|
||||
) : (
|
||||
<i className="material-icons">keyboard_arrow_down</i>
|
||||
)}
|
||||
</CircleIconButton>
|
||||
</div>
|
||||
|
||||
{!this.state.expanded ? null : (
|
||||
<div className="playlist-actions">
|
||||
<CircleIconButton
|
||||
className={this.state.loopRepeat ? 'active' : ''}
|
||||
onClick={this.onLoopClick}
|
||||
title="Loop playlist"
|
||||
>
|
||||
<i className="material-icons">repeat</i>
|
||||
</CircleIconButton>
|
||||
{/*<CircleIconButton className={ this.state.shuffle ? 'active' : '' } onClick={ this.onShuffleClick } title="Shuffle playlist"><i className="material-icons">shuffle</i></CircleIconButton>*/}
|
||||
{/*PlaylistViewStore.get('logged-in-user-playlist') ? null : <CircleIconButton className={ 'add-to-playlist' + ( this.state.savedPlaylist ? ' active' : '' ) } onClick={ this.onSaveClick } title={ this.state.savedPlaylist ? "Remove" : "Save playlist" }><i className="material-icons">{ this.state.savedPlaylist ? 'playlist_add_check' : 'playlist_add' }</i></CircleIconButton>*/}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!this.state.expanded || !this.state.items.length ? null : (
|
||||
<div className="playlist-media">
|
||||
<PlaylistPlaybackMedia items={this.state.items} playlistActiveItem={this.state.activeItem} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PlaylistView.propTypes = {
|
||||
playlistData: PropTypes.object.isRequired,
|
||||
activeItem: PositiveIntegerOrZero,
|
||||
};
|
||||
|
||||
PlaylistView.defaultProps = {};
|
||||
258
frontend/src/static/js/components/media-page/PlaylistView.scss
Executable file
258
frontend/src/static/js/components/media-page/PlaylistView.scss
Executable file
@@ -0,0 +1,258 @@
|
||||
.playlist-view {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
.dark_theme & {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
background-color: var(--playlist-view-header-bg-color);
|
||||
|
||||
.toggle-playlist-view {
|
||||
color: var(--playlist-view-header-toggle-text-color);
|
||||
background-color: var(--playlist-view-header-toggle-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-title {
|
||||
a {
|
||||
color: var(--playlist-view-title-link-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
color: var(--playlist-view-meta-text-color);
|
||||
|
||||
.counter {
|
||||
color: var(--item-meta-text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--playlist-view-meta-link-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--playlist-view-meta-link-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-status {
|
||||
color: var(--playlist-view-status-text-color);
|
||||
background-color: var(--playlist-view-status-bg-color);
|
||||
|
||||
.material-icons {
|
||||
color: var(--playlist-view-status-icon-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-actions {
|
||||
background-color: var(--playlist-view-actions-bg-color);
|
||||
|
||||
.circle-icon-button {
|
||||
background-color: var(--playlist-view-actions-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-media {
|
||||
background-color: var(--playlist-view-media-bg-color);
|
||||
|
||||
.item-order-number {
|
||||
color: var(--playlist-view-media-order-number-color);
|
||||
}
|
||||
|
||||
.item-main {
|
||||
line-height: 1;
|
||||
|
||||
h3 {
|
||||
color: var(--playlist-view-item-title-text-color);
|
||||
|
||||
span {
|
||||
line-height: var(--playlist-item-title-line-height);
|
||||
max-height: calc(var(--horizontal-item-title-max-lines) * var(--playlist-item-title-line-height));
|
||||
background-color: var(--playlist-view-media-bg-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
line-height: 1.230769231em;
|
||||
color: var(--item-meta-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-view-wrap {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.playlist-view {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
position: relative;
|
||||
padding: 12px 16px;
|
||||
|
||||
.toggle-playlist-view {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 17px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.playlist-expanded-view {
|
||||
.playlist-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.playlist-status {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
border-radius: 2px;
|
||||
|
||||
.material-icons {
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
margin: 0 3px 0 0;
|
||||
}
|
||||
|
||||
div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-actions {
|
||||
padding: 0 16px 0 8px;
|
||||
|
||||
.circle-icon-button {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
&.active {
|
||||
color: var(--theme-color, var(--default-theme-color));
|
||||
}
|
||||
|
||||
&.add-to-playlist {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-media {
|
||||
max-height: 415px;
|
||||
padding: 4px 0;
|
||||
overflow: auto;
|
||||
|
||||
.items-list-outer {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
padding: 4px 8px 4px 28px;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
|
||||
transition: background-color 0.05s linear;
|
||||
|
||||
&:hover,
|
||||
&.pl-active-item {
|
||||
background-color: var(--nav-menu-item-hover-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.item-order-number {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
width: 28px;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
|
||||
> div {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.items-list-wrap {
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
padding-left: 100px !important;
|
||||
}
|
||||
|
||||
.item-thumb {
|
||||
width: 100px !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
width: auto;
|
||||
display: block;
|
||||
min-height: 56px !important;
|
||||
|
||||
.item-content-link {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { ItemList } from '../item-list/ItemList';
|
||||
|
||||
export function RelatedMedia(props) {
|
||||
const [items, setItems] = useState(updateMediaItems());
|
||||
const [mediaType, setMediaType] = useState(null);
|
||||
|
||||
function onMediaDataLoad() {
|
||||
setMediaType(MediaPageStore.get('media-type'));
|
||||
setItems(updateMediaItems());
|
||||
}
|
||||
|
||||
function updateMediaItems() {
|
||||
const md = MediaPageStore.get('media-data');
|
||||
return void 0 !== md && null !== md && void 0 !== md.related_media && md.related_media.length
|
||||
? md.related_media
|
||||
: null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
MediaPageStore.on('loaded_media_data', onMediaDataLoad);
|
||||
return () => MediaPageStore.removeListener('loaded_media_data', onMediaDataLoad);
|
||||
}, []);
|
||||
|
||||
return !items || !items.length ? null : (
|
||||
<ItemList
|
||||
className="items-list-hor"
|
||||
items={props.hideFirst && ('video' === mediaType || 'audio' === mediaType) ? items.slice(1) : items}
|
||||
pageItems={PageStore.get('config-options').pages.media.related.initialSize}
|
||||
singleLinkContent={true}
|
||||
horizontalItemsOrientation={true}
|
||||
hideDate={true}
|
||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||
hideAuthor={!PageStore.get('config-media-item').displayAuthor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RelatedMedia.propTypes = {
|
||||
hideFirst: PropTypes.bool,
|
||||
};
|
||||
|
||||
RelatedMedia.defaultProps = {
|
||||
hideFirst: true,
|
||||
};
|
||||
23
frontend/src/static/js/components/media-page/ViewerError.js
Executable file
23
frontend/src/static/js/components/media-page/ViewerError.js
Executable file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
|
||||
export default class ViewerError extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="viewer-container" key="viewer-container-error">
|
||||
<div className="player-container player-container-error">
|
||||
<div className="player-container-inner">
|
||||
<div className="error-container">
|
||||
<div className="error-container-inner">
|
||||
<span className="icon-wrap">
|
||||
<i className="material-icons">error_outline</i>
|
||||
</span>
|
||||
<span className="msg-wrap">{MediaPageStore.get('media-load-error-message')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
61
frontend/src/static/js/components/media-page/ViewerInfo.js
Executable file
61
frontend/src/static/js/components/media-page/ViewerInfo.js
Executable file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import ViewerInfoContent from './ViewerInfoContent';
|
||||
import ViewerInfoTitleBanner from './ViewerInfoTitleBanner';
|
||||
|
||||
export default class ViewerInfo extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
videoLoaded: false,
|
||||
};
|
||||
|
||||
this.onVideoLoad = this.onVideoLoad.bind(this);
|
||||
|
||||
MediaPageStore.on('loaded_media_data', this.onVideoLoad);
|
||||
}
|
||||
|
||||
onVideoLoad() {
|
||||
this.setState({
|
||||
videoLoaded: true,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let views, categories, title, author, published, description;
|
||||
let allowDownload = false;
|
||||
|
||||
if (this.state.videoLoaded) {
|
||||
allowDownload = MediaPageStore.get('media-data').allow_download;
|
||||
|
||||
if (void 0 === allowDownload) {
|
||||
allowDownload = true;
|
||||
} else {
|
||||
allowDownload = !!allowDownload;
|
||||
}
|
||||
|
||||
views = MediaPageStore.get('media-data').views;
|
||||
categories = MediaPageStore.get('media-data').categories_info;
|
||||
title = MediaPageStore.get('media-data').title;
|
||||
|
||||
author = {
|
||||
name: MediaPageStore.get('media-data').author_name,
|
||||
url: MediaPageStore.get('media-data').author_profile,
|
||||
thumb: MediaPageStore.get('media-author-thumbnail-url'),
|
||||
};
|
||||
|
||||
published = MediaPageStore.get('media-data').add_date;
|
||||
description = MediaPageStore.get('media-data').description;
|
||||
}
|
||||
|
||||
return !this.state.videoLoaded ? null : (
|
||||
<div className="viewer-info">
|
||||
<div className="viewer-info-inner">
|
||||
<ViewerInfoTitleBanner title={title} views={views} categories={categories} allowDownload={allowDownload} />
|
||||
<ViewerInfoContent author={author} published={published} description={description} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
246
frontend/src/static/js/components/media-page/ViewerInfoContent.js
Executable file
246
frontend/src/static/js/components/media-page/ViewerInfoContent.js
Executable file
@@ -0,0 +1,246 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SiteContext } from '../../utils/contexts/';
|
||||
import { useUser, usePopup } from '../../utils/hooks/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { formatInnerLink, publishedOnDate } from '../../utils/helpers/';
|
||||
import { PopupMain } from '../_shared/';
|
||||
import CommentsList from '../comments/Comments';
|
||||
|
||||
function metafield(arr) {
|
||||
let i;
|
||||
let sep;
|
||||
let ret = [];
|
||||
|
||||
if (arr.length) {
|
||||
i = 0;
|
||||
sep = 1 < arr.length ? ', ' : '';
|
||||
while (i < arr.length) {
|
||||
ret[i] = (
|
||||
<div key={i}>
|
||||
<a href={arr[i].url} title={arr[i].title}>
|
||||
{arr[i].title}
|
||||
</a>
|
||||
{i < arr.length - 1 ? sep : ''}
|
||||
</div>
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function MediaAuthorBanner(props) {
|
||||
return (
|
||||
<div className="media-author-banner">
|
||||
<div>
|
||||
<a className="author-banner-thumb" href={props.link || null} title={props.name}>
|
||||
<span style={{ backgroundImage: 'url(' + props.thumb + ')' }}>
|
||||
<img src={props.thumb} loading="lazy" alt={props.name} title={props.name} />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
<a href={props.link} className="author-banner-name" title={props.name}>
|
||||
<span>{props.name}</span>
|
||||
</a>
|
||||
</span>
|
||||
{PageStore.get('config-media-item').displayPublishDate && props.published ? (
|
||||
<span className="author-banner-date">Published on {publishedOnDate(new Date(props.published))}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaMetaField(props) {
|
||||
return (
|
||||
<div className={props.id.trim() ? 'media-content-' + props.id.trim() : null}>
|
||||
<div className="media-content-field">
|
||||
<div className="media-content-field-label">
|
||||
<h4>{props.title}</h4>
|
||||
</div>
|
||||
<div className="media-content-field-content">{props.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditMediaButton(props) {
|
||||
let link = props.link;
|
||||
|
||||
if (window.MediaCMS.site.devEnv) {
|
||||
link = '/edit-media.html';
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={link} rel="nofollow" title="Edit media" className="edit-media">
|
||||
EDIT MEDIA
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function EditSubtitleButton(props) {
|
||||
let link = props.link;
|
||||
|
||||
if (window.MediaCMS.site.devEnv) {
|
||||
link = '#';
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={link} rel="nofollow" title="Edit subtitle" className="edit-subtitle">
|
||||
EDIT SUBTITLE
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ViewerInfoContent(props) {
|
||||
const { userCan } = useUser();
|
||||
|
||||
const description = props.description.trim();
|
||||
const tagsContent =
|
||||
!PageStore.get('config-enabled').taxonomies.tags || PageStore.get('config-enabled').taxonomies.tags.enabled
|
||||
? metafield(MediaPageStore.get('media-tags'))
|
||||
: [];
|
||||
const categoriesContent = PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||
? []
|
||||
: !PageStore.get('config-enabled').taxonomies.categories ||
|
||||
PageStore.get('config-enabled').taxonomies.categories.enabled
|
||||
? metafield(MediaPageStore.get('media-categories'))
|
||||
: [];
|
||||
|
||||
let summary = MediaPageStore.get('media-summary');
|
||||
|
||||
summary = summary ? summary.trim() : '';
|
||||
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
const [hasSummary, setHasSummary] = useState('' !== summary);
|
||||
const [isContentVisible, setIsContentVisible] = useState('' == summary);
|
||||
|
||||
function proceedMediaRemoval() {
|
||||
MediaPageActions.removeMedia();
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
|
||||
function cancelMediaRemoval() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
|
||||
function onMediaDelete(mediaId) {
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(function () {
|
||||
PageActions.addNotification('Media removed. Redirecting...', 'mediaDelete');
|
||||
setTimeout(function () {
|
||||
window.location.href =
|
||||
SiteContext._currentValue.url + '/' + MediaPageStore.get('media-data').author_profile.replace(/^\//g, '');
|
||||
}, 2000);
|
||||
}, 100);
|
||||
|
||||
if (void 0 !== mediaId) {
|
||||
console.info("Removed media '" + mediaId + '"');
|
||||
}
|
||||
}
|
||||
|
||||
function onMediaDeleteFail(mediaId) {
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
setTimeout(function () {
|
||||
PageActions.addNotification('Media removal failed', 'mediaDeleteFail');
|
||||
}, 100);
|
||||
|
||||
if (void 0 !== mediaId) {
|
||||
console.info('Media "' + mediaId + '"' + ' removal failed');
|
||||
}
|
||||
}
|
||||
|
||||
function onClickLoadMore() {
|
||||
setIsContentVisible(!isContentVisible);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
MediaPageStore.on('media_delete', onMediaDelete);
|
||||
MediaPageStore.on('media_delete_fail', onMediaDeleteFail);
|
||||
return () => {
|
||||
MediaPageStore.removeListener('media_delete', onMediaDelete);
|
||||
MediaPageStore.removeListener('media_delete_fail', onMediaDeleteFail);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const authorLink = formatInnerLink(props.author.url, SiteContext._currentValue.url);
|
||||
const authorThumb = formatInnerLink(props.author.thumb, SiteContext._currentValue.url);
|
||||
|
||||
return (
|
||||
<div className="media-info-content">
|
||||
{void 0 === PageStore.get('config-media-item').displayAuthor ||
|
||||
null === PageStore.get('config-media-item').displayAuthor ||
|
||||
!!PageStore.get('config-media-item').displayAuthor ? (
|
||||
<MediaAuthorBanner link={authorLink} thumb={authorThumb} name={props.author.name} published={props.published} />
|
||||
) : null}
|
||||
|
||||
<div className="media-content-banner">
|
||||
<div className="media-content-banner-inner">
|
||||
{hasSummary ? <div className="media-content-summary">{summary}</div> : null}
|
||||
{(!hasSummary || isContentVisible) && description ? (
|
||||
PageStore.get('config-options').pages.media.htmlInDescription ? (
|
||||
<div className="media-content-description" dangerouslySetInnerHTML={{ __html: description }}></div>
|
||||
) : (
|
||||
<div className="media-content-description">{description}</div>
|
||||
)
|
||||
) : null}
|
||||
{hasSummary ? (
|
||||
<button className="load-more" onClick={onClickLoadMore}>
|
||||
{isContentVisible ? 'SHOW LESS' : 'SHOW MORE'}
|
||||
</button>
|
||||
) : null}
|
||||
{tagsContent.length ? (
|
||||
<MediaMetaField value={tagsContent} title={1 < tagsContent.length ? 'Tags' : 'Tag'} id="tags" />
|
||||
) : null}
|
||||
{categoriesContent.length ? (
|
||||
<MediaMetaField
|
||||
value={categoriesContent}
|
||||
title={1 < categoriesContent.length ? 'Categories' : 'Category'}
|
||||
id="categories"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{userCan.editMedia || userCan.editSubtitle || userCan.deleteMedia ? (
|
||||
<div className="media-author-actions">
|
||||
{userCan.editMedia ? <EditMediaButton link={MediaPageStore.get('media-data').edit_url} /> : null}
|
||||
{userCan.editSubtitle && 'video' === MediaPageStore.get('media-data').media_type ? (
|
||||
<EditSubtitleButton
|
||||
link={MediaPageStore.get('media-data').edit_url.replace('edit?', 'add_subtitle?')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<button className="remove-media">DELETE MEDIA</button>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef}>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Media removal</span>
|
||||
<span className="popup-message-main">You're willing to remove media permanently?</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-comment-removal" onClick={cancelMediaRemoval}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-comment-removal" onClick={proceedMediaRemoval}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommentsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { formatInnerLink, formatViewsNumber } from '../../utils/helpers/';
|
||||
import { MemberContext, PlaylistsContext, SiteContext } from '../../utils/contexts/';
|
||||
import { MediaLikeIcon, MediaDislikeIcon, OtherMediaDownloadLink, VideoMediaDownloadLink, MediaSaveButton, MediaShareButton, MediaMoreOptionsIcon } from '../media-actions/';
|
||||
|
||||
function Tooltip(el) {
|
||||
const parent = document.body;
|
||||
|
||||
const tooltipElem = document.createElement('span');
|
||||
|
||||
tooltipElem.innerText = el.getAttribute('data-tooltip');
|
||||
tooltipElem.setAttribute('class', 'tooltip');
|
||||
|
||||
el.removeAttribute('data-tooltip');
|
||||
|
||||
function onMouseenter() {
|
||||
const targetClientRect = el.getBoundingClientRect();
|
||||
parent.appendChild(tooltipElem);
|
||||
tooltipElem.style.top = targetClientRect.top - (0 + tooltipElem.offsetHeight) + 'px';
|
||||
tooltipElem.style.left = targetClientRect.left + 'px';
|
||||
document.addEventListener('scroll', onScroll);
|
||||
}
|
||||
|
||||
function onMouseleave() {
|
||||
parent.removeChild(tooltipElem);
|
||||
tooltipElem.style.top = '';
|
||||
tooltipElem.style.left = '';
|
||||
document.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
const targetClientRect = el.getBoundingClientRect();
|
||||
tooltipElem.style.top = targetClientRect.top - (0 + tooltipElem.offsetHeight) + 'px';
|
||||
tooltipElem.style.left = targetClientRect.left + 'px';
|
||||
}
|
||||
|
||||
el.addEventListener('mouseenter', onMouseenter);
|
||||
el.addEventListener('mouseleave', onMouseleave);
|
||||
}
|
||||
|
||||
export default class ViewerInfoTitleBanner extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
likedMedia: MediaPageStore.get('user-liked-media'),
|
||||
dislikedMedia: MediaPageStore.get('user-disliked-media'),
|
||||
};
|
||||
|
||||
this.downloadLink =
|
||||
'video' !== MediaPageStore.get('media-type')
|
||||
? formatInnerLink(MediaPageStore.get('media-original-url'), SiteContext._currentValue.url)
|
||||
: null;
|
||||
|
||||
this.updateStateValues = this.updateStateValues.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MediaPageStore.on('liked_media', this.updateStateValues);
|
||||
MediaPageStore.on('unliked_media', this.updateStateValues);
|
||||
MediaPageStore.on('disliked_media', this.updateStateValues);
|
||||
MediaPageStore.on('undisliked_media', this.updateStateValues);
|
||||
|
||||
const tooltips = document.querySelectorAll('[data-tooltip]');
|
||||
|
||||
if (tooltips.length) {
|
||||
tooltips.forEach((tooltipElem) => Tooltip(tooltipElem));
|
||||
}
|
||||
}
|
||||
|
||||
updateStateValues() {
|
||||
this.setState({
|
||||
likedMedia: MediaPageStore.get('user-liked-media'),
|
||||
dislikedMedia: MediaPageStore.get('user-disliked-media'),
|
||||
});
|
||||
}
|
||||
|
||||
mediaCategories(overTitle) {
|
||||
if (void 0 === this.props.categories || null === this.props.categories || !this.props.categories.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let cats = [];
|
||||
while (i < this.props.categories.length) {
|
||||
cats.push(
|
||||
<span key={i}>
|
||||
<a
|
||||
href={formatInnerLink(this.props.categories[i].url, SiteContext._currentValue.url)}
|
||||
title={this.props.categories[i].title}
|
||||
>
|
||||
{this.props.categories[i].title}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return <div className={'media-under-title-categories' + (!!overTitle ? ' over-title' : '')}>{cats}</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const displayViews = PageStore.get('config-options').pages.media.displayViews && void 0 !== this.props.views;
|
||||
|
||||
const mediaState = MediaPageStore.get('media-data').state;
|
||||
|
||||
let stateTooltip = '';
|
||||
|
||||
switch (mediaState) {
|
||||
case 'private':
|
||||
stateTooltip = 'The site admins have to make its access public';
|
||||
break;
|
||||
case 'unlisted':
|
||||
stateTooltip = 'The site admins have to make it appear on listings';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="media-title-banner">
|
||||
{displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||
? this.mediaCategories(true)
|
||||
: null}
|
||||
|
||||
{void 0 !== this.props.title ? <h1>{this.props.title}</h1> : null}
|
||||
|
||||
{'public' !== mediaState ? (
|
||||
<div className="media-labels-area">
|
||||
<div className="media-labels-area-inner">
|
||||
<span className="media-label-state">
|
||||
<span>{mediaState}</span>
|
||||
</span>
|
||||
<span className="helper-icon" data-tooltip={stateTooltip}>
|
||||
<i className="material-icons">help_outline</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={
|
||||
'media-views-actions' +
|
||||
(this.state.likedMedia ? ' liked-media' : '') +
|
||||
(this.state.dislikedMedia ? ' disliked-media' : '')
|
||||
}
|
||||
>
|
||||
{!displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||
? this.mediaCategories()
|
||||
: null}
|
||||
|
||||
{displayViews ? (
|
||||
<div className="media-views">
|
||||
{formatViewsNumber(this.props.views, true)} {1 >= this.props.views ? 'view' : 'views'}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="media-actions">
|
||||
<div>
|
||||
{MemberContext._currentValue.can.likeMedia ? <MediaLikeIcon /> : null}
|
||||
{MemberContext._currentValue.can.dislikeMedia ? <MediaDislikeIcon /> : null}
|
||||
{MemberContext._currentValue.can.shareMedia ? <MediaShareButton isVideo={false} /> : null}
|
||||
|
||||
{!MemberContext._currentValue.is.anonymous &&
|
||||
MemberContext._currentValue.can.saveMedia &&
|
||||
-1 < PlaylistsContext._currentValue.mediaTypes.indexOf(MediaPageStore.get('media-type')) ? (
|
||||
<MediaSaveButton />
|
||||
) : null}
|
||||
|
||||
{!this.props.allowDownload || !MemberContext._currentValue.can.downloadMedia ? null : !this
|
||||
.downloadLink ? (
|
||||
<VideoMediaDownloadLink />
|
||||
) : (
|
||||
<OtherMediaDownloadLink link={this.downloadLink} title={this.props.title} />
|
||||
)}
|
||||
|
||||
<MediaMoreOptionsIcon allowDownload={this.props.allowDownload} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewerInfoTitleBanner.propTypes = {
|
||||
allowDownload: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
ViewerInfoTitleBanner.defaultProps = {
|
||||
allowDownload: false,
|
||||
};
|
||||
49
frontend/src/static/js/components/media-page/ViewerInfoVideo.js
Executable file
49
frontend/src/static/js/components/media-page/ViewerInfoVideo.js
Executable file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import ViewerInfoContent from './ViewerInfoContent';
|
||||
import ViewerInfoVideoTitleBanner from './ViewerInfoVideoTitleBanner';
|
||||
import ViewerInfo from './ViewerInfo';
|
||||
|
||||
export default class ViewerInfoVideo extends ViewerInfo {
|
||||
render() {
|
||||
let views, categories, title, author, published, description;
|
||||
let allowDownload = false;
|
||||
|
||||
if (this.state.videoLoaded) {
|
||||
allowDownload = MediaPageStore.get('media-data').allow_download;
|
||||
|
||||
if (void 0 === allowDownload) {
|
||||
allowDownload = true;
|
||||
} else {
|
||||
allowDownload = !!allowDownload;
|
||||
}
|
||||
|
||||
views = MediaPageStore.get('media-data').views;
|
||||
categories = MediaPageStore.get('media-data').categories_info;
|
||||
title = MediaPageStore.get('media-data').title;
|
||||
|
||||
author = {
|
||||
name: MediaPageStore.get('media-data').author_name,
|
||||
url: MediaPageStore.get('media-data').author_profile,
|
||||
thumb: MediaPageStore.get('media-author-thumbnail-url'),
|
||||
};
|
||||
|
||||
published = MediaPageStore.get('media-data').add_date;
|
||||
description = MediaPageStore.get('media-data').description;
|
||||
}
|
||||
|
||||
return !this.state.videoLoaded ? null : (
|
||||
<div className="viewer-info">
|
||||
<div className="viewer-info-inner">
|
||||
<ViewerInfoVideoTitleBanner
|
||||
title={title}
|
||||
views={views}
|
||||
categories={categories}
|
||||
allowDownload={allowDownload}
|
||||
/>
|
||||
<ViewerInfoContent author={author} published={published} description={description} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { formatViewsNumber } from '../../utils/helpers/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { MemberContext, PlaylistsContext } from '../../utils/contexts/';
|
||||
import { MediaLikeIcon, MediaDislikeIcon, OtherMediaDownloadLink, VideoMediaDownloadLink, MediaSaveButton, MediaShareButton, MediaMoreOptionsIcon } from '../media-actions/';
|
||||
import ViewerInfoTitleBanner from './ViewerInfoTitleBanner';
|
||||
|
||||
export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
|
||||
render() {
|
||||
const displayViews = PageStore.get('config-options').pages.media.displayViews && void 0 !== this.props.views;
|
||||
|
||||
const mediaState = MediaPageStore.get('media-data').state;
|
||||
|
||||
let stateTooltip = '';
|
||||
|
||||
switch (mediaState) {
|
||||
case 'private':
|
||||
stateTooltip = 'The site admins have to make its access public';
|
||||
break;
|
||||
case 'unlisted':
|
||||
stateTooltip = 'The site admins have to make it appear on listings';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="media-title-banner">
|
||||
{displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||
? this.mediaCategories(true)
|
||||
: null}
|
||||
|
||||
{void 0 !== this.props.title ? <h1>{this.props.title}</h1> : null}
|
||||
|
||||
{'public' !== mediaState ? (
|
||||
<div className="media-labels-area">
|
||||
<div className="media-labels-area-inner">
|
||||
<span className="media-label-state">
|
||||
<span>{mediaState}</span>
|
||||
</span>
|
||||
<span className="helper-icon" data-tooltip={stateTooltip}>
|
||||
<i className="material-icons">help_outline</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={
|
||||
'media-views-actions' +
|
||||
(this.state.likedMedia ? ' liked-media' : '') +
|
||||
(this.state.dislikedMedia ? ' disliked-media' : '')
|
||||
}
|
||||
>
|
||||
{!displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||
? this.mediaCategories()
|
||||
: null}
|
||||
|
||||
{displayViews ? (
|
||||
<div className="media-views">
|
||||
{formatViewsNumber(this.props.views, true)} {1 >= this.props.views ? 'view' : 'views'}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="media-actions">
|
||||
<div>
|
||||
{MemberContext._currentValue.can.likeMedia ? <MediaLikeIcon /> : null}
|
||||
{MemberContext._currentValue.can.dislikeMedia ? <MediaDislikeIcon /> : null}
|
||||
{MemberContext._currentValue.can.shareMedia ? <MediaShareButton isVideo={true} /> : null}
|
||||
|
||||
{!MemberContext._currentValue.is.anonymous &&
|
||||
MemberContext._currentValue.can.saveMedia &&
|
||||
-1 < PlaylistsContext._currentValue.mediaTypes.indexOf(MediaPageStore.get('media-type')) ? (
|
||||
<MediaSaveButton />
|
||||
) : null}
|
||||
|
||||
{!this.props.allowDownload || !MemberContext._currentValue.can.downloadMedia ? null : !this
|
||||
.downloadLink ? (
|
||||
<VideoMediaDownloadLink />
|
||||
) : (
|
||||
<OtherMediaDownloadLink link={this.downloadLink} title={this.props.title} />
|
||||
)}
|
||||
|
||||
<MediaMoreOptionsIcon allowDownload={this.props.allowDownload} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import { AutoPlay } from './AutoPlay';
|
||||
import { RelatedMedia } from './RelatedMedia';
|
||||
import PlaylistView from './PlaylistView';
|
||||
|
||||
export default class ViewerSidebar extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
playlistData: props.playlistData,
|
||||
isPlaylistPage: !!props.playlistData,
|
||||
activeItem: 0,
|
||||
mediaType: MediaPageStore.get('media-type'),
|
||||
};
|
||||
|
||||
if (props.playlistData) {
|
||||
let i = 0;
|
||||
while (i < props.playlistData.playlist_media.length) {
|
||||
if (props.mediaId === props.playlistData.playlist_media[i].friendly_token) {
|
||||
this.state.activeItem = i + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.onMediaLoad = this.onMediaLoad.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
|
||||
}
|
||||
|
||||
onMediaLoad() {
|
||||
this.setState({
|
||||
mediaType: MediaPageStore.get('media-type'),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="viewer-sidebar">
|
||||
{this.state.isPlaylistPage ? (
|
||||
<PlaylistView activeItem={this.state.activeItem} playlistData={this.props.playlistData} />
|
||||
) : 'video' === this.state.mediaType || 'audio' === this.state.mediaType ? (
|
||||
<AutoPlay />
|
||||
) : null}
|
||||
<RelatedMedia hideFirst={!this.state.isPlaylistPage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { putRequest, csrfToken } from '../../utils/helpers/';
|
||||
import { usePopup } from '../../utils/hooks/';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { PageActions, PlaylistPageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, MaterialIcon, NavigationContentApp, NavigationMenuList, PopupMain } from '../_shared';
|
||||
|
||||
function mediaPlaylistPopupPages(proceedRemoval, cancelRemoval) {
|
||||
const settingOptionsList = {
|
||||
deleteMedia: {
|
||||
itemType: 'open-subpage',
|
||||
text: 'Remove from playlist',
|
||||
icon: 'delete',
|
||||
buttonAttr: {
|
||||
className: 'change-page',
|
||||
'data-page-id': 'proceedMediaPlaylistRemoval',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const pages = {
|
||||
main: (
|
||||
<PopupMain>
|
||||
<NavigationMenuList items={[settingOptionsList.deleteMedia]} />
|
||||
</PopupMain>
|
||||
),
|
||||
proceedMediaPlaylistRemoval: (
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Media playlist removal</span>
|
||||
<span className="popup-message-main">You're willing to remove media from playlist permanently?</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-playlist-removal" onClick={cancelRemoval}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-playlist-removal" onClick={proceedRemoval}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
),
|
||||
};
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
export function MediaPlaylistOptions(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
|
||||
const [popupCurrentPage, setPopupCurrentPage] = useState('main');
|
||||
|
||||
const [popupPages] = useState(mediaPlaylistPopupPages(proceedRemoval, cancelRemoval));
|
||||
|
||||
function mediaPlaylistRemovalCompleted() {
|
||||
popupContentRef.current.tryToHide();
|
||||
const props_media_id = props.media_id;
|
||||
const props_playlist_id = props.playlist_id;
|
||||
setTimeout(function () {
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
PageActions.addNotification('Media removed from playlist', 'mediaPlaylistRemove');
|
||||
PlaylistPageActions.removedMediaFromPlaylist(props_media_id, props_playlist_id);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function mediaPlaylistRemovalFailed() {
|
||||
popupContentRef.current.tryToHide();
|
||||
setTimeout(function () {
|
||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||
PageActions.addNotification('Media removal from playlist failed', 'mediaPlaylistRemoveFail');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function proceedRemoval() {
|
||||
putRequest(
|
||||
PageStore.get('api-playlists') + '/' + props.playlist_id,
|
||||
{
|
||||
type: 'remove',
|
||||
media_friendly_token: props.media_id,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken(),
|
||||
},
|
||||
},
|
||||
false,
|
||||
mediaPlaylistRemovalCompleted,
|
||||
mediaPlaylistRemovalFailed
|
||||
);
|
||||
}
|
||||
|
||||
function cancelRemoval() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
|
||||
function onPopupPageChange(newPage) {
|
||||
setPopupCurrentPage(newPage);
|
||||
}
|
||||
function onPopupHide() {
|
||||
setPopupCurrentPage('main');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'item-playlist-options-wrap' + ('main' === popupCurrentPage ? ' item-playlist-options-main' : '')}>
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<CircleIconButton>
|
||||
<MaterialIcon type="more_vert" />
|
||||
</CircleIconButton>
|
||||
</PopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef} hideCallback={onPopupHide}>
|
||||
<NavigationContentApp
|
||||
pageChangeCallback={onPopupPageChange}
|
||||
initPage={popupCurrentPage}
|
||||
focusFirstItemOnPageChange={false}
|
||||
pages={popupPages}
|
||||
pageChangeSelector={'.change-page'}
|
||||
pageIdSelectorAttr={'data-page-id'}
|
||||
/>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MediaPlaylistOptions.propTypes = {};
|
||||
MediaPlaylistOptions.propTypes.media_id = PropTypes.string.isRequired;
|
||||
MediaPlaylistOptions.propTypes.playlist_id = PropTypes.string.isRequired;
|
||||
15
frontend/src/static/js/components/media-viewer/AttachmentViewer.js
Executable file
15
frontend/src/static/js/components/media-viewer/AttachmentViewer.js
Executable file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function AttachmentPlayer() {
|
||||
return (
|
||||
<div className="player-container viewer-attachment-container">
|
||||
<div className="player-container-inner">
|
||||
<span>
|
||||
<span>
|
||||
<i className="material-icons">insert_drive_file</i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user