From 6afb721be982212221c0ee4e07fc2cf5bdbfbda0 Mon Sep 17 00:00:00 2001 From: ALC Consulting Date: Mon, 6 Mar 2023 13:44:42 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(website)=20list=20lives=20on=20the=20?= =?UTF-8?q?website?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit List the lives on the website: - add link on the menu - integrate router - list lives --- CHANGELOG.md | 1 + .../src/assets/svg/iko_live.svg | 21 ++ .../components/Contents/Contents.spec.tsx | 5 +- .../Contents/components/Contents/Contents.tsx | 18 +- .../ContentsRouter/ContentsRouter.tsx | 5 +- .../components/Read/ClassRoomItem.tsx | 10 +- .../Live/components/LiveRouter.spec.tsx | 29 +++ .../features/Live/components/LiveRouter.tsx | 18 ++ .../Live/components/Read/Live.spec.tsx | 63 ++++++ .../features/Live/components/Read/Live.tsx | 68 +++++++ .../Live/components/Read/Lives.spec.tsx | 179 ++++++++++++++++++ .../features/Live/components/Read/Lives.tsx | 52 +++++ .../features/Contents/features/Live/index.tsx | 2 + .../src/features/Contents/index.tsx | 1 + .../standalone_site/src/routes/routes.tsx | 28 +-- 15 files changed, 481 insertions(+), 19 deletions(-) create mode 100644 src/frontend/apps/standalone_site/src/assets/svg/iko_live.svg create mode 100644 src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.spec.tsx create mode 100644 src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.tsx create mode 100644 src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.spec.tsx create mode 100644 src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.tsx create mode 100644 src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.spec.tsx create mode 100644 src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.tsx create mode 100644 src/frontend/apps/standalone_site/src/features/Contents/features/Live/index.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e83fae6dd..92822c4b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Versioning](https://semver.org/spec/v2.0.0.html). - standalone website: - Integrate VOD dashboard (#2086) + - List the lives in the contents section (#2104) ## [4.0.0-beta.17] - 2023-03-03 diff --git a/src/frontend/apps/standalone_site/src/assets/svg/iko_live.svg b/src/frontend/apps/standalone_site/src/assets/svg/iko_live.svg new file mode 100644 index 0000000000..b893990863 --- /dev/null +++ b/src/frontend/apps/standalone_site/src/assets/svg/iko_live.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.spec.tsx b/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.spec.tsx index cac79d2b17..b6d205529f 100644 --- a/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.spec.tsx +++ b/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.spec.tsx @@ -6,6 +6,7 @@ import Contents from './Contents'; jest.mock('features/Contents', () => ({ ClassRooms: () =>
Classrooms Component
, Videos: () =>
Videos Component
, + Lives: () =>
Lives Component
, })); describe('', () => { @@ -13,8 +14,10 @@ describe('', () => { render(); expect(screen.getByText(/My Classrooms/)).toBeInTheDocument(); expect(screen.getByText(/My Videos/)).toBeInTheDocument(); + expect(screen.getByText(/My Lives/)).toBeInTheDocument(); expect(screen.getByText(/Classrooms Component/i)).toBeInTheDocument(); + expect(screen.getByText(/Lives Component/i)).toBeInTheDocument(); expect(screen.getByText(/Videos Component/i)).toBeInTheDocument(); - expect(screen.getAllByText(/See Everything/i)).toHaveLength(2); + expect(screen.getAllByText(/See Everything/i)).toHaveLength(3); }); }); diff --git a/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.tsx b/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.tsx index d18d2cf1e4..05644fab94 100644 --- a/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.tsx +++ b/src/frontend/apps/standalone_site/src/features/Contents/components/Contents/Contents.tsx @@ -3,10 +3,15 @@ import { StyledLink } from 'lib-components'; import { defineMessages, useIntl } from 'react-intl'; import styled from 'styled-components'; -import { ClassRooms, Videos } from 'features/Contents'; +import { ClassRooms, Videos, Lives } from 'features/Contents'; import { routes } from 'routes'; const messages = defineMessages({ + MyLives: { + defaultMessage: 'My Lives', + description: 'My contents page, my lives title', + id: 'features.Contents.Contents.MyLives', + }, MyVideos: { defaultMessage: 'My Videos', description: 'My contents page, my videos title', @@ -33,6 +38,17 @@ const Contents = () => { return ( + + + {intl.formatMessage(messages.MyLives)} + + + › {intl.formatMessage(messages.SeeEverything)} + + + + + {intl.formatMessage(messages.MyVideos)} diff --git a/src/frontend/apps/standalone_site/src/features/Contents/components/ContentsRouter/ContentsRouter.tsx b/src/frontend/apps/standalone_site/src/features/Contents/components/ContentsRouter/ContentsRouter.tsx index 8b8cd2d409..8e761d3e6c 100644 --- a/src/frontend/apps/standalone_site/src/features/Contents/components/ContentsRouter/ContentsRouter.tsx +++ b/src/frontend/apps/standalone_site/src/features/Contents/components/ContentsRouter/ContentsRouter.tsx @@ -1,6 +1,6 @@ import { Route, Switch } from 'react-router-dom'; -import { ClassRoomRouter, VideoRouter } from 'features/Contents'; +import { ClassRoomRouter, VideoRouter, LiveRouter } from 'features/Contents'; import { routes } from 'routes'; import Contents from '../Contents/Contents'; @@ -17,6 +17,9 @@ const ContentsRouter = () => { + + + ); }; diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/ClassRoom/components/Read/ClassRoomItem.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/ClassRoom/components/Read/ClassRoomItem.tsx index 7063a9d075..baac327730 100644 --- a/src/frontend/apps/standalone_site/src/features/Contents/features/ClassRoom/components/Read/ClassRoomItem.tsx +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/ClassRoom/components/Read/ClassRoomItem.tsx @@ -32,8 +32,14 @@ const ClassRoom = ({ classroom }: { classroom: ClassroomLite }) => { justify="center" background="radial-gradient(ellipse at center, #8682bc 0%,#6460c3 100%);" > - - + + {classroom.welcome_text} diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.spec.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.spec.tsx new file mode 100644 index 0000000000..f1329dd956 --- /dev/null +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.spec.tsx @@ -0,0 +1,29 @@ +import { screen } from '@testing-library/react'; +import { render } from 'lib-tests'; + +import LiveRouter from './LiveRouter'; + +jest.mock('./Read/Lives', () => ({ + __esModule: true, + default: () =>
My Lives Read
, +})); + +describe('', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + test('render route /my-contents/lives', () => { + render(, { + routerOptions: { history: ['/my-contents/lives'] }, + }); + expect(screen.getByText('My Lives Read')).toBeInTheDocument(); + }); + + test('render live no match', () => { + render(, { + routerOptions: { history: ['/some/bad/route'] }, + }); + expect(screen.getByText('My Lives Read')).toBeInTheDocument(); + }); +}); diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.tsx new file mode 100644 index 0000000000..3c943b5e87 --- /dev/null +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/LiveRouter.tsx @@ -0,0 +1,18 @@ +import { Box } from 'grommet'; +import { Route, Switch } from 'react-router-dom'; + +import Lives from './Read/Lives'; + +const LiveRouter = () => { + return ( + + + + + + + + ); +}; + +export default LiveRouter; diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.spec.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.spec.tsx new file mode 100644 index 0000000000..d302e0501c --- /dev/null +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.spec.tsx @@ -0,0 +1,63 @@ +import { screen } from '@testing-library/react'; +import { thumbnailMockFactory, videoMockFactory } from 'lib-components'; +import { render } from 'lib-tests'; + +import Live from './Live'; + +describe('', () => { + test('renders Live', () => { + const live = videoMockFactory({ + id: '4321', + title: 'New live title', + description: 'New live description', + playlist: { + ...videoMockFactory().playlist, + title: 'New playlist title', + }, + }); + render(); + + expect( + screen.getByRole('img', { + name: 'thumbnail', + }), + ).toHaveStyle( + `background: url(https://example.com/default_thumbnail/240) no-repeat center / cover`, + ); + expect(screen.getByText('New live title')).toBeInTheDocument(); + expect(screen.getByText('New live description')).toBeInTheDocument(); + expect(screen.getByText('New playlist title')).toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveAttribute( + 'href', + '/my-contents/lives/4321', + ); + }); + + test('renders thumbnail', () => { + const live = videoMockFactory({ + id: '4321', + title: 'New live title', + description: 'New live description', + playlist: { + ...videoMockFactory().playlist, + title: 'New playlist title', + }, + thumbnail: { + ...thumbnailMockFactory(), + urls: { + ...thumbnailMockFactory().urls, + 240: 'https://example.com/240', + }, + }, + }); + render(); + + expect( + screen.getByRole('img', { + name: 'thumbnail', + }), + ).toHaveStyle( + `background: url(https://example.com/240) no-repeat center / cover`, + ); + }); +}); diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.tsx new file mode 100644 index 0000000000..ad735eb940 --- /dev/null +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Live.tsx @@ -0,0 +1,68 @@ +import { Text, Box } from 'grommet'; +import { StyledLink, Video } from 'lib-components'; +import { Fragment } from 'react'; +import styled from 'styled-components'; + +import { ReactComponent as LiveIcon } from 'assets/svg/iko_live.svg'; +import { ReactComponent as VueListIcon } from 'assets/svg/iko_vuelistesvg.svg'; +import { ContentCard } from 'features/Contents/'; +import { routes } from 'routes'; + +const TextTruncated = styled(Text)` + display: -webkit-box; + -webkit-line-clamp: 5; + -webkit-box-orient: vertical; + overflow: hidden; +`; + +const Live = ({ live }: { live: Video }) => { + const livePath = routes.CONTENTS.subRoutes.LIVE.path; + const thumbnail = live.thumbnail?.urls?.[240] || live.urls?.thumbnails?.[240]; + + return ( + + + +
+ } + footer={ + + + + + + + {live.playlist.title} + + + + } + title={live.title || ''} + > + {live.description && ( + + {live.description} + + )} + + + ); +}; + +export default Live; diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.spec.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.spec.tsx new file mode 100644 index 0000000000..292d8735a0 --- /dev/null +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.spec.tsx @@ -0,0 +1,179 @@ +import { screen, waitFor } from '@testing-library/react'; +import fetchMock from 'fetch-mock'; +import { ResponsiveContext } from 'grommet'; +import { useJwt, videoMockFactory } from 'lib-components'; +import { render } from 'lib-tests'; +import { QueryClient } from 'react-query'; + +import { getFullThemeExtend } from 'styles/theme.extend'; + +import Lives from './Lives'; + +const fullTheme = getFullThemeExtend(); + +const mockGetDecodedJwt = jest.fn(); + +const someResponse = { + count: 1, + next: null, + previous: null, + results: [ + videoMockFactory({ + id: '4321', + title: 'New live title', + description: 'New live description', + playlist: { + ...videoMockFactory().playlist, + title: 'New playlist title', + }, + }), + ], +}; + +describe('', () => { + beforeEach(() => { + useJwt.setState({ + jwt: 'token', + getDecodedJwt: mockGetDecodedJwt, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + fetchMock.restore(); + }); + + test('render lives with api call error', async () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => jest.fn()); + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + fetchMock.get( + '/api/videos/?limit=20&offset=0&ordering=-created_on&is_live=true', + Promise.reject(new Error('Failed to perform the request')), + ); + + render(, { + queryOptions: { + client: queryClient, + }, + }); + expect(screen.getByRole('alert', { name: /spinner/i })).toBeInTheDocument(); + + expect( + await screen.findByText(/Sorry, an error has occurred./i), + ).toBeInTheDocument(); + expect(consoleError).toHaveBeenCalled(); + }); + + test('render without lives', async () => { + const someStuff = { + count: 0, + next: null, + previous: null, + results: [], + }; + fetchMock.get( + '/api/videos/?limit=20&offset=0&ordering=-created_on&is_live=true', + someStuff, + ); + + render(); + expect(screen.getByRole('alert', { name: /spinner/i })).toBeInTheDocument(); + expect( + await screen.findByText(/There is no live to display./i), + ).toBeInTheDocument(); + }); + + test('render Lives', async () => { + fetchMock.get( + '/api/videos/?limit=20&offset=0&ordering=-created_on&is_live=true', + someResponse, + ); + + render(); + expect(screen.getByRole('alert', { name: /spinner/i })).toBeInTheDocument(); + expect(await screen.findByText(/New live title/)).toBeInTheDocument(); + expect(screen.getByText(/New live description/)).toBeInTheDocument(); + expect( + screen.queryByLabelText('Pagination Navigation'), + ).not.toBeInTheDocument(); + }); + + test('render pagination', async () => { + fetchMock.get( + '/api/videos/?limit=20&offset=0&ordering=-created_on&is_live=true', + { + ...someResponse, + count: 111, + }, + ); + + render(); + + expect( + await screen.findByLabelText('Pagination Navigation'), + ).toBeInTheDocument(); + }); + + test('render without pagination', async () => { + fetchMock.get( + '/api/videos/?limit=20&offset=0&ordering=-created_on&is_live=true', + { + ...someResponse, + count: 111, + }, + ); + + render(); + + await waitFor(() => { + expect( + screen.queryByLabelText('Pagination Navigation'), + ).not.toBeInTheDocument(); + }); + }); + + test('render with limit', async () => { + fetchMock.get( + '/api/videos/?limit=1&offset=0&ordering=-created_on&is_live=true', + { + ...someResponse, + }, + ); + + render(); + + expect(await screen.findByText(/New live title/)).toBeInTheDocument(); + }); + + test('api limit depend the responsive', async () => { + fetchMock.get( + '/api/videos/?limit=4&offset=0&ordering=-created_on&is_live=true', + { + ...someResponse, + count: 111, + }, + ); + + render( + + + , + { + grommetOptions: { + theme: fullTheme, + }, + }, + ); + + expect(await screen.findByText('New live title')).toBeInTheDocument(); + }); +}); diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.tsx new file mode 100644 index 0000000000..b6f8dbdfd1 --- /dev/null +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/components/Read/Lives.tsx @@ -0,0 +1,52 @@ +import { useVideos, VideosOrderType } from 'lib-video'; +import { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { REACT_QUERY_CONF_API } from 'conf/global'; +import { ContentsWrapper, useContentPerPage } from 'features/Contents/'; + +import Live from './Live'; + +const messages = defineMessages({ + NoLive: { + defaultMessage: 'There is no live to display.', + description: 'Text when there is no live to display.', + id: 'features.Contents.features.ReadLives.NoLive', + }, +}); + +interface LivesProps { + withPagination?: boolean; + limit?: number; +} + +const Lives = ({ withPagination = true, limit }: LivesProps) => { + const intl = useIntl(); + const [currentPage, setCurrentPage] = useState(1); + const contentPerPage = useContentPerPage(); + + const apiResponse = useVideos( + { + offset: `${(currentPage - 1) * contentPerPage}`, + limit: `${limit || contentPerPage}`, + ordering: VideosOrderType.BY_CREATED_ON_REVERSED, + is_live: 'true', + }, + REACT_QUERY_CONF_API, + ); + + return ( + ( + + )} + currentPage={currentPage} + setCurrentPage={(page) => setCurrentPage(page)} + noContentMessage={intl.formatMessage(messages.NoLive)} + withPagination={withPagination} + /> + ); +}; + +export default Lives; diff --git a/src/frontend/apps/standalone_site/src/features/Contents/features/Live/index.tsx b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/index.tsx new file mode 100644 index 0000000000..ebcbd5df16 --- /dev/null +++ b/src/frontend/apps/standalone_site/src/features/Contents/features/Live/index.tsx @@ -0,0 +1,2 @@ +export { default as LiveRouter } from './components/LiveRouter'; +export { default as Lives } from './components/Read/Lives'; diff --git a/src/frontend/apps/standalone_site/src/features/Contents/index.tsx b/src/frontend/apps/standalone_site/src/features/Contents/index.tsx index edd51f3dbf..89aacf6938 100644 --- a/src/frontend/apps/standalone_site/src/features/Contents/index.tsx +++ b/src/frontend/apps/standalone_site/src/features/Contents/index.tsx @@ -5,4 +5,5 @@ export { default as ContentsRouter } from './components/ContentsRouter/ContentsR export { default as ContentsShuffle } from './components/ContentsShuffle/ContentsShuffle'; export { default as ManageAPIState } from './components/ManageAPIState/ManageAPIState'; export { ClassRoomRouter, ClassRooms } from './features/ClassRoom/'; +export { LiveRouter, Lives } from './features/Live/'; export { VideoRouter, Videos } from './features/Video/'; diff --git a/src/frontend/apps/standalone_site/src/routes/routes.tsx b/src/frontend/apps/standalone_site/src/routes/routes.tsx index 2cf2e81213..e761d68a86 100644 --- a/src/frontend/apps/standalone_site/src/routes/routes.tsx +++ b/src/frontend/apps/standalone_site/src/routes/routes.tsx @@ -1,8 +1,8 @@ import { defineMessages, FormattedMessage } from 'react-intl'; import { ReactComponent as AvatarIcon } from 'assets/svg/iko_avatarsvg.svg'; -//import { ReactComponent as CheckListIcon } from 'assets/svg/iko_checklistsvg.svg'; import { ReactComponent as HomeIcon } from 'assets/svg/iko_homesvg.svg'; +import { ReactComponent as LiveIcon } from 'assets/svg/iko_live.svg'; import { ReactComponent as VideoIcon } from 'assets/svg/iko_next.svg'; import { ReactComponent as StarIcon } from 'assets/svg/iko_starsvg.svg'; import { ReactComponent as VueListIcon } from 'assets/svg/iko_vuelistesvg.svg'; @@ -83,7 +83,7 @@ enum ERouteNames { } enum EMyContentsSubRouteNames { VIDEO = 'VIDEO', - //LIVE = 'LIVE', + LIVE = 'LIVE', CLASSROOM = 'CLASSROOM', //LESSON = 'LESSON', } @@ -246,18 +246,18 @@ export const routes: Routes = { }, isNavStrict: true, }, - // LIVE: { - // label: , - // path: `/my-contents/lives`, - // menuIcon: ( - // - // ), - // }, + LIVE: { + label: , + path: `/my-contents/lives`, + menuIcon: ( + + ), + isNavStrict: true, + }, CLASSROOM: { label: , path: `/my-contents/classroom`,