Skip to content

Commit

Permalink
feat: added video detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
Maham Akif authored and Maham Akif committed Jul 12, 2024
1 parent 516a21b commit 0f99e70
Show file tree
Hide file tree
Showing 14 changed files with 568 additions and 8 deletions.
129 changes: 129 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"universal-cookie": "4.0.4",
"uuid": "9.0.0",
"video.js": "8.3.0",
"videojs-vjstranscribe": "^1.0.3",
"videojs-youtube": "3.0.1"
},
"devDependencies": {
Expand Down
15 changes: 15 additions & 0 deletions src/components/app/routes/createAppRouter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,21 @@ export default function createAppRouter(queryClient) {
};
}}
/>
<Route
path="videos/:edxVideoID"
lazy={async () => {
const { VideoDetailPage } = await import('../../microlearning');
return {
Component: VideoDetailPage,
};
}}
errorElement={(
<RouteErrorBoundary
showSiteHeader={false}
showSiteFooter={false}
/>
)}
/>
<Route path="*" element={<NotFoundPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
Expand Down
133 changes: 133 additions & 0 deletions src/components/microlearning/VideoDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { useEffect } from 'react';
import { useParams, Link, useLocation } from 'react-router-dom';
import {
Container, Breadcrumb, Row, MediaQuery, breakpoints, Badge, Skeleton,
} from '@openedx/paragon';
import loadable from '@loadable/component';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { useEnterpriseCustomer } from '../app/data';
import { Sidebar } from '../layout';
import './styles/VideoDetailPage.scss';
import DelayedFallbackContainer from '../DelayedFallback/DelayedFallbackContainer';
import { useVideoData } from './data/hooks';
import { features } from '../../config';

const VideoPlayer = loadable(() => import(/* webpackChunkName: "videojs" */ '../video/VideoPlayer'), {
fallback: (
<DelayedFallbackContainer>
<Skeleton height={200} />
</DelayedFallbackContainer>
),
});

const VideoDetailPage = () => {
const { edxVideoID } = useParams();
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const location = useLocation();

const [videoData, isLoading, error] = useVideoData(edxVideoID);

const customOptions = {
showPlaybackMenu: true,
showTranscripts: true,
transcriptUrls: videoData?.transcriptUrls,
};

useEffect(() => {
if (videoData?.videoURL) {
VideoPlayer.preload();
}
}, [videoData?.videoURL]);

const routeLinks = [
{
label: 'Explore Videos',
to: `/${enterpriseCustomer.slug}/videos`,
},
];
if (location.state?.parentRoute) {
routeLinks.push(location.state.parentRoute);
}

if (isLoading) {
return (
<DelayedFallbackContainer>
<Skeleton height={400} />
</DelayedFallbackContainer>
);
}

// Comprehensive error handling will be implemented upon receiving specific error use cases from the UX team
// and corresponding Figma designs.
if (error || !features.FEATURE_ENABLE_VIDEO_CATALOG) {
return (
<div className="text-center py-5">
<h1>404</h1>
<p>{error?.message}</p>
</div>
);
}

return (
<Container size="lg" className="pt-3">
<div className="small">
<Breadcrumb
links={routeLinks}
activeLabel={videoData.courseTitle}
linkAs={Link}
/>
</div>
<Row>
<article className="col-12 col-lg-9">
<div className="d-flex flex-column align-items-start flex-grow-1 video-container">
<div className="d-flex flex-row align-items-center justify-content-between">
<h2 data-testid="video-title" className="mb-0">
{videoData.courseTitle}
</h2>
<span className="small ml-2 mt-2">
{videoData.videoDuration && `(${videoData.videoDuration} minutes)`}
</span>
</div>
<p className="small align-self-stretch text-justify mb-2">
{videoData.videoSummary}
</p>
<div className="d-flex flex-row align-items-center">
<h4>
<FormattedMessage
id="videoDetailPage.skills.label"
defaultMessage="Skills:"
description="Label for skills on video detail page"
/>
</h4>
<div className="ml-2 mb-2.5">
{videoData.videoSkills && (
videoData.videoSkills.map((skill) => (
<Badge
key={skill.skill_id}
className="video-skills"
variant="light"
>
{skill.name}
</Badge>
))
)}
</div>
</div>
</div>
<div className="video-wrapper mt-2">
<VideoPlayer videoURL={videoData.videoUrl} customOptions={customOptions} />
</div>
</article>
<MediaQuery minWidth={breakpoints.large.minWidth}>
{matches => matches && (
<Sidebar>
{/* Course Sidebar will be inserted here */}
</Sidebar>
)}
</MediaQuery>
</Row>
</Container>
);
};

export default VideoDetailPage;
27 changes: 27 additions & 0 deletions src/components/microlearning/data/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useState, useEffect } from 'react';
import { logError } from '@edx/frontend-platform/logging';
import { fetchVideoData } from './service';

export function useVideoData(edxVideoID) {
const [videoData, setVideoData] = useState({});
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const fetchData = async () => {
try {
const data = await fetchVideoData(edxVideoID);
setVideoData(data);
} catch (err) {
logError(err);
setError(err);
} finally {
setIsLoading(false);
}
};

fetchData();
}, [edxVideoID]);

return [videoData, isLoading, error];
}
11 changes: 11 additions & 0 deletions src/components/microlearning/data/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { getConfig } from '@edx/frontend-platform/config';
import { transformVideoData } from './utils';

export const fetchVideoData = async (edxVideoID) => {
const config = getConfig();
const url = `${config.ENTERPRISE_CATALOG_API_BASE_URL}/api/v1/videos/${edxVideoID}`;
const result = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(transformVideoData(result?.data || {}));
};
21 changes: 21 additions & 0 deletions src/components/microlearning/data/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const transformVideoData = (data) => {
const skillNames = Object.values(data.skills).map(skill => ({
skill_id: skill.skill_id,
name: skill.name,
}));

const minutes = Math.floor(data.json_metadata.duration / 60);
const seconds = Math.round(data.json_metadata.duration % 60);
const duration = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;

const transformedData = {
videoUrl: data.json_metadata.download_link,
courseTitle: data.parent_content_metadata.title,
videoSummary: data.summary_transcripts['list-item'],
transcriptUrls: data.json_metadata.transcript_urls,
videoSkills: skillNames,
videoDuration: duration,
};

return transformedData;
};
1 change: 1 addition & 0 deletions src/components/microlearning/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as VideoDetailPage } from './VideoDetailPage';
Loading

0 comments on commit 0f99e70

Please sign in to comment.