Docs: Feedback component v2.0

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7242
GitOrigin-RevId: 4398205fc74b0a9745d15ade4c78a21467b7475f
This commit is contained in:
Sean Park-Ross 2023-01-11 16:50:35 +02:00 committed by hasura-bot
parent f82eaf7ea5
commit af639392ce
9 changed files with 465 additions and 308 deletions

View File

@ -2,11 +2,11 @@ import React from 'react';
import ActualDocItem from '@theme/DocItem';
import HasuraConBanner from '@site/src/components/HasuraConBanner';
import GraphQLWithHasuraBanner from '@site/src/components/GraphQLWithHasuraBanner';
import PageHelpful from '@site/src/components/PageHelpful';
import CustomFooter from '@site/src/components/CustomFooter';
import styles from './styles.module.scss';
import {ScrollToFeedbackButton} from "@site/src/components/Feedback/ScrollToFeedbackButton";
const CustomDocItem = (props) => {
const CustomDocItem = props => {
return (
<div
className={
@ -17,7 +17,8 @@ const CustomDocItem = (props) => {
>
<ActualDocItem {...props} />
<div className={styles['custom_doc_item_footer']}>
<PageHelpful />
{/*<PageHelpful />*/}
<ScrollToFeedbackButton/>
<HasuraConBanner {...props} />
<GraphQLWithHasuraBanner />
<CustomFooter />

View File

@ -0,0 +1,164 @@
import React, {ReactNode, useRef, useState} from 'react';
import {saTrack} from '@site/src/utils/segmentAnalytics';
import styles from './styles.module.scss';
export const Feedback = ({metadata}: {metadata: any}) => {
const [rating, setRating] = useState<1 | 2 | 3 | 4 | 5 | null>(null);
const [notes, setNotes] = useState<string | null>(null);
const [errorText, setErrorText] = useState<string | null>(null);
const [hoveredScore, setHoveredScore] = useState<Number | null>(null);
const [textAreaLabel, setTextAreaLabel] = useState<ReactNode | null>(null);
const [textAreaPlaceholder, setTextAreaPlaceholder] = useState<string>('This section is optional ✌️');
const [isSubmitSuccess, setIsSubmitSuccess] = useState<boolean>(false);
const submitDisabled = rating === null || (rating < 4 && (notes === null || notes === ''));
const scores: (1 | 2 | 3 | 4 | 5)[] = [1, 2, 3, 4, 5];
const handleSubmit = async () => {
if (rating === null) {
setErrorText('Please select a score.');
return;
}
if (rating < 4 && notes === null) {
setErrorText(
"Because this doc wasn't up to scratch please provide us with some feedback of where we can improve."
);
return;
}
const sendData = async () => {
const myHeaders = new Headers();
myHeaders.append('Content-Type', 'application/json');
const raw = JSON.stringify({
feedback: {
isHelpful: rating >= 4 ? `👍` : `👎`,
score: rating,
notes,
pageTitle: document.title,
url: window.location.href,
},
});
const requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow',
}
fetch('https://us-central1-websitecloud-352908.cloudfunctions.net/docs-feedback', requestOptions)
.then(response => response.text())
.catch(error => console.error('error', error));
};
if (window.location.hostname === 'localhost') {
alert('Testing feedback (not) sent!');
setRating(null);
setNotes(null);
setIsSubmitSuccess(true);
return;
}
sendData().then(() => {
saTrack('Responded to Did You Find This Page Helpful', {
label: 'Responded to Did You Find This Page Helpful',
response: rating >= 4 ? 'YES' : 'NO',
pageUrl: window.location.href,
});
setRating(null);
setNotes(null);
setIsSubmitSuccess(true);
}).catch((e) => {console.error(e)});
return;
};
const handleScoreClick = (scoreItem: 1 | 2 | 3 | 4 | 5) => {
if (scoreItem === rating) {
setRating(null);
setErrorText(null);
setHoveredScore(null);
return
}
setErrorText(null);
setRating(scoreItem);
if (scoreItem < 4) {
setTextAreaLabel(<>
<p>What can we do to improve it? Please be as detailed as you like.</p>
<p>Real human beings read every single review.</p>
</>);
setTextAreaPlaceholder('This section is required... how can we do better? ✍️');
}
if (scoreItem >= 4) {
setTextAreaLabel(
<>
<p>Any general feedback you'd like to add?</p>
<p>We'll take it all... tell us how well we're doing or where we can improve.</p>
<p>Real human beings read every single review.</p>
</>
);
setTextAreaPlaceholder('This section is optional ✌️');
}
};
// Do not show on Intro page
if (metadata.source === '@site/docs/index.mdx') {
return null;
}
return (
<div className={styles.feedback} id={'feedback'}>
<div className={styles.form}>
<div className={styles.topSection}>
<h3>What did you think of this doc?</h3>
{isSubmitSuccess ?
<div className={styles.successMessage}>
<p>Thanks for your feedback.</p>
<p>Feel free to review as many docs pages as you like!</p>
</div>
: <div className={styles.numberRow}>
{scores.map((star, index) => (
<span key={star} onClick={() => handleScoreClick(star)}
onMouseEnter={() => setHoveredScore(index + 1)}
onMouseLeave={() => setHoveredScore(-1)}>
{rating >= star ? (
<svg width="45" height="45" viewBox="0 0 24 24">
<path fill="#ffc107"
d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
</svg>
) : (
<svg width="45" height="45" viewBox="0 0 24 24">
<path fill={hoveredScore > index ? '#ffc107' : '#B1BCC7'}
d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
</svg>
)}
</span>
))}
</div>
}
</div>
<div style={rating ? {display: "block"} : {display: "none"}}>
<div className={styles.textAreaLabel}>{textAreaLabel}</div>
<textarea
className={styles.textarea}
value={notes ?? ''}
placeholder={textAreaPlaceholder ?? ''}
rows={5}
onChange={e => setNotes(e.target.value)}
/>
<div className={styles.errorAndButton}>
<p className={styles.errorText}>{errorText}</p>
<div className={styles.buttonContainer}>
<button className={submitDisabled ? styles.buttonDisabled : ''} onClick={() => handleSubmit()}>Send your
review!
</button>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,18 @@
import styles from "./styles.module.scss";
import Hand from "@site/static/img/mascot-hand.png";
import React from "react";
export const ScrollToFeedbackButton = () => {
const scrollToFeedback = () => {
const feedbackElement = document.getElementById('feedback');
const y = feedbackElement.getBoundingClientRect().top + window.scrollY - 100;
window.scrollTo({top: y, behavior: 'smooth'});
}
return (
<div className={styles.scrollToWrapper} onClick={scrollToFeedback}>
Feedback 👋
</div>
)
}

View File

@ -0,0 +1,191 @@
.scrollToWrapper {
position: fixed;
display: grid;
place-items: center center;
color: white;
bottom: 85px;
right: 20px;
padding: 10px;
border-radius: 10px;
cursor: pointer;
background-color: var(--ifm-color-primary-light);
box-shadow: var(--ifm-global-shadow-tl);
font-size: 14px;
&:hover {
background-color: var(--ifm-color-primary);
}
&:active {
transform: translateY(3px);
}
}
.feedback {
height: 100%;
padding: 20px;
text-align: center;
width: 100%;
background-color: #f2f5f7;
margin-top: 2rem;
border-radius: 8px;
}
html[data-theme='dark'] {
.feedback {
background-color: var(--color-gray-82);
}
}
.numberRow {
display: flex;
margin-top: 15px;
height: 45px;
justify-content: center;
}
.numberCircle {
display: grid;
place-items: center center;
height: 30px;
width: 30px;
border-radius: 50%;
box-shadow: var(--ifm-global-shadow-tl);
padding: 0.25rem;
cursor: pointer;
background-color: white;
}
.numberActive {
display: grid;
place-items: center center;
height: 30px;
width: 30px;
border-radius: 50%;
box-shadow: var(--ifm-global-shadow-tl);
padding: 0.25rem;
cursor: pointer;
background-color: var(--ifm-color-primary);
color: white;
}
.form {
display: grid;
text-align: center;
h3 {
color: var(--ifm-heading-color);
margin-bottom: 0.5rem;
}
//p {
// margin-bottom: 0.5rem;
//}
input,
textarea {
border: none;
border-radius: 6px;
font-size: 1.1rem;
padding: 0.75rem;
font-family: var(--ifm-font-family-base);
color: var(--color-gray-74);
margin-top: 1rem;
box-shadow: var(--ifm-global-shadow-lw);
width: 100%;
&::placeholder {
color: var(--color-gray-36);
}
}
button {
//margin-top: 20px;
margin-left: auto;
background: var(--ifm-color-primary);
color: white;
font-weight: 500;
font-size: 16px;
font-family: var(--ifm-font-family-base);
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
cursor: pointer;
}
}
.textAreaLabel {
margin-top: 1rem;
p {
margin-bottom: 0.5rem;
}
}
.buttonDisabled {
background-color: var(--ifm-color-gray-500) !important;
}
html[data-theme='dark'] {
input,
textarea {
background-color: white;
}
}
.topSection {
}
.bottomSection {
}
.successMessage {
display: block;
place-items: center center;
width: 100%;
text-align: center;
color: var(--ifm-color-primary);
font-size: 1.1rem;
font-weight: 500;
font-family: var(--ifm-font-family-base);
p {
margin-bottom: 0 !important;
}
}
html[data-theme='dark'] {
.successMessage {
color: var(--ifm-color-primary-lighter);
}
}
.errorText {
color: var(--ifm-color-primary);
margin-top: .8rem;
margin-bottom: 1rem;
font-weight: 500;
font-size: 1.1rem;
font-family: var(--ifm-font-family-base);
}
html[data-theme='dark'] {
.errorText {
color: var(--ifm-color-primary-lighter);
}
}
.errorAndButton {
}
.buttonContainer {
display: flex;
justify-content: flex-end;
}
span {
transition: .1s ease-out;
cursor: pointer;
&:hover {
transform: translateY(-3px) scale(1.1);
transform-origin: center center;
}
}

View File

@ -1,128 +0,0 @@
import React, { Fragment, useState } from 'react';
import { saTrack } from '@site/src/utils/segmentAnalytics';
import styles from './styles.module.scss';
import Hand from '@site/static/img/mascot-hand.png';
// Sleepy time for space between animations / state after submission
function wait(ms = 0) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const PageHelpful = () => {
const [isExpanded, setIsExpanded] = useState(false);
const [score, setScore] = useState<number>(10);
const [notes, setNotes] = useState<string>('');
const [hasResponse, setHasResponse] = useState<boolean>(false);
function handleNotes(e) {
setNotes(e.target.value);
}
const scores = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const recordResponse = async () => {
// POST request to Cloud Function
const sendData = async () => {
var myHeaders = new Headers();
myHeaders.append('Content-Type', 'application/json');
var raw = JSON.stringify({
feedback: {
isHelpful: score >= 7 ? `👍` : `👎`,
score,
notes,
pageTitle: document.title,
url: window.location.href,
},
});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow',
};
fetch('https://us-central1-websitecloud-352908.cloudfunctions.net/docs-feedback', requestOptions)
.then((response) => response.text())
.catch((error) => console.log('error', error));
};
sendData();
// For testing, this has been commented out so as not to introduce noise into our analytics
saTrack('Responded to Did You Find This Page Helpful', {
label: 'Responded to Did You Find This Page Helpful',
response: score >= 7 ? 'YES' : 'NO',
pageUrl: window.location.href,
});
// Clear state for next response
setHasResponse(true);
setIsExpanded(false);
setScore(10);
setNotes('');
await wait(1000);
setHasResponse(false);
};
return (
<div
className={isExpanded ? `${styles.wrapper} ${styles.expanded}` : `${styles.wrapper}`}
onClick={() => !isExpanded && setIsExpanded(true)}
>
{!isExpanded ? (
<div className={styles.emoji}>{!hasResponse ? <img src={Hand} /> : <p></p>}</div>
) : (
<div className={styles.feedback}>
<svg
onClick={() => setIsExpanded(false)}
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
strokeWidth={1.5}
stroke='currentColor'
className={styles.close}
>
<path strokeLinecap='round' strokeLinejoin='round' d='M6 18L18 6M6 6l12 12' />
</svg>
<div className={styles.form}>
<h3>Help us with some docs feedback!</h3>
<p>On a scale of 1 to 10, how helpful would you rate this page?</p>
<small>1 meaning the page is not helpful at all and 10 meaning you found what you needed quickly.</small>
<div className={styles.numberRow}>
{scores.map((scoreItem) => (
<div
key={scoreItem}
className={score === scoreItem ? styles.numberActive : ''}
onClick={() => setScore(scoreItem)}
onKeyDown={() => setScore(scoreItem)}
role='button'
tabIndex={0}
>
{scoreItem}
</div>
))}
</div>
<br />
<p>
Any general feedback you'd like to share? We'll take it all...tell us how well we're doing or where can
improve!
</p>
<textarea
value={notes}
placeholder='This section is optional ✌️'
rows='5'
onChange={(e) => handleNotes(e)}
/>
<button onClick={() => recordResponse()}>Send it!</button>
</div>
</div>
)}
</div>
);
};
export default PageHelpful;

View File

@ -1,177 +0,0 @@
.wrapper {
position: fixed;
display: grid;
place-items: center center;
bottom: 75px;
right: 15px;
height: 60px;
width: 60px;
color: var(--docsearch-text-color);
background: var(--ifm-card-background-color);
border-radius: 50%;
cursor: pointer;
transition: cubic-bezier(1, 0, 0, 1) 0.5s;
box-shadow: var(--ifm-global-shadow-tl);
svg {
height: 75%;
width: 75%;
cursor: pointer;
color: var(--ifm-heading-color);
}
}
.emoji {
display: grid;
place-items: center center;
height: 100%;
width: 100%;
img {
height: 35px;
width: auto;
}
p {
margin: 0;
padding: 0;
font-size: 1.5rem;
}
:hover {
animation-name: wave-animation;
animation-duration: 2.5s;
animation-iteration-count: 1;
transform-origin: 70% 70%;
display: inline-block;
}
}
.expanded {
width: 80vw;
height: 500px;
padding: 20px 20px;
opacity: 1;
border-radius: 8px;
z-index: 1000;
cursor: default;
overflow-y: scroll;
// media query for mobile
@media (max-width: 768px) {
width: 90vw;
height: 80svh;
}
}
.feedback {
display: grid;
place-items: start start;
height: 100%;
width: 100%;
padding: 20px;
text-align: center;
}
.numberRow {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
div {
display: grid;
place-items: center center;
height: 30px;
width: 30px;
border-radius: 50%;
box-shadow: var(--ifm-global-shadow-tl);
padding: 0.25rem;
cursor: pointer;
}
:hover {
box-shadow: var(--ifm-global-shadow-lw);
}
}
.numberActive {
background: var(--ifm-color-primary);
color: white;
}
.close {
position: absolute;
height: 20px !important;
width: 20px !important;
top: 10px;
right: 10px;
cursor: pointer;
}
.form {
display: grid;
text-align: left;
h3 {
color: var(--ifm-heading-color);
margin-bottom: 0.5rem;
}
p {
margin-bottom: 0.5rem;
}
input,
textarea {
border: none;
border-radius: 6px;
font-size: 1.1rem;
padding: 0.75rem;
font-family: var(--ifm-font-family-base);
// color: var(--color-gray-74);
margin-top: 1rem;
box-shadow: var(--ifm-global-shadow-lw);
}
button {
margin-top: 20px;
margin-left: auto;
background: var(--ifm-color-primary);
color: white;
font-weight: 500;
font-size: 16px;
font-family: var(--ifm-font-family-base);
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
cursor: pointer;
}
}
@keyframes wave-animation {
0% {
transform: rotate(0deg);
}
10% {
transform: rotate(14deg);
}
20% {
transform: rotate(-8deg);
}
30% {
transform: rotate(14deg);
}
40% {
transform: rotate(-4deg);
}
50% {
transform: rotate(10deg);
}
60% {
transform: rotate(0deg);
}
100% {
transform: rotate(0deg);
}
}

View File

@ -81,6 +81,22 @@
--color-gray-8: #e7ebef;
--color-gray-12: #dce2e8;
--color-gray-16: #cfd8df;
//new
--color-gray-20: #c2d0d8;
--color-gray-24: #b6c6ce;
--color-gray-28: #a9bcc4;
--color-gray-32: #9db2ba;
--color-gray-36: #91a8b0;
--color-gray-40: #849ea6;
--color-gray-44: #78949c;
--color-gray-48: #6c8a92;
--color-gray-52: #608088;
--color-gray-56: #54767e;
--color-gray-60: #486c74;
--color-gray-64: #3c626a;
--color-gray-68: #305860;
--color-gray-72: #244e56;
//end new
--color-gray-74: #344658;
--color-gray-78: #2c3b4b;
--color-gray-82: #23303d;

View File

@ -0,0 +1,61 @@
import React from 'react';
import clsx from 'clsx';
import { ThemeClassNames } from '@docusaurus/theme-common';
import { useDoc } from '@docusaurus/theme-common/internal';
import LastUpdated from '@theme/LastUpdated';
import EditThisPage from '@theme/EditThisPage';
import TagsListInline from '@theme/TagsListInline';
import styles from './styles.module.css';
import { Feedback } from '@site/src/components/Feedback/Feedback';
function TagsRow(props) {
return (
<div className={clsx(ThemeClassNames.docs.docFooterTagsRow, 'row margin-bottom--sm')}>
<div className="col">
<TagsListInline {...props} />
</div>
</div>
);
}
function EditMetaRow({ editUrl, lastUpdatedAt, lastUpdatedBy, formattedLastUpdatedAt }) {
return (
<div className={clsx(ThemeClassNames.docs.docFooterEditMetaRow, 'row')}>
<div className="col">{editUrl && <EditThisPage editUrl={editUrl} />}</div>
<div className={clsx('col', styles.lastUpdated)}>
{(lastUpdatedAt || lastUpdatedBy) && (
<LastUpdated
lastUpdatedAt={lastUpdatedAt}
formattedLastUpdatedAt={formattedLastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
/>
)}
</div>
</div>
);
}
export default function DocItemFooter() {
const { metadata } = useDoc();
const { editUrl, lastUpdatedAt, formattedLastUpdatedAt, lastUpdatedBy, tags } = metadata;
const canDisplayTagsRow = tags.length > 0;
const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow;
if (!canDisplayFooter) {
return null;
}
return (
<>
<Feedback metadata={metadata}/>
<footer className={clsx(ThemeClassNames.docs.docFooter, 'docusaurus-mt-lg')}>
{canDisplayTagsRow && <TagsRow tags={tags} />}
{canDisplayEditMetaRow && (
<EditMetaRow
editUrl={editUrl}
lastUpdatedAt={lastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
formattedLastUpdatedAt={formattedLastUpdatedAt}
/>
)}
</footer>
</>
);
}

View File

@ -0,0 +1,11 @@
.lastUpdated {
margin-top: 0.2rem;
font-style: italic;
font-size: smaller;
}
@media (min-width: 997px) {
.lastUpdated {
text-align: right;
}
}