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

View file

@ -28,6 +28,7 @@
display: inline-flex;
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0;
text-transform: uppercase;
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_speed": "Interactive speed",
"isMissing": "Is Missing",
"latest_galleries": "Latest Galleries",
"latest_movies": "Latest Movies",
"latest_performers": "Latest Performers",
"latest_scenes": "Latest Scenes",
"library": "Library",
"loading": {
"generic": "Loading…"
@ -796,7 +792,6 @@
},
"megabits_per_second": "{value} megabits per second",
"metadata": "Metadata",
"most_active_studios": "Most Active Studios",
"movie": "Movie",
"movie_scene_number": "Movie Scene Number",
"movies": "Movies",
@ -830,6 +825,11 @@
"queue": "Queue",
"random": "Random",
"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",
"scene": "Scene",
"sceneTagger": "Scene Tagger",
@ -969,6 +969,7 @@
"updated_at": "Updated At",
"url": "URL",
"videos": "Videos",
"view_all": "View All",
"weight": "Weight",
"years_old": "years old",
"stashbox": {