Skip to content

Commit

Permalink
feat: add video feedback component to track user feedback via segment…
Browse files Browse the repository at this point in the history
… events (#1195)

Co-authored-by: Maham Akif <[email protected]>
  • Loading branch information
mahamakifdar19 and Maham Akif committed Sep 24, 2024
1 parent f7bcbfe commit 5f5a7c5
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 3 deletions.
13 changes: 12 additions & 1 deletion src/components/microlearning/VideoDetailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { hasTruthyValue, isDefinedAndNotNull } from '../../utils/common';
import { getLevelType } from './data/utils';
import { hasActivatedAndCurrentSubscription } from '../search/utils';
import { features } from '../../config';
import VideoFeedbackCard from './VideoFeedbackCard';

const VideoPlayer = loadable(() => import(/* webpackChunkName: "videojs" */ '../video/VideoPlayer'), {
fallback: (
Expand Down Expand Up @@ -171,7 +172,7 @@ const VideoDetailPage = () => {
)}
</article>
{isDefinedAndNotNull(courseMetadata.activeCourseRun) && (
<article className="col-12 col-lg-3 pr-0">
<article className="col-12 col-lg-3 pr-0 pb-3">
<div className="d-flex flex-column align-items-start">
<h3 className="m-0">
<FormattedMessage
Expand Down Expand Up @@ -304,6 +305,16 @@ const VideoDetailPage = () => {
</div>
</article>
)}
<article className="col-12 col-lg-9">
<div className="pt-3 pb-6">
<VideoFeedbackCard
videoId={videoData?.edxVideoId}
courseRunKey={videoData?.courseKey}
enterpriseCustomerUuid={enterpriseCustomer.uuid}
videoUsageKey={videoData?.videoUsageKey}
/>
</div>
</article>
</Row>
</Container>
);
Expand Down
229 changes: 229 additions & 0 deletions src/components/microlearning/VideoFeedbackCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
Card, Icon, IconButton, ActionRow, Form, Input, Button,
} from '@openedx/paragon';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
ThumbUpOutline, ThumbUp, ThumbDownOffAlt, ThumbDownAlt, Close,
} from '@openedx/paragon/icons';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import { VIDEO_FEEDBACK_CARD, VIDEO_FEEDBACK_SUBMITTED_LOCALSTORAGE_KEY } from './constants';

const VideoFeedbackCard = ({
videoId, courseRunKey, enterpriseCustomerUuid, videoUsageKey,
}) => {
const intl = useIntl();
const [response, setResponse] = useState(null);
const [selectedOptions, setSelectedOptions] = useState([]);
const [comments, setComments] = useState('');
const [showFeedbackCard, setShowFeedbackCard] = useState(true);
const [showFeedbackSubmittedCard, setShowFeedbackSubmittedCard] = useState(false);
const feedbackLocalStorageKey = VIDEO_FEEDBACK_SUBMITTED_LOCALSTORAGE_KEY(videoId);

// On component mount, check if feedback has been previously submitted
useEffect(() => {
const feedbackSubmitted = localStorage.getItem(feedbackLocalStorageKey);
if (feedbackSubmitted === 'true') {
setShowFeedbackCard(false);
setShowFeedbackSubmittedCard(true);
}
}, [feedbackLocalStorageKey]);

const handleThumbClick = (feedbackResponse) => {
setResponse(feedbackResponse);
if (feedbackResponse) {
setShowFeedbackCard(false);
setShowFeedbackSubmittedCard(true);
}
sendEnterpriseTrackEvent(
enterpriseCustomerUuid,
'edx.ui.enterprise.learner_portal.video.feedback.thumb.submitted',
{
videoId,
courseRunKey,
video_usage_key: videoUsageKey,
prompt: VIDEO_FEEDBACK_CARD.prompt,
response: feedbackResponse,
},
);
localStorage.setItem(feedbackLocalStorageKey, 'true');
};

const handleSubmitFeedback = () => {
sendEnterpriseTrackEvent(
enterpriseCustomerUuid,
'edx.ui.enterprise.learner_portal.video.feedback.response.submitted',
{
videoId,
courseRunKey,
video_usage_key: videoUsageKey,
prompt: VIDEO_FEEDBACK_CARD.prompt,
response,
selectedOptions,
comments,
},
);
setShowFeedbackSubmittedCard(true);
setShowFeedbackCard(false);
};

return (
<>
{showFeedbackCard && (
<Card>
<Card.Header
className="mb-3"
title={(
<h4>
<FormattedMessage
id="enterprise.VideoFeedbackCard.prompt"
defaultMessage="{prompt}"
description="Prompt to ask for feedback"
values={{ prompt: VIDEO_FEEDBACK_CARD.prompt }}
/>
</h4>
)}
actions={(
<ActionRow className="pt-1">
<IconButton
key="dark"
src={response === true ? ThumbUp : ThumbUpOutline}
iconAs={Icon}
onClick={() => handleThumbClick(true)}
variant="dark"
className="border rounded-circle border-1 border-light-400 p-3 mr-2"
aria-label="thumbs up"
/>
<IconButton
key="dark"
src={response === false ? ThumbDownAlt : ThumbDownOffAlt}
iconAs={Icon}
onClick={() => handleThumbClick(false)}
variant="dark"
className="border rounded-circle border-1 border-light-400 p-3 mr-2"
aria-label="thumbs down"
/>
<div className="border-left border-1 border-light-400" style={{ height: 52, marginTop: -8 }} />
<IconButton
className="ml-3"
src={Close}
iconAs={Icon}
onClick={() => setShowFeedbackCard(false)}
/>
</ActionRow>
)}
size="sm"
/>
{/* Display additional options when the user selects thumbs down (negative feedback)." */}
{response === false && (
<Card.Section>
<div className="mb-3">
<FormattedMessage
id="enterprise.VideoFeedbackCard.additionalDetailsLabel"
defaultMessage="{additionalDetailsLabel}"
description="Additional details section title"
values={{ additionalDetailsLabel: VIDEO_FEEDBACK_CARD.additionalDetailsLabel }}
/>
</div>
<Form.Group className="mb-3">
{VIDEO_FEEDBACK_CARD.options.map((option) => (
<div className="mb-2" key={option}>
<Form.Checkbox
value={option}
onChange={(e) => {
const { checked } = e.target;
setSelectedOptions((prevOptions) => (checked
? [...prevOptions, option]
: prevOptions.filter(opt => opt !== option)));
}}
>
<FormattedMessage
id="enterprise.VideoFeedbackCard.additionalDetailsOption"
defaultMessage="{additionalDetailsOption}"
description="Additional details option for video feedback"
values={{ additionalDetailsOption: option }}
/>
</Form.Checkbox>
</div>
))}
</Form.Group>
<Input
type="text"
placeholder={
intl.formatMessage(
{
id: 'enterprise.VideoFeedbackCard.additionalCommentsPlaceholder',
defaultMessage: '{inputPlaceholder}',
description: 'Additional comments placeholder for video feedback',
},
{ inputPlaceholder: VIDEO_FEEDBACK_CARD.inputPlaceholder },
)
}
className="mb-4"
onChange={(e) => setComments(e.target.value)}
/>
<Button
variant="primary"
onClick={handleSubmitFeedback}
>
<FormattedMessage
id="enterprise.VideoFeedbackCard.submitButton"
defaultMessage="{submitButtonLabel}"
description="Button to submit video feedback"
values={{ submitButtonLabel: VIDEO_FEEDBACK_CARD.submitButton }}
/>
</Button>
</Card.Section>
)}
</Card>
)}
{showFeedbackSubmittedCard && (
<Card>
<Card.Header
title={(
<h4 className="pb-1.5">
<FormattedMessage
id="enterprise.VideoFeedbackCard.thankYouMessage"
defaultMessage="{thankYouMessage}"
description="Thank you message after submitting feedback"
values={{ thankYouMessage: VIDEO_FEEDBACK_CARD.thankYouMessage }}
/>
</h4>
)}
actions={(
<ActionRow className="pt-1">
<IconButton
className=""
src={Close}
iconAs={Icon}
onClick={() => {
setShowFeedbackSubmittedCard(false);
}}
/>
</ActionRow>
)}
size="sm"
/>
<Card.Section>
<FormattedMessage
id="enterprise.VideoFeedbackCard.feedbackSentMessage"
defaultMessage="{feedbackSentMessage}"
description="Message displayed after user has successfully submitted their feedback"
values={{ feedbackSentMessage: VIDEO_FEEDBACK_CARD.feedbackSentMessage }}
/>
</Card.Section>
</Card>
)}
</>
);
};

VideoFeedbackCard.propTypes = {
videoId: PropTypes.string.isRequired,
courseRunKey: PropTypes.string.isRequired,
enterpriseCustomerUuid: PropTypes.string.isRequired,
videoUsageKey: PropTypes.string.isRequired,
};

export default VideoFeedbackCard;
15 changes: 15 additions & 0 deletions src/components/microlearning/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const VIDEO_FEEDBACK_CARD = {
prompt: 'Was this page helpful?',
additionalDetailsLabel: 'Any additional details? Select all that apply:',
options: [
'Videos are hard to find or navigate',
'Video wasn’t relevant',
'Video was low quality or confusing',
],
inputPlaceholder: 'Type comments (optional)',
submitButton: 'Submit feedback',
thankYouMessage: 'Thank you!',
feedbackSentMessage: 'Your feedback has been sent to the edX research team!',
};

export const VIDEO_FEEDBACK_SUBMITTED_LOCALSTORAGE_KEY = (videoId) => (`${videoId}-feedbackSubmitted`);
1 change: 1 addition & 0 deletions src/components/microlearning/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const formatSkills = (skills) => skills?.map(skill => ({
}));

export const transformVideoData = (data) => ({
edxVideoId: data?.edx_video_id,
videoUrl: data?.json_metadata?.download_link,
courseTitle: data?.title || data?.parent_content_metadata?.title,
videoSummary: data?.summary_transcripts?.[0],
Expand Down
3 changes: 1 addition & 2 deletions src/components/microlearning/styles/VideoDetailPage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
gap: 16px;
flex: 1 0 0;
}

/*
Custom CSS is necessary here because we are using a custom video.js plugin - videojs-vjstranscribe
The elements of this plugin need to be customized to cater to the hidden classes and other specific
Expand All @@ -29,7 +29,6 @@

.video-player-container-with-transcript {
display: flex;
padding-bottom: 55px;
}

.video-js-wrapper {
Expand Down
Loading

0 comments on commit 5f5a7c5

Please sign in to comment.