diff --git a/gatsby/create-pages.ts b/gatsby/create-pages.ts index 182ca435d..af4b58485 100644 --- a/gatsby/create-pages.ts +++ b/gatsby/create-pages.ts @@ -108,6 +108,9 @@ export const createDocs = async ({ version: versionRecord[pathConfig.locale][pathConfig.repo][name], }, buildType: (process.env.WEBSITE_BUILD_TYPE ?? "prod") as BuildType, // prod | archive, default is prod; archive is for archive site + feature: { + feedback: true + } }, }); @@ -257,6 +260,9 @@ export const createDocHome = async ({ version: [], }, buildType: (process.env.WEBSITE_BUILD_TYPE ?? "prod") as BuildType, + feature: { + feedback: false + } }, }); }); diff --git a/locale/en/translation.json b/locale/en/translation.json index 552b0594e..fb3fc08af 100644 --- a/locale/en/translation.json +++ b/locale/en/translation.json @@ -108,6 +108,30 @@ "thumbUp": "Yes", "thumbDown": "No" }, + "docFeedbackSurvey": { + "action": { + "submit": "Submit", + "skip": "Skip" + }, + "message": { + "thank": "Thank you for your valuable feedback!" + }, + "positive": { + "title": "Thanks! What did you like?", + "easy": "Easy to understand", + "solvedProblem": "Solved my problem", + "helpToDecide": "Helped me decide to use the product", + "other": "Others" + }, + "negative": { + "title": "What went wrong?", + "hard": "Hard to understand", + "nothingFound": "Couldn't find what I need", + "inaccurate": "Inaccurate or out of date", + "sampleError": "Code sample errors", + "other": "Others" + } + }, "doc404": { "title": "Sorry, we can't find the page you were looking for.", "youMayWish": "You may wish to:", diff --git a/locale/ja/translation.json b/locale/ja/translation.json index 850bc9ec6..b83dbd6cb 100644 --- a/locale/ja/translation.json +++ b/locale/ja/translation.json @@ -108,6 +108,30 @@ "thumbUp": "はい", "thumbDown": "いいえ" }, + "docFeedbackSurvey": { + "action": { + "submit": "送信", + "skip": "スキップ" + }, + "message": { + "thank": "貴重なご意見をありがとうございます!" + }, + "positive": { + "title": "ありがとうございます!ドキュメントサイト改善のため、フィードバックをお願いします。", + "easy": "理解しやすい", + "solvedProblem": "問題を解決できた", + "helpToDecide": "製品使用を決断するのに役立った", + "other": "その他" + }, + "negative": { + "title": "ドキュメントサイト改善のため、フィードバックをお願いします。", + "hard": "分かりづらい", + "nothingFound": "必要なものが見つからなかった", + "inaccurate": "不正確または古い情報だった", + "sampleError": "コードサンプルがエラーだった", + "other": "その他" + } + }, "doc404": { "title": "お探しのページが見つかりません。", "youMayWish": "You may wish to:", diff --git a/locale/zh/translation.json b/locale/zh/translation.json index a3e849298..02b735352 100644 --- a/locale/zh/translation.json +++ b/locale/zh/translation.json @@ -105,6 +105,30 @@ "thumbUp": "是", "thumbDown": "否" }, + "docFeedbackSurvey": { + "action": { + "submit": "提交", + "skip": "跳过" + }, + "message": { + "thank": "感谢您的宝贵意见!" + }, + "positive": { + "title": "谢谢!您喜欢文档的原因是?", + "easy": "容易理解", + "solvedProblem": "解决了我的问题", + "helpToDecide": "帮助我决定使用该产品", + "other": "其他原因" + }, + "negative": { + "title": "您不喜欢文档的原因是?", + "hard": "难以理解", + "nothingFound": "找不到我想要的信息", + "inaccurate": "内容不准确或过时", + "sampleError": "代码示例错误", + "other": "其他原因" + } + }, "doc404": { "title": "抱歉,您要找的页面走丢了…", "youMayWish": "您可以:", diff --git a/src/components/Card/FeedbackSection/FeedbackSection.tsx b/src/components/Card/FeedbackSection/FeedbackSection.tsx new file mode 100644 index 000000000..4af923ccd --- /dev/null +++ b/src/components/Card/FeedbackSection/FeedbackSection.tsx @@ -0,0 +1,273 @@ +import { Trans } from "gatsby-plugin-react-i18next"; +import { + Box, + Stack, + Typography, + FormControl, + RadioGroup, + Radio, + FormControlLabel, +} from "@mui/material"; +import { ThumbUpOutlined, ThumbDownOutlined } from "@mui/icons-material"; +import { Locale } from "static/Type"; +import { useState } from "react"; +import { trackCustomEvent } from "gatsby-plugin-google-analytics"; +import { submitFeedbackDetail, submitLiteFeedback } from "./tracking"; +import { FeedbackCategory } from "./types"; +import { + ActionButton, + controlLabelSx, + labelProps, + radioSx, + ThumbButton, + typoFontFamily, +} from "./components"; + +interface FeedbackSectionProps { + title: string; + locale: Locale; +} + +export function FeedbackSection({ title, locale }: FeedbackSectionProps) { + const [thumbVisible, setThumbVisible] = useState(true); + const [helpful, setHelpful] = useState(); + const [surveyVisible, setSurverVisible] = useState(false); + + const onThumbClick = (helpful: boolean) => { + trackCustomEvent({ + category: helpful ? `doc-${locale}-useful` : `doc-${locale}-useless`, + action: "click", + label: title, + transport: "beacon", + }); + submitLiteFeedback({ + locale, + category: helpful ? FeedbackCategory.Positive : FeedbackCategory.Negative + }) + + setHelpful(helpful); + setThumbVisible(false); + setSurverVisible(true); + }; + + const [positiveVal, setPositiveVal] = useState(""); + const onPositiveChange = (event: React.ChangeEvent) => { + setPositiveVal((event.target as HTMLInputElement).value); + }; + + const [negativeVal, setNegativeVal] = useState(""); + const onNegativeChange = (event: React.ChangeEvent) => { + setNegativeVal((event.target as HTMLInputElement).value); + }; + + const [submitted, setSubmitted] = useState(false); + + const onPositiveSubmit = () => { + submitFeedbackDetail({ + locale, + category: FeedbackCategory.Positive, + reason: positiveVal, + }); + setSurverVisible(false); + setSubmitted(true); + }; + const onNegativeSubmit = () => { + submitFeedbackDetail({ + locale, + category: FeedbackCategory.Negative, + reason: negativeVal, + }); + setSurverVisible(false); + setSubmitted(true); + }; + + const onSkip = () => { + setSurverVisible(false); + setPositiveVal(""); + setNegativeVal(""); + setSubmitted(true); + }; + + return ( + + + + + {thumbVisible && ( + + } + className="FeedbackBtn-thumbUp" + aria-label="Thumb Up" + onClick={() => onThumbClick(true)} + > + + + } + className="FeedbackBtn-thumbDown" + aria-label="Thumb Down" + onClick={() => onThumbClick(false)} + > + + + + )} + {surveyVisible && helpful && ( + + + + + + + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + + + + + + + + + + + + )} + {surveyVisible && !helpful && ( + + + + + + + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + } + control={} + componentsProps={labelProps} + sx={controlLabelSx} + /> + + + + + + + + + + + + )} + {submitted && ( + + + + )} + + ); +} diff --git a/src/components/Card/FeedbackSection/components.ts b/src/components/Card/FeedbackSection/components.ts new file mode 100644 index 000000000..779dcb428 --- /dev/null +++ b/src/components/Card/FeedbackSection/components.ts @@ -0,0 +1,46 @@ +import { Button, styled } from "@mui/material"; + +export const ThumbButton = styled(Button)(({ theme }) => ({ + backgroundColor: theme.palette.website.k1, + boxShadow: "none", + "&:hover": { + backgroundColor: "#0A85C2", + boxShadow: "0px 1px 4px rgba(0, 0, 0, 0.16)", + }, + ".MuiButton-startIcon": { + marginRight: 4, + }, +})); + +export const ActionButton = styled(Button)(({ theme }) => ({ + color: theme.palette.website.f1, + backgroundColor: "#F9F9F9", + border: "1px solid #E6E6E6", + padding: "4px 8px", + "&:hover": { + backgroundColor: "#F9F9F9", + border: "1px solid #E6E6E6", + boxShadow: "0px 1px 4px rgba(0, 0, 0, 0.08)", + }, +})); + +export const typoFontFamily = `-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"` + +export const controlLabelSx = { + ml: 0, + py: "6px", +}; +export const radioSx = { + color: "#BBBBBB", + p: "2px", + mr: "8px", + "&.Mui-checked": { + color: "website.k1", + }, +}; +export const labelProps = { + typography: { + fontFamily: typoFontFamily, + color: "website.f1", + }, +}; diff --git a/src/components/Card/FeedbackSection/index.ts b/src/components/Card/FeedbackSection/index.ts new file mode 100644 index 000000000..919d353ed --- /dev/null +++ b/src/components/Card/FeedbackSection/index.ts @@ -0,0 +1 @@ +export * from './FeedbackSection' diff --git a/src/components/Card/FeedbackSection/tracking.ts b/src/components/Card/FeedbackSection/tracking.ts new file mode 100644 index 000000000..7dcf21262 --- /dev/null +++ b/src/components/Card/FeedbackSection/tracking.ts @@ -0,0 +1,129 @@ +import axios from "axios"; +import { Locale } from "static/Type"; +import { FeedbackCategory, TrackingType } from "./types"; + +const hubspotFormURL = 'https://api.hsforms.com/submissions/v3/integration/submit/4466002/{{formId}}' + +const hubspotForms = [ + { + category: FeedbackCategory.Positive, + tracking: TrackingType.Lite, + formId: "08c7738f-9e08-4283-a080-179c224dfe9e", + }, + { + category: FeedbackCategory.Negative, + tracking: TrackingType.Lite, + formId: '2df2e54d-d82b-4262-8480-a250959503e8' + }, + { + category: FeedbackCategory.Positive, + tracking: TrackingType.Detail, + formId: "89234e15-05f2-42e2-9005-f50443c310db", + }, + { + category: FeedbackCategory.Negative, + tracking: TrackingType.Detail, + formId: 'bfc8bbe6-8ed9-4a4c-8e9e-a875ee3ed26d' + } +] + +function getCookie(name: string) { + const match = document.cookie.match( + RegExp("(?:^|;\\s*)" + name + "=([^;]*)") + ); + return match ? match[1] : null; +} + +export function submitLiteFeedback(options: { + locale: Locale; + category: FeedbackCategory +}) { + const { locale, category } = options; + const formId = hubspotForms.find((item) => item.tracking === TrackingType.Lite && item.category === category)?.formId; + + if (!formId) { + return; + } + + const url = hubspotFormURL.replace('{{formId}}', formId) + + return axios + .post(url, { + fields: [ + { + objectTypeId: "0-1", + name: "hs_language", + value: locale, + }, + { + objectTypeId: "0-1", + name: 'website', + value: document.URL + } + ], + context: { + hutk: getCookie("hubspotutk"), + pageUri: document.URL, + pageName: document.title, + }, + legalConsentOptions: { + consent: { + consentToProcess: true, + text: "I agree to the PingCAP Privacy Policy.", + }, + }, + }) + .catch((e) => { + console.error("Failed to submit lite feedback to hubspot", e); + }); +} + +export function submitFeedbackDetail(options: { + locale: Locale; + category: FeedbackCategory; + reason: string; +}) { + const { locale, category, reason } = options; + const formId = hubspotForms.find((item) => item.tracking === TrackingType.Detail && item.category === category)?.formId; + + if (!formId) { + return; + } + + const url = hubspotFormURL.replace('{{formId}}', formId) + + return axios + .post(url, { + fields: [ + { + objectTypeId: "0-1", + name: "hs_language", + value: locale, + }, + { + objectTypeId: "0-1", + name: "message", + value: reason, + }, + { + objectTypeId: "0-1", + name: 'website', + value: document.URL + } + ], + context: { + hutk: getCookie("hubspotutk"), + pageUri: document.URL, + pageName: document.title, + }, + legalConsentOptions: { + consent: { + consentToProcess: true, + text: "I agree to the PingCAP Privacy Policy.", + }, + }, + }) + .catch((e) => { + console.error("Failed to submit feedback details to hubspot", e); + }); +} diff --git a/src/components/Card/FeedbackSection/types.ts b/src/components/Card/FeedbackSection/types.ts new file mode 100644 index 000000000..0f665c99b --- /dev/null +++ b/src/components/Card/FeedbackSection/types.ts @@ -0,0 +1,13 @@ +export const FeedbackCategory = { + Positive: 'positive', + Negative: 'negative' +} as const + +export type FeedbackCategory = typeof FeedbackCategory[keyof typeof FeedbackCategory] + +export const TrackingType = { + Lite: 'lite', + Detail: 'detail' +} as const + +export type TrackingType = typeof TrackingType[keyof typeof TrackingType] diff --git a/src/templates/DocTemplate.tsx b/src/templates/DocTemplate.tsx index 439be158a..67a676761 100644 --- a/src/templates/DocTemplate.tsx +++ b/src/templates/DocTemplate.tsx @@ -13,7 +13,6 @@ import { LeftNavDesktop, LeftNavMobile } from "components/Navigation/LeftNav"; import MDXContent from "components/Layout/MDXContent"; import RightNav, { RightNavMobile } from "components/Navigation/RightNav"; import ScrollToTopBtn from "components/Button/ScrollToTopBtn"; -import FeedbackBtn from "components/Button/FeedbackBtn"; import { TableOfContent, PageContext, @@ -25,9 +24,14 @@ import { import Seo from "components/Layout/Seo"; import { getStable, generateUrl } from "utils"; import GitCommitInfoCard from "components/Card/GitCommitInfoCard"; +import { FeedbackSection } from "components/Card/FeedbackSection"; interface DocTemplateProps { - pageContext: PageContext & { pageUrl: string; buildType: BuildType }; + pageContext: PageContext & { + pageUrl: string; + buildType: BuildType; + feature?: { feedback?: boolean }; + }; data: { site: { siteMetadata: { @@ -46,7 +50,15 @@ interface DocTemplateProps { } export default function DocTemplate({ - pageContext: { name, availIn, pathConfig, filePath, pageUrl, buildType }, + pageContext: { + name, + availIn, + pathConfig, + filePath, + pageUrl, + buildType, + feature, + }, data, }: DocTemplateProps) { const { @@ -195,6 +207,12 @@ export default function DocTemplate({ title={frontmatter.title} /> )} + {!!feature?.feedback && buildType !== "archive" && ( + + )} {!frontmatter?.hide_sidebar && ( <> @@ -232,21 +250,6 @@ export default function DocTemplate({ )} - {language !== "ja" && buildType !== "archive" && ( - - - - )}