mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
website: Hide feedback popup when user seen form
Signed-off-by: Charlie Egan <charlie@styra.com>
This commit is contained in:
committed by
Charlie Egan
parent
ade26387f2
commit
02b0a2db32
@@ -1,35 +1,73 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import { Icon } from "@iconify/react";
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
/**
|
||||
* A feedback form component that is automatically added to the end of documentation pages.
|
||||
* It can also be manually included in other pages by setting enablePopup to true.
|
||||
* The form allows users to provide positive or negative feedback with optional comments
|
||||
* and email for follow-up. Feedback is submitted to Netlify Forms via AJAX.
|
||||
*
|
||||
* Note: When adding new fields to this form, remember to update the static/netlify-forms.html
|
||||
* file so that Netlify knows which fields to permit. If this is not updated, new fields
|
||||
* in the form will be dropped.
|
||||
*/
|
||||
export default function FeedbackForm({ enablePopup = false }) {
|
||||
// this is used to ensure the response is categorized in netlify forms and that
|
||||
// local state is remembered.
|
||||
const formName = "page-feedback";
|
||||
|
||||
const [feedbackType, setFeedbackType] = useState("");
|
||||
const [comment, setComment] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [path, setPath] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const [showFloatingPopup, setShowFloatingPopup] = useState(false);
|
||||
const [hasScrolled, setHasScrolled] = useState(false);
|
||||
const [popupEnabled, setPopupEnabled] = useState(enablePopup);
|
||||
const feedbackFormRef = useRef(null);
|
||||
|
||||
// The popup will never show before the user has been on the page for 10s
|
||||
const feedbackPopupTimeoutMs = 10000;
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
if (window.location.hostname === "localhost") {
|
||||
// setMessage("Feedback form disabled on localhost.");
|
||||
// return;
|
||||
}
|
||||
setUrl(window.location);
|
||||
setPath(window.location.pathname);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// This effect sets up an intersection observer to detect when the feedback form
|
||||
// comes into view. When the form is visible, it permanently disables the popup
|
||||
// to prevent it from showing again. This ensures users who have found the form
|
||||
// don't get interrupted by the popup.
|
||||
useEffect(() => {
|
||||
if (enablePopup) {
|
||||
if (feedbackFormRef.current) {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
setShowFloatingPopup(false);
|
||||
setPopupEnabled(false);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(feedbackFormRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
}, [feedbackFormRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (popupEnabled) {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 100) {
|
||||
setHasScrolled(true);
|
||||
@@ -43,19 +81,21 @@ export default function FeedbackForm({ enablePopup = false }) {
|
||||
if (hasScrolled) {
|
||||
setShowFloatingPopup(true);
|
||||
}
|
||||
}, 10000);
|
||||
}, feedbackPopupTimeoutMs);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}
|
||||
}, [enablePopup, hasScrolled]);
|
||||
}, [popupEnabled, hasScrolled]);
|
||||
|
||||
// feedback is stored in local storage to prevent the form / popup from showing again
|
||||
useEffect(() => {
|
||||
const feedbackSubmitted = localStorage.getItem(`${formName}-${path}`);
|
||||
if (feedbackSubmitted) {
|
||||
setMessage("Thank you for your feedback!");
|
||||
setPopupEnabled(false);
|
||||
}
|
||||
}, [path]);
|
||||
|
||||
@@ -91,151 +131,171 @@ export default function FeedbackForm({ enablePopup = false }) {
|
||||
}
|
||||
};
|
||||
|
||||
const FloatingPopup = () => (
|
||||
<div className={styles.popup}>
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={() => setShowFloatingPopup(false)}
|
||||
aria-label="Close feedback popup"
|
||||
>
|
||||
<Icon icon="mdi:close" size="24px" />
|
||||
</button>
|
||||
<div className={styles["popup-content"]}>
|
||||
<p>How's this page?</p>
|
||||
<div className={styles["popup-buttons"]}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFeedbackType("positive");
|
||||
scrollToFeedback();
|
||||
}}
|
||||
className={`${styles.button} ${styles.positive}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-up" size="24px" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFeedbackType("negative");
|
||||
scrollToFeedback();
|
||||
}}
|
||||
className={`${styles.button} ${styles.negative}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-down" size="24px" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (message !== "") {
|
||||
return (
|
||||
<BrowserOnly>
|
||||
{() => {
|
||||
return (
|
||||
{() => (
|
||||
<Admonition
|
||||
className={styles.wrapper}
|
||||
type="note"
|
||||
icon={<Icon icon="mdi:feedback-outline" size="48px" />}
|
||||
title="Feedback"
|
||||
>
|
||||
<p>{message}</p>
|
||||
|
||||
<p>
|
||||
Got more to say? Questions for the OPA experts? Please come and find us on the{" "}
|
||||
<strong>
|
||||
<a href="https://slack.openpolicyagent.org">OPA Slack</a>.
|
||||
</strong>{" "}
|
||||
The <code>#help</code> channel is a great place to get stated.
|
||||
</p>
|
||||
</Admonition>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserOnly>
|
||||
{() => (
|
||||
<>
|
||||
{popupEnabled && showFloatingPopup && (
|
||||
<FloatingPopup
|
||||
onClose={() => setShowFloatingPopup(false)}
|
||||
onFeedbackSelect={(type) => {
|
||||
setFeedbackType(type);
|
||||
scrollToFeedback();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div ref={feedbackFormRef}>
|
||||
<Admonition
|
||||
className={styles.wrapper}
|
||||
type="note"
|
||||
icon={<Icon icon="mdi:feedback-outline" size="48px" />}
|
||||
title="Feedback"
|
||||
>
|
||||
<p>{message}</p>
|
||||
|
||||
<p>
|
||||
Got more to say? Questions for the OPA experts? Please come and find us on the{" "}
|
||||
<strong>
|
||||
<a href="https://slack.openpolicyagent.org">OPA Slack</a>.
|
||||
</strong>{" "}
|
||||
The <code>#help</code> channel is a great place to get stated.
|
||||
</p>
|
||||
<p>We are always trying to make our documentation the best it can be and welcome your comments.</p>
|
||||
<Form
|
||||
formName={formName}
|
||||
url={url}
|
||||
feedbackType={feedbackType}
|
||||
setFeedbackType={setFeedbackType}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</Admonition>
|
||||
);
|
||||
}}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: remember to update the static/netlify-forms.html file so that they
|
||||
// know which files to permit. If this is not updated, new fields in the
|
||||
// form will be dropped.
|
||||
return (
|
||||
<BrowserOnly>
|
||||
{() => {
|
||||
return (
|
||||
<>
|
||||
{enablePopup && showFloatingPopup && <FloatingPopup />}
|
||||
<div ref={feedbackFormRef}>
|
||||
<Admonition
|
||||
className={styles.wrapper}
|
||||
type="note"
|
||||
icon={<Icon icon="mdi:feedback-outline" size="48px" />}
|
||||
title="Feedback"
|
||||
>
|
||||
<p>We are always trying to make our documentation the best it can be and welcome your comments.</p>
|
||||
<form
|
||||
className={styles.feedbackForm}
|
||||
name={formName}
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input type="hidden" name="form-name" value={formName} />
|
||||
<input type="hidden" name="url" value={url} />
|
||||
<input type="hidden" name="feedback-type" value={feedbackType} />
|
||||
|
||||
<div className={styles.section} style={{ display: "flex", gap: "10px" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackType("positive")}
|
||||
className={`${styles.button} ${feedbackType === "positive" ? styles.positive : ""}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-up" size="48px" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackType("negative")}
|
||||
className={`${styles.button} negative ${feedbackType === "negative" ? styles.negative : ""
|
||||
}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-down" size="48px" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{feedbackType && (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.comment}>
|
||||
<textarea
|
||||
name="comment"
|
||||
rows="4"
|
||||
placeholder={feedbackType === "positive"
|
||||
? "What do you like about this page? (optional)"
|
||||
: "What could be improved on this page?"}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
required={feedbackType === "negative"}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.email}>
|
||||
<div>
|
||||
<input type="email" name="email" placeholder="Email (optional)" />
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.note}>Email will only be used for comment follow-up.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feedbackType && (
|
||||
<div className={styles.section}>
|
||||
<button className={styles.button} type="submit">
|
||||
<Icon icon="mdi:send" size="48px" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Admonition>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A floating popup that appears after scrolling to encourage users to provide feedback.
|
||||
* The popup shows thumbs up/down buttons that, when clicked, will scroll the user to
|
||||
* the feedback form and pre-select their feedback type. The popup is automatically
|
||||
* hidden when the feedback form comes into view.
|
||||
*/
|
||||
const FloatingPopup = ({ onClose, onFeedbackSelect }) => (
|
||||
<div className={styles.popup}>
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={onClose}
|
||||
aria-label="Close feedback popup"
|
||||
>
|
||||
<Icon icon="mdi:close" size="24px" />
|
||||
</button>
|
||||
<div className={styles["popup-content"]}>
|
||||
<p>How's this page?</p>
|
||||
<div className={styles["popup-buttons"]}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedbackSelect("positive")}
|
||||
className={`${styles.button} ${styles.positive}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-up" size="24px" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedbackSelect("negative")}
|
||||
className={`${styles.button} ${styles.negative}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-down" size="24px" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Form = ({
|
||||
formName,
|
||||
url,
|
||||
feedbackType,
|
||||
setFeedbackType,
|
||||
comment,
|
||||
setComment,
|
||||
handleSubmit,
|
||||
}) => (
|
||||
<form
|
||||
className={styles.feedbackForm}
|
||||
name={formName}
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input type="hidden" name="form-name" value={formName} />
|
||||
<input type="hidden" name="url" value={url} />
|
||||
<input type="hidden" name="feedback-type" value={feedbackType} />
|
||||
|
||||
<div className={styles.section} style={{ display: "flex", gap: "10px" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackType("positive")}
|
||||
className={`${styles.button} ${feedbackType === "positive" ? styles.positive : ""}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-up" size="48px" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackType("negative")}
|
||||
className={`${styles.button} negative ${feedbackType === "negative" ? styles.negative : ""}`}
|
||||
>
|
||||
<Icon icon="mdi:thumbs-down" size="48px" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{feedbackType && (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.comment}>
|
||||
<textarea
|
||||
name="comment"
|
||||
rows="4"
|
||||
placeholder={feedbackType === "positive"
|
||||
? "What do you like about this page? (optional)"
|
||||
: "What could be improved on this page?"}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
required={feedbackType === "negative"}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.email}>
|
||||
<div>
|
||||
<input type="email" name="email" placeholder="Email (optional)" />
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.note}>Email will only be used for comment follow-up.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feedbackType && (
|
||||
<div className={styles.section}>
|
||||
<button className={styles.button} type="submit">
|
||||
<Icon icon="mdi:send" size="48px" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user