parent
f2fffe5304
commit
bdc1adb2ed
@ -0,0 +1,76 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 0 12px $black;
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 200ms linear 150ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.posterContainer {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background-color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayTitle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: $offWhite;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
background-color: #fafbfc;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #707070;
|
||||||
|
color: $white;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
composes: button from '~Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $radarrYellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
|
import styles from './MovieCastPoster.css';
|
||||||
|
|
||||||
|
class MovieCastPoster extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasPosterError: false,
|
||||||
|
isEditMovieModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditMoviePress = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditMovieModalClose = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onPosterLoad = () => {
|
||||||
|
if (this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPosterLoadError = () => {
|
||||||
|
if (!this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
castName,
|
||||||
|
character,
|
||||||
|
images,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasPosterError
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.action}
|
||||||
|
name={icons.EDIT}
|
||||||
|
title="Edit movie"
|
||||||
|
onPress={this.onEditMoviePress}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
>
|
||||||
|
<MovieHeadshot
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={this.onPosterLoadError}
|
||||||
|
onLoad={this.onPosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
hasPosterError &&
|
||||||
|
<div className={styles.overlayTitle}>
|
||||||
|
{castName}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.title}>
|
||||||
|
{castName}
|
||||||
|
</div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{character}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCastPoster.propTypes = {
|
||||||
|
castId: PropTypes.number.isRequired,
|
||||||
|
castName: PropTypes.string.isRequired,
|
||||||
|
character: PropTypes.string.isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
posterWidth: PropTypes.number.isRequired,
|
||||||
|
posterHeight: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieCastPoster;
|
@ -0,0 +1,7 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
@ -0,0 +1,228 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import MovieCastPoster from './MovieCastPoster';
|
||||||
|
import styles from './MovieCastPosters.css';
|
||||||
|
|
||||||
|
// Poster container dimensions
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
||||||
|
const additionalColumnCount = {
|
||||||
|
small: 3,
|
||||||
|
medium: 2,
|
||||||
|
large: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
||||||
|
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
|
||||||
|
const columns = Math.floor(width / maxiumColumnWidth);
|
||||||
|
const remainder = width % maxiumColumnWidth;
|
||||||
|
|
||||||
|
if (remainder === 0 && posterSize === 'large') {
|
||||||
|
return maxiumColumnWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRowHeight(posterHeight, isSmallScreen) {
|
||||||
|
const titleHeight = 19;
|
||||||
|
const characterHeight = 19;
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
posterHeight,
|
||||||
|
titleHeight,
|
||||||
|
characterHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||||
|
];
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePosterHeight(posterWidth) {
|
||||||
|
return Math.ceil((250 / 170) * posterWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MovieCastPosters extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
columnWidth: 182,
|
||||||
|
columnCount: 1,
|
||||||
|
posterWidth: 162,
|
||||||
|
posterHeight: 238,
|
||||||
|
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
||||||
|
};
|
||||||
|
|
||||||
|
this._isInitialized = false;
|
||||||
|
this._grid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const {
|
||||||
|
cast
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (this._grid &&
|
||||||
|
(prevState.width !== width ||
|
||||||
|
prevState.columnWidth !== columnWidth ||
|
||||||
|
prevState.columnCount !== columnCount ||
|
||||||
|
prevState.rowHeight !== rowHeight ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.cast, cast))) {
|
||||||
|
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||||
|
this._grid.recomputeGridSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setGridRef = (ref) => {
|
||||||
|
this._grid = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||||
|
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
|
||||||
|
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
||||||
|
const posterWidth = columnWidth - padding;
|
||||||
|
const posterHeight = calculatePosterHeight(posterWidth);
|
||||||
|
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||||
|
const {
|
||||||
|
cast
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
columnCount
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||||
|
const movie = cast[movieIdx];
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<MovieCastPoster
|
||||||
|
key={movie.order}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
castId={movie.tmdbId}
|
||||||
|
castName={movie.name}
|
||||||
|
character={movie.character}
|
||||||
|
images={movie.images}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ width }) => {
|
||||||
|
this.calculateGrid(width, this.props.isSmallScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
cast
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const rowCount = Math.ceil(cast.length / columnCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure
|
||||||
|
whitelist={['width']}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<WindowScroller
|
||||||
|
scrollElement={undefined}
|
||||||
|
>
|
||||||
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||||
|
if (!height) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={registerChild}>
|
||||||
|
<Grid
|
||||||
|
ref={this.setGridRef}
|
||||||
|
className={styles.grid}
|
||||||
|
autoHeight={true}
|
||||||
|
height={height}
|
||||||
|
columnCount={columnCount}
|
||||||
|
columnWidth={columnWidth}
|
||||||
|
rowCount={rowCount}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
width={width}
|
||||||
|
onScroll={onChildScroll}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={this.cellRenderer}
|
||||||
|
scrollToAlignment={'start'}
|
||||||
|
isScrollingOptOut={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</WindowScroller>
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCastPosters.propTypes = {
|
||||||
|
cast: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieCastPosters;
|
@ -0,0 +1,25 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import MovieCastPosters from './MovieCastPosters';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.moviePeople.items,
|
||||||
|
(people) => {
|
||||||
|
const cast = _.reduce(people, (acc, person) => {
|
||||||
|
if (person.type === 'cast') {
|
||||||
|
acc.push(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cast
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(MovieCastPosters);
|
@ -0,0 +1,76 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 0 12px $black;
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 200ms linear 150ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.posterContainer {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background-color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayTitle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: $offWhite;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
background-color: #fafbfc;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #707070;
|
||||||
|
color: $white;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
composes: button from '~Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $radarrYellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
|
import styles from './MovieCrewPoster.css';
|
||||||
|
|
||||||
|
class MovieCrewPoster extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasPosterError: false,
|
||||||
|
isEditMovieModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditMoviePress = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditMovieModalClose = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onPosterLoad = () => {
|
||||||
|
if (this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPosterLoadError = () => {
|
||||||
|
if (!this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
crewName,
|
||||||
|
job,
|
||||||
|
images,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasPosterError
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.action}
|
||||||
|
name={icons.EDIT}
|
||||||
|
title="Edit movie"
|
||||||
|
onPress={this.onEditMoviePress}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
>
|
||||||
|
<MovieHeadshot
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={this.onPosterLoadError}
|
||||||
|
onLoad={this.onPosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
hasPosterError &&
|
||||||
|
<div className={styles.overlayTitle}>
|
||||||
|
{crewName}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.title}>
|
||||||
|
{crewName}
|
||||||
|
</div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{job}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCrewPoster.propTypes = {
|
||||||
|
crewId: PropTypes.number.isRequired,
|
||||||
|
crewName: PropTypes.string.isRequired,
|
||||||
|
job: PropTypes.string.isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
posterWidth: PropTypes.number.isRequired,
|
||||||
|
posterHeight: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieCrewPoster;
|
@ -0,0 +1,7 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
@ -0,0 +1,228 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import MovieCrewPoster from './MovieCrewPoster';
|
||||||
|
import styles from './MovieCrewPosters.css';
|
||||||
|
|
||||||
|
// Poster container dimensions
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
||||||
|
const additionalColumnCount = {
|
||||||
|
small: 3,
|
||||||
|
medium: 2,
|
||||||
|
large: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
||||||
|
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
|
||||||
|
const columns = Math.floor(width / maxiumColumnWidth);
|
||||||
|
const remainder = width % maxiumColumnWidth;
|
||||||
|
|
||||||
|
if (remainder === 0 && posterSize === 'large') {
|
||||||
|
return maxiumColumnWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRowHeight(posterHeight, isSmallScreen) {
|
||||||
|
const titleHeight = 19;
|
||||||
|
const characterHeight = 19;
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
posterHeight,
|
||||||
|
titleHeight,
|
||||||
|
characterHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||||
|
];
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePosterHeight(posterWidth) {
|
||||||
|
return Math.ceil((250 / 170) * posterWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MovieCrewPosters extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
columnWidth: 182,
|
||||||
|
columnCount: 1,
|
||||||
|
posterWidth: 162,
|
||||||
|
posterHeight: 238,
|
||||||
|
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
||||||
|
};
|
||||||
|
|
||||||
|
this._isInitialized = false;
|
||||||
|
this._grid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const {
|
||||||
|
crew
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (this._grid &&
|
||||||
|
(prevState.width !== width ||
|
||||||
|
prevState.columnWidth !== columnWidth ||
|
||||||
|
prevState.columnCount !== columnCount ||
|
||||||
|
prevState.rowHeight !== rowHeight ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.crew, crew))) {
|
||||||
|
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||||
|
this._grid.recomputeGridSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setGridRef = (ref) => {
|
||||||
|
this._grid = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||||
|
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
|
||||||
|
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
||||||
|
const posterWidth = columnWidth - padding;
|
||||||
|
const posterHeight = calculatePosterHeight(posterWidth);
|
||||||
|
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||||
|
const {
|
||||||
|
crew
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
columnCount
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||||
|
const movie = crew[movieIdx];
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<MovieCrewPoster
|
||||||
|
key={movie.order}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
crewId={movie.tmdbId}
|
||||||
|
crewName={movie.name}
|
||||||
|
job={movie.job}
|
||||||
|
images={movie.images}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ width }) => {
|
||||||
|
this.calculateGrid(width, this.props.isSmallScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
crew
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const rowCount = Math.ceil(crew.length / columnCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure
|
||||||
|
whitelist={['width']}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<WindowScroller
|
||||||
|
scrollElement={undefined}
|
||||||
|
>
|
||||||
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||||
|
if (!height) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={registerChild}>
|
||||||
|
<Grid
|
||||||
|
ref={this.setGridRef}
|
||||||
|
className={styles.grid}
|
||||||
|
autoHeight={true}
|
||||||
|
height={height}
|
||||||
|
columnCount={columnCount}
|
||||||
|
columnWidth={columnWidth}
|
||||||
|
rowCount={rowCount}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
width={width}
|
||||||
|
onScroll={onChildScroll}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={this.cellRenderer}
|
||||||
|
scrollToAlignment={'start'}
|
||||||
|
isScrollingOptOut={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</WindowScroller>
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCrewPosters.propTypes = {
|
||||||
|
crew: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieCrewPosters;
|
@ -0,0 +1,25 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import MovieCrewPosters from './MovieCrewPosters';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.moviePeople.items,
|
||||||
|
(people) => {
|
||||||
|
const crew = _.reduce(people, (acc, person) => {
|
||||||
|
if (person.type === 'crew') {
|
||||||
|
acc.push(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
crew
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(MovieCrewPosters);
|
@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import MovieImage from './MovieImage';
|
||||||
|
|
||||||
|
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
|
||||||
|
|
||||||
|
function MovieHeadshot(props) {
|
||||||
|
return (
|
||||||
|
<MovieImage
|
||||||
|
{...props}
|
||||||
|
coverType="headshot"
|
||||||
|
placeholder={posterPlaceholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieHeadshot.propTypes = {
|
||||||
|
size: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
MovieHeadshot.defaultProps = {
|
||||||
|
size: 250
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieHeadshot;
|
@ -0,0 +1,81 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import { set, update } from './baseActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'movieCredits';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_MOVIE_CREDITS = 'movieCredits/fetchMovieCredits';
|
||||||
|
export const CLEAR_MOVIE_CREDITS = 'movieCredits/clearMovieCredits';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchMovieCredits = createThunk(FETCH_MOVIE_CREDITS);
|
||||||
|
export const clearMovieCredits = createAction(CLEAR_MOVIE_CREDITS);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
|
[FETCH_MOVIE_CREDITS]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/credit',
|
||||||
|
data: payload
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
update({ section, data }),
|
||||||
|
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: true,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[CLEAR_MOVIE_CREDITS]: (state) => {
|
||||||
|
return Object.assign({}, state, defaultState);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, defaultState, section);
|
Loading…
Reference in new issue