Recommendations page bug fixes and refactoring (#2578)

* Changed Most Active Studios to Latest Studios
* dynamically create view all link and created message for view all
* created shared determineSlidesToScroll method
* removed added code in Shared/index.ts
* renamed getSlickSettings to getSlickSliderSettings
* Updated row headers to follow plex naming convention
* removed extra s in Sceness
* updated row header css to better align header text with view all anchor
This commit is contained in:
cj
2022-05-11 21:57:41 -05:00
committed by GitHub
parent 31cb8e2cbd
commit dce4591911
9 changed files with 360 additions and 197 deletions

View File

@@ -0,0 +1,37 @@
import React, { FunctionComponent } from "react";
import { FindGalleriesQueryResult } from "src/core/generated-graphql";
import Slider from "react-slick";
import { GalleryCard } from "./GalleryCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
result: FindGalleriesQueryResult;
header: String;
linkText: String;
}
export const GalleryRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
const cardCount = props.result.data?.findGalleries.count;
return (
<div className="recommendation-row gallery-recommendations">
<div className="recommendation-row-head">
<div>
<h2>{props.header}</h2>
</div>
<a href={`/galleries?${props.filter.makeQueryParameters()}`}>
{props.linkText}
</a>
</div>
<Slider {...getSlickSliderSettings(cardCount!, props.isTouch)}>
{props.result.data?.findGalleries.galleries.map((gallery) => (
<GalleryCard key={gallery.id} gallery={gallery} zoomIndex={1} />
))}
</Slider>
</div>
);
};

View File

@@ -0,0 +1,37 @@
import React, { FunctionComponent } from "react";
import { FindMoviesQueryResult } from "src/core/generated-graphql";
import Slider from "react-slick";
import { MovieCard } from "./MovieCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
result: FindMoviesQueryResult;
header: String;
linkText: String;
}
export const MovieRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
const cardCount = props.result.data?.findMovies.count;
return (
<div className="recommendation-row movie-recommendations">
<div className="recommendation-row-head">
<div>
<h2>{props.header}</h2>
</div>
<a href={`/movies?${props.filter.makeQueryParameters()}`}>
{props.linkText}
</a>
</div>
<Slider {...getSlickSliderSettings(cardCount!, props.isTouch)}>
{props.result.data?.findMovies.movies.map((p) => (
<MovieCard key={p.id} movie={p} />
))}
</Slider>
</div>
);
};

View File

@@ -0,0 +1,37 @@
import React, { FunctionComponent } from "react";
import { FindPerformersQueryResult } from "src/core/generated-graphql";
import Slider from "react-slick";
import { PerformerCard } from "./PerformerCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
result: FindPerformersQueryResult;
header: String;
linkText: String;
}
export const PerformerRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
const cardCount = props.result.data?.findPerformers.count;
return (
<div className="recommendation-row performer-recommendations">
<div className="recommendation-row-head">
<div>
<h2>{props.header}</h2>
</div>
<a href={`/performers?${props.filter.makeQueryParameters()}`}>
{props.linkText}
</a>
</div>
<Slider {...getSlickSliderSettings(cardCount!, props.isTouch)}>
{props.result.data?.findPerformers.performers.map((p) => (
<PerformerCard key={p.id} performer={p} />
))}
</Slider>
</div>
);
};

View File

@@ -8,14 +8,14 @@ import {
useFindGalleries, useFindGalleries,
useFindPerformers, useFindPerformers,
} from "src/core/StashService"; } from "src/core/StashService";
import { SceneCard } from "src/components/Scenes/SceneCard"; import { SceneRecommendationRow } from "src/components/Scenes/SceneRecommendationRow";
import { StudioCard } from "src/components/Studios/StudioCard"; import { StudioRecommendationRow } from "src/components/Studios/StudioRecommendationRow";
import { MovieCard } from "src/components/Movies/MovieCard"; import { MovieRecommendationRow } from "src/components/Movies/MovieRecommendationRow";
import { PerformerCard } from "src/components/Performers/PerformerCard"; import { PerformerRecommendationRow } from "src/components/Performers/PerformerRecommendationRow";
import { GalleryCard } from "src/components/Galleries/GalleryCard"; import { GalleryRecommendationRow } from "src/components/Galleries/GalleryRecommendationRow";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import Slider from "react-slick"; import { LoadingIndicator } from "src/components/Shared";
const Recommendations: React.FC = () => { const Recommendations: React.FC = () => {
function isTouchEnabled() { function isTouchEnabled() {
@@ -31,50 +31,35 @@ const Recommendations: React.FC = () => {
scenefilter.sortDirection = GQL.SortDirectionEnum.Desc; scenefilter.sortDirection = GQL.SortDirectionEnum.Desc;
scenefilter.itemsPerPage = itemsPerPage; scenefilter.itemsPerPage = itemsPerPage;
const sceneResult = useFindScenes(scenefilter); const sceneResult = useFindScenes(scenefilter);
const hasScenes = const hasScenes = !!sceneResult?.data?.findScenes?.count;
sceneResult.data &&
sceneResult.data.findScenes &&
sceneResult.data.findScenes.count > 0;
const studiofilter = new ListFilterModel(GQL.FilterMode.Studios); const studiofilter = new ListFilterModel(GQL.FilterMode.Studios);
studiofilter.sortBy = "scenes_count"; studiofilter.sortBy = "created_at";
studiofilter.sortDirection = GQL.SortDirectionEnum.Desc; studiofilter.sortDirection = GQL.SortDirectionEnum.Desc;
studiofilter.itemsPerPage = itemsPerPage; studiofilter.itemsPerPage = itemsPerPage;
const studioResult = useFindStudios(studiofilter); const studioResult = useFindStudios(studiofilter);
const hasStudios = const hasStudios = !!studioResult?.data?.findStudios?.count;
studioResult.data &&
studioResult.data.findStudios &&
studioResult.data.findStudios.count > 0;
const moviefilter = new ListFilterModel(GQL.FilterMode.Movies); const moviefilter = new ListFilterModel(GQL.FilterMode.Movies);
moviefilter.sortBy = "date"; moviefilter.sortBy = "date";
moviefilter.sortDirection = GQL.SortDirectionEnum.Desc; moviefilter.sortDirection = GQL.SortDirectionEnum.Desc;
moviefilter.itemsPerPage = itemsPerPage; moviefilter.itemsPerPage = itemsPerPage;
const movieResult = useFindMovies(moviefilter); const movieResult = useFindMovies(moviefilter);
const hasMovies = const hasMovies = !!movieResult?.data?.findMovies?.count;
movieResult.data &&
movieResult.data.findMovies &&
movieResult.data.findMovies.count > 0;
const performerfilter = new ListFilterModel(GQL.FilterMode.Performers); const performerfilter = new ListFilterModel(GQL.FilterMode.Performers);
performerfilter.sortBy = "created_at"; performerfilter.sortBy = "created_at";
performerfilter.sortDirection = GQL.SortDirectionEnum.Desc; performerfilter.sortDirection = GQL.SortDirectionEnum.Desc;
performerfilter.itemsPerPage = itemsPerPage; performerfilter.itemsPerPage = itemsPerPage;
const performerResult = useFindPerformers(performerfilter); const performerResult = useFindPerformers(performerfilter);
const hasPerformers = const hasPerformers = !!performerResult?.data?.findPerformers?.count;
performerResult.data &&
performerResult.data.findPerformers &&
performerResult.data.findPerformers.count > 0;
const galleryfilter = new ListFilterModel(GQL.FilterMode.Galleries); const galleryfilter = new ListFilterModel(GQL.FilterMode.Galleries);
galleryfilter.sortBy = "date"; galleryfilter.sortBy = "date";
galleryfilter.sortDirection = GQL.SortDirectionEnum.Desc; galleryfilter.sortDirection = GQL.SortDirectionEnum.Desc;
galleryfilter.itemsPerPage = itemsPerPage; galleryfilter.itemsPerPage = itemsPerPage;
const galleryResult = useFindGalleries(galleryfilter); const galleryResult = useFindGalleries(galleryfilter);
const hasGalleries = const hasGalleries = !!galleryResult?.data?.findGalleries?.count;
galleryResult.data &&
galleryResult.data.findGalleries &&
galleryResult.data.findGalleries.count > 0;
const messages = defineMessages({ const messages = defineMessages({
emptyServer: { emptyServer: {
@@ -82,182 +67,108 @@ const Recommendations: React.FC = () => {
defaultMessage: defaultMessage:
"Add some scenes to your server to view recommendations on this page.", "Add some scenes to your server to view recommendations on this page.",
}, },
latestScenes: { recentlyAddedStudios: {
id: "latest_scenes", id: "recently_added_studios",
defaultMessage: "Latest Scenes", defaultMessage: "Recently Added Studios",
}, },
mostActiveStudios: { recentlyAddedPerformers: {
id: "most_active_studios", id: "recently_added_performers",
defaultMessage: "Most Active Studios", defaultMessage: "Recently Added Performers",
}, },
latestMovies: { recentlyReleasedGalleries: {
id: "latest_movies", id: "recently_released_galleries",
defaultMessage: "Latest Movies", defaultMessage: "Recently Released Galleries",
}, },
latestPerformers: { recentlyReleasedMovies: {
id: "latest_performers", id: "recently_released_movies",
defaultMessage: "Latest Performers", defaultMessage: "Recently Released Movies",
}, },
latestGalleries: { recentlyReleasedScenes: {
id: "latest_galleries", id: "recently_released_scenes",
defaultMessage: "Latest Galleries", defaultMessage: "Recently Released Scenes",
},
viewAll: {
id: "view_all",
defaultMessage: "View All",
}, },
}); });
var settings = { if (
dots: !isTouch, sceneResult.loading ||
arrows: !isTouch, studioResult.loading ||
infinite: !isTouch, movieResult.loading ||
speed: 300, performerResult.loading ||
variableWidth: true, galleryResult.loading
swipeToSlide: true, ) {
slidesToShow: 5, return <LoadingIndicator />;
slidesToScroll: !isTouch ? 5 : 1, } else {
responsive: [ return (
{ <div className="recommendations-container">
breakpoint: 1909, {!hasScenes &&
settings: { !hasStudios &&
slidesToShow: 4, !hasMovies &&
slidesToScroll: !isTouch ? 4 : 1, !hasPerformers &&
}, !hasGalleries ? (
}, <div className="no-recommendations">
{ {intl.formatMessage(messages.emptyServer)}
breakpoint: 1542, </div>
settings: { ) : (
slidesToShow: 3, <div>
slidesToScroll: !isTouch ? 3 : 1, {hasScenes && (
}, <SceneRecommendationRow
}, isTouch={isTouch}
{ filter={scenefilter}
breakpoint: 1170, result={sceneResult}
settings: { queue={SceneQueue.fromListFilterModel(scenefilter)}
slidesToShow: 2, header={intl.formatMessage(messages.recentlyReleasedScenes)}
slidesToScroll: !isTouch ? 2 : 1, linkText={intl.formatMessage(messages.viewAll)}
}, />
}, )}
{
breakpoint: 801,
settings: {
slidesToShow: 1,
slidesToScroll: 1,
dots: false,
},
},
],
};
const queue = SceneQueue.fromListFilterModel(scenefilter);
return ( {hasStudios && (
<div className="recommendations-container"> <StudioRecommendationRow
{!hasScenes && isTouch={isTouch}
!hasStudios && filter={studiofilter}
!hasMovies && result={studioResult}
!hasPerformers && header={intl.formatMessage(messages.recentlyAddedStudios)}
!hasGalleries ? ( linkText={intl.formatMessage(messages.viewAll)}
<div className="no-recommendations"> />
{intl.formatMessage(messages.emptyServer)} )}
</div>
) : (
<div>
{hasScenes && (
<div className="recommendation-row">
<div className="recommendation-row-head">
<div>
<h2>{intl.formatMessage(messages.latestScenes)}</h2>
</div>
<a href="/scenes?sortby=date&sortdir=desc">View all</a>
</div>
<Slider {...settings}>
{sceneResult.data?.findScenes.scenes.map((scene, index) => (
<SceneCard
key={scene.id}
scene={scene}
queue={queue}
index={index}
zoomIndex={1}
/>
))}
</Slider>
</div>
)}
{hasStudios && ( {hasMovies && (
<div className="recommendation-row"> <MovieRecommendationRow
<div className="recommendation-row-head"> isTouch={isTouch}
<div> filter={moviefilter}
<h2>{intl.formatMessage(messages.mostActiveStudios)}</h2> result={movieResult}
</div> header={intl.formatMessage(messages.recentlyReleasedMovies)}
<a href="/studios?sortby=scenes_count&sortdir=desc">View all</a> linkText={intl.formatMessage(messages.viewAll)}
</div> />
<Slider {...settings}> )}
{studioResult.data?.findStudios.studios.map((studio) => (
<StudioCard
key={studio.id}
studio={studio}
hideParent={true}
/>
))}
</Slider>
</div>
)}
{hasMovies && ( {hasPerformers && (
<div className="recommendation-row"> <PerformerRecommendationRow
<div className="recommendation-row-head"> isTouch={isTouch}
<div> filter={performerfilter}
<h2>{intl.formatMessage(messages.latestMovies)}</h2> result={performerResult}
</div> header={intl.formatMessage(messages.recentlyAddedPerformers)}
<a href="/movies?sortby=date&sortdir=desc">View all</a> linkText={intl.formatMessage(messages.viewAll)}
</div> />
<Slider {...settings}> )}
{movieResult.data?.findMovies.movies.map((p) => (
<MovieCard key={p.id} movie={p} />
))}
</Slider>
</div>
)}
{hasPerformers && ( {hasGalleries && (
<div className="recommendation-row"> <GalleryRecommendationRow
<div className="recommendation-row-head"> isTouch={isTouch}
<div> filter={galleryfilter}
<h2>{intl.formatMessage(messages.latestPerformers)}</h2> result={galleryResult}
</div> header={intl.formatMessage(messages.recentlyReleasedGalleries)}
<a href="/performers?sortby=created_at&sortdir=desc"> linkText={intl.formatMessage(messages.viewAll)}
View all />
</a> )}
</div> </div>
<Slider {...settings}> )}
{performerResult.data?.findPerformers.performers.map((p) => ( </div>
<PerformerCard key={p.id} performer={p} /> );
))} }
</Slider>
</div>
)}
{hasGalleries && (
<div className="recommendation-row">
<div className="recommendation-row-head">
<div>
<h2>{intl.formatMessage(messages.latestGalleries)}</h2>
</div>
<a href="/galleries?sortby=date&sortdir=desc">View all</a>
</div>
<Slider {...settings}>
{galleryResult.data?.findGalleries.galleries.map((gallery) => (
<GalleryCard
key={gallery.id}
gallery={gallery}
zoomIndex={1}
/>
))}
</Slider>
</div>
)}
</div>
)}
</div>
);
}; };
export default Recommendations; export default Recommendations;

View File

@@ -28,6 +28,7 @@
display: inline-flex; display: inline-flex;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
margin-bottom: 0;
text-transform: uppercase; text-transform: uppercase;
white-space: normal; white-space: normal;
} }

View File

@@ -0,0 +1,45 @@
import React, { FunctionComponent } from "react";
import { FindScenesQueryResult } from "src/core/generated-graphql";
import Slider from "react-slick";
import { SceneCard } from "./SceneCard";
import { SceneQueue } from "src/models/sceneQueue";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
result: FindScenesQueryResult;
queue: SceneQueue;
header: String;
linkText: String;
}
export const SceneRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
const cardCount = props.result.data?.findScenes.count;
return (
<div className="recommendation-row scene-recommendations">
<div className="recommendation-row-head">
<div>
<h2>{props.header}</h2>
</div>
<a href={`/scenes?${props.filter.makeQueryParameters()}`}>
{props.linkText}
</a>
</div>
<Slider {...getSlickSliderSettings(cardCount!, props.isTouch)}>
{props.result.data?.findScenes.scenes.map((scene, index) => (
<SceneCard
key={scene.id}
scene={scene}
queue={props.queue}
index={index}
zoomIndex={1}
/>
))}
</Slider>
</div>
);
};

View File

@@ -0,0 +1,37 @@
import React, { FunctionComponent } from "react";
import { FindStudiosQueryResult } from "src/core/generated-graphql";
import Slider from "react-slick";
import { StudioCard } from "./StudioCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
result: FindStudiosQueryResult;
header: String;
linkText: String;
}
export const StudioRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
const cardCount = props.result.data?.findStudios.count;
return (
<div className="recommendation-row studio-recommendations">
<div className="recommendation-row-head">
<div>
<h2>{props.header}</h2>
</div>
<a href={`/studios?${props.filter.makeQueryParameters()}`}>
{props.linkText}
</a>
</div>
<Slider {...getSlickSliderSettings(cardCount!, props.isTouch)}>
{props.result.data?.findStudios.studios.map((studio) => (
<StudioCard key={studio.id} studio={studio} hideParent={true} />
))}
</Slider>
</div>
);
};

View File

@@ -0,0 +1,57 @@
function determineSlidesToScroll(
cardCount: number,
prefered: number,
isTouch: boolean
) {
if (isTouch) {
return 1;
} else if (cardCount! > prefered) {
return prefered;
} else {
return cardCount;
}
}
export function getSlickSliderSettings(cardCount: number, isTouch: boolean) {
return {
dots: !isTouch,
arrows: !isTouch,
infinite: !isTouch,
speed: 300,
variableWidth: true,
swipeToSlide: true,
slidesToShow: cardCount! > 5 ? 5 : cardCount,
slidesToScroll: determineSlidesToScroll(cardCount!, 5, isTouch),
responsive: [
{
breakpoint: 1909,
settings: {
slidesToShow: cardCount! > 4 ? 4 : cardCount,
slidesToScroll: determineSlidesToScroll(cardCount!, 4, isTouch),
},
},
{
breakpoint: 1542,
settings: {
slidesToShow: cardCount! > 3 ? 3 : cardCount,
slidesToScroll: determineSlidesToScroll(cardCount!, 3, isTouch),
},
},
{
breakpoint: 1170,
settings: {
slidesToShow: cardCount! > 2 ? 2 : cardCount,
slidesToScroll: determineSlidesToScroll(cardCount!, 2, isTouch),
},
},
{
breakpoint: 801,
settings: {
slidesToShow: 1,
slidesToScroll: 1,
dots: false,
},
},
],
};
}

View File

@@ -769,10 +769,6 @@
"interactive": "Interactive", "interactive": "Interactive",
"interactive_speed": "Interactive speed", "interactive_speed": "Interactive speed",
"isMissing": "Is Missing", "isMissing": "Is Missing",
"latest_galleries": "Latest Galleries",
"latest_movies": "Latest Movies",
"latest_performers": "Latest Performers",
"latest_scenes": "Latest Scenes",
"library": "Library", "library": "Library",
"loading": { "loading": {
"generic": "Loading…" "generic": "Loading…"
@@ -796,7 +792,6 @@
}, },
"megabits_per_second": "{value} megabits per second", "megabits_per_second": "{value} megabits per second",
"metadata": "Metadata", "metadata": "Metadata",
"most_active_studios": "Most Active Studios",
"movie": "Movie", "movie": "Movie",
"movie_scene_number": "Movie Scene Number", "movie_scene_number": "Movie Scene Number",
"movies": "Movies", "movies": "Movies",
@@ -830,6 +825,11 @@
"queue": "Queue", "queue": "Queue",
"random": "Random", "random": "Random",
"rating": "Rating", "rating": "Rating",
"recently_added_performers": "Recently Added Performers",
"recently_added_studios": "Recently Added Studios",
"recently_released_galleries": "Recently Released Galleries",
"recently_released_movies": "Recently Released Movies",
"recently_released_scenes": "Recently Released Scenes",
"resolution": "Resolution", "resolution": "Resolution",
"scene": "Scene", "scene": "Scene",
"sceneTagger": "Scene Tagger", "sceneTagger": "Scene Tagger",
@@ -969,6 +969,7 @@
"updated_at": "Updated At", "updated_at": "Updated At",
"url": "URL", "url": "URL",
"videos": "Videos", "videos": "Videos",
"view_all": "View All",
"weight": "Weight", "weight": "Weight",
"years_old": "years old", "years_old": "years old",
"stashbox": { "stashbox": {