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

feat: add feedback feature to self-learning pages #2093

Merged
merged 12 commits into from
Oct 1, 2024
Merged
5 changes: 5 additions & 0 deletions .changeset/kind-balloons-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@commercetools-docs/gatsby-theme-docs': minor
---

Add feedback functionality to self-learning pages
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import styled from '@emotion/styled';
import thumbsUpIcon from '../icons/assistant-thumbs-up.png';
import thumbsDownIcon from '../icons/assistant-thumbs-down.png';
import thumbsUpIconFilled from '../icons/assistant-thumbs-up-filled.png';
import thumbsDownIconFilled from '../icons/assistant-thumbs-down-filled.png';
import { designSystem } from '@commercetools-docs/ui-kit';

type ThumbsButtonProps = {
isClickable: boolean;
iconSize: number;
};

export const FEEDBACK_UP = 1;
export const FEEDBACK_DOWN = -1;

const ThumbsButton = styled.button<ThumbsButtonProps>`
display: flex;
align-items: center;
cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')};
background: transparent;
height: ${(props) => props.iconSize}px;
width: ${(props) => props.iconSize}px;
border: 0;
padding: 3px;
color: ${designSystem.colors.light.linkHover};
:focus {
outline: none;
}
img {
margin-right: 5px;
}
`;

type ButtonsWrapperProps = {
hasText: boolean;
};

const ButtonsWrapper = styled.div<ButtonsWrapperProps>`
display: flex;
gap: ${(props) => (props.hasText ? '40px' : '0')};
`;

type TPageFeedbackButtonsProps = {
isPositiveClickable: boolean;
isNegativeClickable: boolean;
currentFeedback: number;
onPositiveClick: () => void;
onNegativeClick: () => void;
iconSize?: number;
positiveText?: string;
negativeText?: string;
};

const PageFeedbackButtons = (props: TPageFeedbackButtonsProps) => {
const iconSize = props.iconSize || 28;
return (
<ButtonsWrapper hasText={!!(props.positiveText || props.negativeText)}>
<ThumbsButton
iconSize={iconSize}
isClickable={props.isPositiveClickable}
onClick={
(props.isPositiveClickable && props.onPositiveClick) || undefined
}
>
<img
alt="positive feedback"
src={
props.currentFeedback === FEEDBACK_UP
? thumbsUpIconFilled
: thumbsUpIcon
}
width={iconSize}
/>
{props.positiveText && <span>{props.positiveText}</span>}
</ThumbsButton>
<ThumbsButton
iconSize={iconSize}
isClickable={props.isNegativeClickable}
onClick={
(props.isNegativeClickable && props.onNegativeClick) || undefined
}
>
<img
alt="negative feedback"
src={
props.currentFeedback === FEEDBACK_DOWN
? thumbsDownIconFilled
: thumbsDownIcon
}
width={iconSize}
/>
{props.negativeText && <span>{props.negativeText}</span>}
</ThumbsButton>
</ButtonsWrapper>
);
};

export default PageFeedbackButtons;
114 changes: 114 additions & 0 deletions packages/gatsby-theme-docs/src/components/page-feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useState } from 'react';
import styled from '@emotion/styled';

import PageFeedbackButtons, {
FEEDBACK_DOWN,
FEEDBACK_UP,
} from './page-feedback-buttons';
import { designSystem } from '@commercetools-docs/ui-kit';
import { gtagEvent } from '../modules/sso/utils/analytics.utils';

const POSITIVE_SURVEY_ID = 3628; // id for the userguiding survey triggered by thumbs up click
const NEGATIVE_SURVEY_ID = 3627; // id for the userguiding survey triggered by thumbs down click
const USERGUIDING_SESSION_KEY = '__UGS__uid'; // local storage key for userguiding session
const USER_GUIDING_ID = 'U4I78799B6RID'; // userguiding user id for the embedded script
const MAX_SCRIPT_LOAD_TIME = 30 * 1000; // 30 seconds

const FeedbackQuestion = styled.div`
padding-bottom: ${designSystem.dimensions.spacings.s};
`;

const PageFeedbackWrapper = styled.div`
border-top: 1px solid ${designSystem.colors.light.borderPrimary};
padding-top: ${designSystem.dimensions.spacings.l};
font-size: ${designSystem.typography.fontSizes.small};
`;

const PageFeedback = () => {
const [currentFeedback, setCurrentFeedback] = useState(0);

const isScriptLoaded = (): boolean => {
const isUserGuidingSessionReady =
gabriele-ct marked this conversation as resolved.
Show resolved Hide resolved
localStorage.getItem(USERGUIDING_SESSION_KEY) !== null;
const isUserGuidingScriptLoaded =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typeof (window as any).userGuiding?.launchSurvey === 'function';
gabriele-ct marked this conversation as resolved.
Show resolved Hide resolved
return isUserGuidingScriptLoaded && isUserGuidingSessionReady;
};

const injectUserGuidingScript = (): Promise<void> => {
return new Promise((resolve, reject) => {
if (isScriptLoaded()) {
resolve(); // Script is already loaded, resolve immediately
return;
}

const script = document.createElement('script');
script.type = 'text/javascript';
script.src = `https://eu-static.userguiding.com/media/user-guiding-${USER_GUIDING_ID}-embedded.js`;

script.onload = () => {
// Poll for userGuiding object in the global scope
let loadTime = 0;
const interval = setInterval(() => {
if (isScriptLoaded()) {
clearInterval(interval); // Stop polling
resolve();
return;
}
if (loadTime >= MAX_SCRIPT_LOAD_TIME) {
clearInterval(interval); // Stop polling
reject(new Error('Userguiding script loading timeout.'));
}
loadTime += 100;
}, 100);
};

script.onerror = () => {
reject(new Error('Userguiding script loading failed.'));
};

document.head.appendChild(script);
});
};

const handleClick = async (feedback: number) => {
setCurrentFeedback(feedback);

// track the event on google analytics
gtagEvent('page_feedback', {
feedback_page: window.location.pathname,
feedback_value: feedback.toString(),
});

try {
await injectUserGuidingScript();
const surveyId =
feedback === FEEDBACK_UP ? POSITIVE_SURVEY_ID : NEGATIVE_SURVEY_ID;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).userGuiding?.launchSurvey(surveyId);
} catch (error) {
console.error(error);
}
};

const isClickable = currentFeedback === 0;

return (
<PageFeedbackWrapper>
<FeedbackQuestion>Was this page helpful?</FeedbackQuestion>
<PageFeedbackButtons
onPositiveClick={() => handleClick(FEEDBACK_UP)}
onNegativeClick={() => handleClick(FEEDBACK_DOWN)}
currentFeedback={currentFeedback}
isPositiveClickable={isClickable}
isNegativeClickable={isClickable}
iconSize={30}
positiveText="Yes"
negativeText="No"
/>
</PageFeedbackWrapper>
);
};

export default PageFeedback;
1 change: 1 addition & 0 deletions packages/gatsby-theme-docs/src/layouts/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const LayoutContent = (props) => {
navLevels={props.pageData.navLevels}
beta={isBeta}
planTags={planTags}
isSelfLearning={siteData.siteMetadata?.isSelfLearning}
/>
</LayoutPage>
</LayoutPageWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import PlaceholderPageHeaderSide from '../../overrides/page-header-side';
import PlaceholderPageHeaderSideBannerArea from '../../overrides/page-header-banner-area';
import { Overlay, BetaTag, SearchInput, PlanTag } from '../../components';
import PageNavigation from './page-navigation';
import PageFeedback from '../../components/page-feedback';

const StackedLinesIndentedIcon = createStyledIcon(
Icons.StackedLinesIndentedIconSvgIcon
Expand Down Expand Up @@ -166,6 +167,14 @@ const OverlayBackground = styled.div`
);
`;

const PageFeedbackContainer = styled.div`
@media screen and (${designSystem.dimensions.viewports.mobile}) {
display: none;
}
padding: ${designSystem.dimensions.spacings.s}
${designSystem.dimensions.spacings.m} 0;
`;

const LayoutPageNavigation = (props) => {
const [isMenuOpen, setMenuOpen] = React.useState(false);
const [modalPortalNode, setModalPortalNode] = React.useState();
Expand Down Expand Up @@ -245,6 +254,11 @@ const LayoutPageNavigation = (props) => {
tableOfContents={props.tableOfContents}
navLevels={props.navLevels}
/>
{props.isSelfLearning && (
<PageFeedbackContainer>
<PageFeedback />
</PageFeedbackContainer>
)}
</SpacingsStack>
</nav>
);
Expand Down Expand Up @@ -322,6 +336,7 @@ LayoutPageNavigation.propTypes = {
navLevels: PropTypes.number.isRequired,
beta: PropTypes.bool.isRequired,
planTags: PropTypes.arrayOf(PropTypes.string).isRequired,
isSelfLearning: PropTypes.bool,
};

export default LayoutPageNavigation;
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@ import {
import { FirstName } from '@commercetools-docs/gatsby-theme-docs';
import { CHAT_ROLE_ASSISTANT, CHAT_ROLE_USER } from './chat.const';
import { getAssistantAvatarIcon } from './chat.utils';
import thumbsUpIcon from '../icons/assistant-thumbs-up.png';
import thumbsDownIcon from '../icons/assistant-thumbs-down.png';
import thumbsUpIconFilled from '../icons/assistant-thumbs-up-filled.png';
import thumbsDownIconFilled from '../icons/assistant-thumbs-down-filled.png';
import codeIcon from '../icons/assistant-code.png';
import { DEV_TOOLING_MODE } from './chat-modal';
import PageFeedbackButtons from '../../../components/page-feedback-buttons';

export const FEEDBACK_UP = 1;
export const FEEDBACK_DOWN = -1;
Expand Down Expand Up @@ -82,34 +79,6 @@ const FeedbackWrapper = styled.div`
justify-content: flex-end;
`;

const ThumbsDownButton = styled.button`
cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')};
background: transparent;
height: 28px;
width: 28px;
border: 0;
padding: 3px;
margin-right: 8px;
img {
transform: scaleX(-1);
}
:focus {
outline: none;
}
`;

const ThumbsUpButton = styled.button`
cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')};
background: transparent;
height: 28px;
width: 28px;
border: 0;
padding: 3px;
:focus {
outline: none;
}
`;

const ChatMessages = (props) => {
const [feedbackResults, setFeedbackResults] = useState({});
const AssistantAvatarIcon =
Expand Down Expand Up @@ -145,7 +114,9 @@ const ChatMessages = (props) => {
Hello <FirstName />, {markdownFragmentToReact(props.chatMode.intro)}
</IntroMessageText>
</MessageContainer>
{props.messages.map((message, index) => (
{props.messages.map((message, index) => {
const messageFeedback = feedbackResults[message.id];
return (
<div key={index}>
<MessageContainer>
{message.role === 'assistant' ? (
Expand Down Expand Up @@ -185,46 +156,26 @@ const ChatMessages = (props) => {

{message.role === 'assistant' ? (
<FeedbackWrapper>
<ThumbsUpButton
isClickable={!feedbackResults[message.id]}
onClick={
!feedbackResults[message.id]
<PageFeedbackButtons
onPositiveClick={
!messageFeedback
? (e) => handleThumbsClick(e, message.id, FEEDBACK_UP)
: null
}
>
<img
alt="positive feedback"
src={
feedbackResults[message.id] === FEEDBACK_UP
? thumbsUpIconFilled
: thumbsUpIcon
}
width={18}
/>
</ThumbsUpButton>
<ThumbsDownButton
isClickable={!feedbackResults[message.id]}
onClick={
!feedbackResults[message.id]
onNegativeClick={
!messageFeedback
? (e) => handleThumbsClick(e, message.id, FEEDBACK_DOWN)
: null
}
>
<img
alt="negative feedback"
src={
feedbackResults[message.id] === FEEDBACK_DOWN
? thumbsDownIconFilled
: thumbsDownIcon
}
width={18}
/>
</ThumbsDownButton>
currentFeedback={messageFeedback}
isPositiveClickable={!messageFeedback}
isNegativeClickable={!messageFeedback}
iconSize={24}
/>
</FeedbackWrapper>
) : null}
</div>
))}
)})}
{props.chatLocked && (
<MessageContainer>
<ContentNotifications.Info>
Expand Down
Loading
Loading