Skip to content

Commit

Permalink
Replace scrolling carousel on homepage (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
trybick authored Jun 26, 2023
1 parent e29f7a2 commit 96b589a
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 59 deletions.
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

0 comments on commit 96b589a

Please sign in to comment.