Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace scrolling carousel on homepage #134

Merged
merged 1 commit into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion front/src/components/common/FollowButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const FollowButton = ({
<Button
colorScheme="cyan"
isLoading={isLoading}
leftIcon={<SmallAddIcon />}
leftIcon={<SmallAddIcon fontSize="22px" marginInlineEnd="-0.2rem" />}
onClick={onFollowShow}
variant="outline"
{...(unfollowedWidth && { minW: unfollowedWidth })}
Expand Down
4 changes: 2 additions & 2 deletions front/src/components/following/FollowingList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const FollowingList = () => {
px={isMobile ? '10px' : 'unset'}
variant={isMobile ? 'enclosed' : 'solid-rounded'}
>
<TabList mb="18px">
<Tab mr="4px">All Shows</Tab>
<TabList mb="19px">
<Tab mr="4px">All</Tab>
<Tab isDisabled={!activeSeasonShows.length} mr="4px">
Airing Now
</Tab>
Expand Down
2 changes: 1 addition & 1 deletion front/src/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const Header = ({ email, isLoggedIn, setIsLoggedOut }: Props) => {
mr: 'unset',
})}
>
<NavLink linkTo={ROUTES.HOME} text="Search" />
<NavLink linkTo={ROUTES.HOME} text="Discover" />
<NavLink linkTo={ROUTES.CALENDAR} text="Calendar" />
<NavLink linkTo={ROUTES.FOLLOWING} text="Following" />
{isLoggedIn ? (
Expand Down
176 changes: 131 additions & 45 deletions front/src/components/popularShows/PopularShows.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,155 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Box, Flex, Heading } from '@chakra-ui/react';
import { css } from '@emotion/react';
import {
Box,
Button,
Collapse,
Flex,
Heading,
useBreakpointValue,
useDisclosure,
} from '@chakra-ui/react';
import { AiOutlineCaretDown, AiOutlineCaretUp } from 'react-icons/ai';
import styled from '@emotion/styled';
import { useAppDispatch } from 'store';
import { getPopularShowsAction } from 'store/tv/actions';
import { selectPopularShowsForDisplay } from 'store/tv/selectors';
import { selectPopularShowsForDisplay, selectTopRatedShowsForDisplay } from 'store/tv/selectors';
import PopularShow from './subcomponents/PopularShow';
import { useIsMobile } from '../../hooks/useIsMobile';

const fadeCss = css`
&:after {
content: '';
position: absolute;
bottom: -17px;
right: 0;
top: 50px;
width: 50px;
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 100%
);
will-change: opacity;
}
const StyledCollapse = styled(Collapse)`
flex-grow: 1;
`;

const PopularShows = () => {
const dispatch = useAppDispatch();
const popularShows = useSelector(selectPopularShowsForDisplay);
const topRatedShows = useSelector(selectTopRatedShowsForDisplay);
const isMobile = useIsMobile();
const { isOpen, onToggle } = useDisclosure();

useEffect(() => {
dispatch(getPopularShowsAction());
}, [dispatch]);

// Handle hiding the fade effect on the last show
const [shouldFade, setShouldFade] = useState(true);
const scrollWrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ref = scrollWrapperRef;
function handleHoriztonalScroll(event: any) {
const isScrollAtEnd = event.srcElement.scrollLeft > 2410;
isScrollAtEnd ? setShouldFade(false) : setShouldFade(true);
}
ref.current?.addEventListener('scroll', handleHoriztonalScroll);
return function cleanup() {
ref.current?.removeEventListener('scroll', handleHoriztonalScroll);
};
});
const numberShowsInFirstRow =
useBreakpointValue(
{
base: 2,
md: 3,
lg: 4,
xl: 5,
'2xl': 6,
},
{ ssr: false }
) || 2;
const numberRowsToRender = Math.ceil(popularShows.length / numberShowsInFirstRow) - 1;

return (
<Box h="330px" m="18px 0 30px" maxW="100%" position="relative">
<Heading as="h2" fontSize="1.3rem" fontWeight="600" m="0 18px 25px">
Popular Now
<Box m="18px 0 30px" maxW="1500px" w="95%">
<Heading
as="h2"
fontSize="1.9rem"
fontWeight="600"
mb="22px"
mx="4px"
textAlign={isMobile ? 'center' : 'left'}
>
Popular Shows
</Heading>
<Flex
css={shouldFade && fadeCss}
maxW="800px"
gap="34px 30px"
justifyContent={isMobile ? 'center' : 'space-between'}
mt="14px"
overflow="auto"
ref={scrollWrapperRef}
wrap="wrap"
>
<Flex justifyContent="center">
{popularShows?.slice(0, 20).map(show => (
<PopularShow key={show.id} show={show} />
{popularShows?.slice(0, numberShowsInFirstRow).map(show => (
<PopularShow isMobile={isMobile} key={show.id} show={show} />
))}
<StyledCollapse in={isOpen}>
{[...Array(numberRowsToRender).keys()].map(i => (
<Flex
_notLast={{
justifyContent: isMobile ? 'center' : 'space-between',
marginBottom: '34px',
}}
columnGap="30px"
key={`row-${i}`}
>
{popularShows
?.slice(
(i + 1) * numberShowsInFirstRow,
(i + 1) * numberShowsInFirstRow + numberShowsInFirstRow
)
.map(show => (
<PopularShow isMobile={isMobile} key={show.id} show={show} />
))}
</Flex>
))}
<Box bg="transparent" w="20px" />
</Flex>
</StyledCollapse>
</Flex>
<Flex>
<Button
mt="20px"
mx="auto"
onClick={onToggle}
rightIcon={isOpen ? <AiOutlineCaretUp /> : <AiOutlineCaretDown />}
>
{isOpen ? 'Less ' : 'More Popular Shows'}
</Button>
</Flex>

<Heading
as="h2"
fontSize="1.9rem"
fontWeight="600"
mb="22px"
mt={isMobile ? '40px' : '30px'}
mx="4px"
textAlign={isMobile ? 'center' : 'left'}
>
Top Rated Shows
</Heading>
<Flex
gap="34px 30px"
justifyContent={isMobile ? 'center' : 'space-between'}
mt="14px"
wrap="wrap"
>
{topRatedShows?.slice(0, numberShowsInFirstRow).map(show => (
<PopularShow isMobile={isMobile} key={show.id} show={show} />
))}
<StyledCollapse in={isOpen}>
{[...Array(numberRowsToRender).keys()].map(i => (
<Flex
_notLast={{
justifyContent: isMobile ? 'center' : 'space-between',
marginBottom: '34px',
}}
columnGap="30px"
key={`row-${i}`}
>
{topRatedShows
?.slice(
(i + 1) * numberShowsInFirstRow,
(i + 1) * numberShowsInFirstRow + numberShowsInFirstRow
)
.map(show => (
<PopularShow isMobile={isMobile} key={show.id} show={show} />
))}
</Flex>
))}
</StyledCollapse>
</Flex>
<Flex>
<Button
mt="20px"
mx="auto"
onClick={onToggle}
rightIcon={isOpen ? <AiOutlineCaretUp /> : <AiOutlineCaretDown />}
>
{isOpen ? 'Less' : 'More Top Rated Shows'}
</Button>
</Flex>
</Box>
);
Expand Down
24 changes: 15 additions & 9 deletions front/src/components/popularShows/subcomponents/PopularShow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,40 @@ import { Box, Flex, Heading, Image, Link } from '@chakra-ui/react';
import { PopularShow as PopularShowType } from 'types/external';
import { fallbackImagePath } from 'constants/strings';
import { ROUTES } from 'constants/routes';
import { imagePath154 } from 'constants/strings';
import { imagePath342 } from 'constants/strings';
import FollowButton from 'components/common/FollowButton';

type Props = {
show: PopularShowType;
isMobile: boolean;
};

const PopularShow = ({ show: { id, name, posterPath } }: Props) => (
<Box borderRadius="8px" borderWidth="1px" mb="18px" minW="0" ml="20px" w="144px">
const PopularShow = ({ show: { id, name, posterPath }, isMobile }: Props) => (
<Box
borderRadius="8px"
borderWidth="1px"
flexGrow="1"
maxW="270px"
w={isMobile ? '140px' : '190px'}
>
<Link as={RouterLink} to={`${ROUTES.SHOW}/${id}`}>
<Image
alt={`popular-show-${name}`}
borderRadius="8px 8px 0 0"
cursor="pointer"
fallbackSrc={fallbackImagePath}
fallbackStrategy="onError"
h="213px"
src={posterPath ? imagePath154 + posterPath : fallbackImagePath}
w="142px"
src={posterPath ? imagePath342 + posterPath : fallbackImagePath}
w="100%"
/>
</Link>
<Flex direction="column" p="8px 12px">
<Flex direction="column" mt="5px" p="8px 12px">
<Link as={RouterLink} to={`${ROUTES.SHOW}/${id}`}>
<Heading fontSize="sm" noOfLines={1} textAlign="center">
<Heading fontSize="md" noOfLines={1} textAlign="center">
{name}
</Heading>
</Link>
<FollowButton m="12px auto 6px" minW="108px" showId={id} size="sm" />
<FollowButton m="14px auto 9px" minW="108px" showId={id} size={isMobile ? 'sm' : 'md'} />
</Flex>
</Box>
);
Expand Down
31 changes: 31 additions & 0 deletions front/src/store/tv/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const SAVE_BASIC_SHOW_INFO_FOR_FOLLOWED_SHOWS = 'SAVE_BASIC_SHOW_INFO_FOR
export const SAVE_BASIC_SHOW_INFO_FOR_SHOW = 'SAVE_BASIC_SHOW_INFO_FOR_SHOW';
export const SET_IS_LOADING_BASIC_SHOW_INFO_FOR_SHOW = 'SET_IS_LOADING_BASIC_SHOW_INFO_FOR_SHOW';
export const SAVE_POPULAR_SHOWS = 'SAVE_POPULAR_SHOWS';
export const SAVE_TOP_RATED_SHOWS = 'SAVE_TOP_RATED_SHOWS';

export const saveSearchQueryAction =
(query: SavedQuery): AppThunk =>
Expand Down Expand Up @@ -183,6 +184,12 @@ export const getPopularShowsAction = (): AppThunk => (dispatch, getState) => {
axios
// The Popular Shows feature used to use the '/tv/popular' endpoint but that was returning
// a lot foreign shows. Using the '/trending' endpoint seems to have better results.
// Full possibly useful endpoints status:
// - /trending = useful, current Popular Shows list
// - /top-rated = useful and accurate
// - /popular = not useful, foreign shows
// - /airing_today = not useful, foreign shows
// - /on_the_air = not useful, foreign shows
.get(`${ENDPOINTS.THE_MOVIE_DB}/trending/tv/week`, {
params: { api_key: import.meta.env.VITE_THE_MOVIE_DB_KEY },
})
Expand All @@ -196,3 +203,27 @@ export const getPopularShowsAction = (): AppThunk => (dispatch, getState) => {
.catch(handleErrors);
}
};

export const getTopRatedShowsAction = (): AppThunk => (dispatch, getState) => {
const { topRatedShows: cachedTopRatedShows } = getState().tv;
const cacheAge =
cachedTopRatedShows?.length &&
cachedTopRatedShows[0].fetchedAt &&
moment().diff(moment(cachedTopRatedShows[0].fetchedAt), 'days');
const isCacheValid = cachedTopRatedShows?.length && cacheDurationDays.popularShows > cacheAge;

if (!isCacheValid) {
axios
.get(`${ENDPOINTS.THE_MOVIE_DB}/tv/top_rated`, {
params: { api_key: import.meta.env.VITE_THE_MOVIE_DB_KEY },
})
.then(({ data: { results } }) => {
const dataWithTimestamp = results.map((show: any) => ({ ...show, fetchedAt: moment() }));
dispatch({
type: SAVE_TOP_RATED_SHOWS,
payload: dataWithTimestamp,
});
})
.catch(handleErrors);
}
};
9 changes: 9 additions & 0 deletions front/src/store/tv/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SAVE_BASIC_SHOW_INFO_FOR_SHOW,
SAVE_CALENDAR_EPISODES_CACHE,
SAVE_POPULAR_SHOWS,
SAVE_TOP_RATED_SHOWS,
SET_CURRENT_CALENDAR_EPISODES,
SET_IS_LOADING_BASIC_SHOW_INFO_FOR_SHOW,
SET_SEARCH_QUERY,
Expand All @@ -18,6 +19,7 @@ export type TvState = {
isLoadingBasicShowInfoForShow: boolean;
calendarEpisodesForDisplay: CalendarEpisode[];
popularShows: Record<string, any>[];
topRatedShows: Record<string, any>[];
};

const initialState = {
Expand All @@ -27,6 +29,7 @@ const initialState = {
isLoadingBasicShowInfoForShow: false,
calendarEpisodesForDisplay: [],
popularShows: [],
topRatedShows: [],
};

export const tvReducer: Reducer<TvState, Action> = (state = initialState, action: AnyAction) => {
Expand Down Expand Up @@ -79,6 +82,12 @@ export const tvReducer: Reducer<TvState, Action> = (state = initialState, action
popularShows: action.payload,
};
}
case SAVE_TOP_RATED_SHOWS: {
return {
...state,
topRatedShows: action.payload,
};
}
default:
return state;
}
Expand Down
16 changes: 16 additions & 0 deletions front/src/store/tv/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const selectIsLoadingBasicShowInfoForShow = (state: AppState) =>
export const selectCalendarEpisodesForDisplay = (state: AppState) =>
state.tv.calendarEpisodesForDisplay;
export const selectPopularShows = (state: AppState) => state.tv.popularShows;
export const selectTopRatedShows = (state: AppState) => state.tv.topRatedShows;

export const selectBasicShowInfoForFollowedShows: Selector<AppState, BasicShowInfo[]> =
createSelector(selectBasicShowInfo, selectFollowedShows, (showInfo, followedShows) => {
Expand Down Expand Up @@ -55,6 +56,21 @@ export const selectPopularShowsForDisplay: Selector<AppState, PopularShow[]> = c
})
);

export const selectTopRatedShowsForDisplay: Selector<AppState, PopularShow[]> = createSelector(
selectTopRatedShows,
shows =>
shows &&
Object.values(shows)?.map(show => {
const { id, fetchedAt, name, poster_path: posterPath } = show;
return {
id,
fetchedAt,
name,
posterPath,
};
})
);

export const getCurrentShowId = (): ID => {
const id = window.location.pathname.split('/')[2];
if (!id) {
Expand Down
Loading