Skip to content

Commit

Permalink
feat(feedback): add feedback form (#399)
Browse files Browse the repository at this point in the history
* feat(feedback): add feedback form

* feat(feedback): add test

* fix: enable feedback for non-JS browser

* refactor: split UserFeedback into sub-components

* refactor: rename helpful -> rating

---------

Co-authored-by: Pram Gurusinga <[email protected]>
Co-authored-by: Joschka de Cuveland <[email protected]>
  • Loading branch information
3 people authored Nov 13, 2023
1 parent 92842d4 commit e184e5f
Show file tree
Hide file tree
Showing 11 changed files with 396 additions and 123 deletions.
4 changes: 3 additions & 1 deletion app/components/Textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ const Textarea = ({
id: name,
placeholder,
})}
className={classNames("ds-textarea", { "has-error": error })}
className={classNames("ds-textarea placeholder-gray-600", {
"has-error": error,
})}
aria-invalid={error !== undefined}
aria-describedby={error && errorId}
aria-errormessage={error && errorId}
Expand Down
92 changes: 0 additions & 92 deletions app/components/UserFeedback.tsx

This file was deleted.

83 changes: 83 additions & 0 deletions app/components/UserFeedback/FeedbackFormBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { z } from "zod";
import { withZod } from "@remix-validated-form/with-zod";
import { ValidatedForm } from "remix-validated-form";
import CloseIcon from "@mui/icons-material/CloseOutlined";
import SendIcon from "@mui/icons-material/SendOutlined";
import Button from "../Button";
import ButtonContainer from "../ButtonContainer";
import Heading from "../Heading";
import Textarea from "../Textarea";

export const feedbackFormName = "feedbackForm";
const feedbackFieldname = "feedback";
const feedbackButtonFieldname = "feedbackButton";

enum FeedbackButtons {
Abort = "abort",
Submit = "submit",
}

export const feedbackValidator = withZod(
z.object({
feedback: z
.string()
.refine(
(feedback) => !/\s0\d/.test(feedback),
"Bitte geben sie keine Telefonnummer ein.",
)
.refine(
(feedback) => !feedback.includes("@"),
"Bitte geben sie keine E-Mailadresse ein.",
),
}),
);

export interface FeedbackBoxProps {
destination: string;
heading: string;
placeholder: string;
abortButtonLabel: string;
submitButtonLabel: string;
}

export const FeedbackFormBox = ({
destination,
heading,
placeholder,
abortButtonLabel,
submitButtonLabel,
}: FeedbackBoxProps) => (
<>
<Heading look="ds-label-01-bold" tagName="h2" text={heading} />
<ValidatedForm
validator={feedbackValidator}
subaction={feedbackFormName}
method="post"
action={destination}
>
<div className="ds-stack-16">
<Textarea name={feedbackFieldname} placeholder={placeholder} />
<ButtonContainer>
<Button
iconLeft={<CloseIcon />}
look="tertiary"
name={feedbackButtonFieldname}
value={FeedbackButtons.Abort}
type="submit"
>
{abortButtonLabel}
</Button>
<Button
look="primary"
iconLeft={<SendIcon />}
name={feedbackButtonFieldname}
value={FeedbackButtons.Submit}
type="submit"
>
{submitButtonLabel}
</Button>
</ButtonContainer>
</div>
</ValidatedForm>
</>
);
17 changes: 17 additions & 0 deletions app/components/UserFeedback/PostSubmissionBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Heading from "../Heading";
import RichText from "../RichText";

export interface PostSubmissionBoxProps {
heading: string;
text: string;
}

export const PostSubmissionBox = ({
heading,
text,
}: PostSubmissionBoxProps) => (
<div data-testid="user-feedback-submission">
<Heading look="ds-label-01-bold" tagName="h2" text={heading} />
<RichText markdown={text} />
</div>
);
62 changes: 62 additions & 0 deletions app/components/UserFeedback/RatingBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useEffect, useState } from "react";
import { useFetcher } from "@remix-run/react";
import ThumbDownIcon from "@mui/icons-material/ThumbDownOutlined";
import ThumbUpIcon from "@mui/icons-material/ThumbUpOutlined";
import Button from "../Button";
import ButtonContainer from "../ButtonContainer";
import Heading from "../Heading";

export const userRatingFieldname = "wasHelpful";

export interface RatingBoxProps {
heading: string;
url: string;
context?: string;
yesButtonLabel: string;
noButtonLabel: string;
}

export const RatingBox = ({
heading,
url,
context,
yesButtonLabel,
noButtonLabel,
}: RatingBoxProps) => {
const ratingFetcher = useFetcher();
const [jsAvailable, setJsAvailable] = useState(false);
useEffect(() => setJsAvailable(true), []);

return (
<>
<Heading look="ds-label-01-bold" tagName="h2" text={heading} />
<ratingFetcher.Form
method="post"
action={`/action/send-rating?url=${url}&context=${
context ?? ""
}&js=${String(jsAvailable)}`}
>
<ButtonContainer>
<Button
iconLeft={<ThumbUpIcon />}
look="tertiary"
name={userRatingFieldname}
value="yes"
type="submit"
>
{yesButtonLabel}
</Button>
<Button
iconLeft={<ThumbDownIcon />}
look="tertiary"
name={userRatingFieldname}
value="no"
type="submit"
>
{noButtonLabel}
</Button>
</ButtonContainer>
</ratingFetcher.Form>
</>
);
};
53 changes: 53 additions & 0 deletions app/components/UserFeedback/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useLocation } from "@remix-run/react";
import Background from "../Background";
import Container from "../Container";
import {
type PostSubmissionBoxProps,
PostSubmissionBox,
} from "./PostSubmissionBox";
import { type FeedbackBoxProps, FeedbackFormBox } from "./FeedbackFormBox";
import { type RatingBoxProps, RatingBox } from "./RatingBox";

export enum BannerState {
ShowRating = "showRating",
ShowFeedback = "showFeedback",
FeedbackGiven = "feedbackGiven",
}

type UserFeedbackProps = {
bannerState: BannerState;
rating: Omit<RatingBoxProps, "url">;
feedback: Omit<FeedbackBoxProps, "destination">;
postSubmission: PostSubmissionBoxProps;
};

export default function UserFeedback(props: Readonly<UserFeedbackProps>) {
const { pathname } = useLocation();

return (
<Background paddingTop="32" paddingBottom="40" backgroundColor="white">
<Container
paddingTop="32"
paddingBottom="32"
overhangingBackground
backgroundColor="midBlue"
>
<div className="ds-stack-16" data-testid="user-feedback-banner">
{
{
[BannerState.ShowRating]: (
<RatingBox url={pathname} {...props.rating} />
),
[BannerState.ShowFeedback]: (
<FeedbackFormBox destination={pathname} {...props.feedback} />
),
[BannerState.FeedbackGiven]: (
<PostSubmissionBox {...props.postSubmission} />
),
}[props.bannerState]
}
</div>
</Container>
</Background>
);
}
25 changes: 16 additions & 9 deletions app/routes/action.send-rating.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { ActionFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { wasHelpfulFieldname } from "~/components/UserFeedback";
import { BannerState } from "~/components/UserFeedback";
import { userRatingFieldname } from "~/components/UserFeedback/RatingBox";
import { getSessionForContext } from "~/services/session";
import { PostHog } from "posthog-node";
import { config } from "~/services/env/web";
import { bannerStateName } from "~/services/feedback/handleFeedback";

export const loader = () => redirect("/");

Expand All @@ -13,28 +15,33 @@ const posthogClient = POSTHOG_API_KEY
: undefined;

export const action = async ({ request }: ActionFunctionArgs) => {
console.log("action");
const { searchParams } = new URL(request.url);
const clientJavaScriptAvailable = searchParams.get("js") === "true";
const url = searchParams.get("url") ?? "";
const context = searchParams.get("context") ?? "";
const formData = await request.formData();

const cookie = request.headers.get("Cookie");
const { getSession, commitSession } = getSessionForContext("main");
const session = await getSession(cookie);
const wasHelpful =
(session.get(wasHelpfulFieldname) as Record<string, boolean>) ?? {};
console.log({ wasHelpful });
const formData = await request.formData();
wasHelpful[url] = formData.get(wasHelpfulFieldname) === "yes";
session.set(wasHelpfulFieldname, wasHelpful);

const userRatings =
(session.get(userRatingFieldname) as Record<string, boolean>) ?? {};
userRatings[url] = formData.get(userRatingFieldname) === "yes";
session.set(userRatingFieldname, userRatings);

const bannerState =
(session.get(bannerStateName) as Record<string, BannerState>) ?? {};
bannerState[url] = BannerState.ShowFeedback;
session.set(bannerStateName, bannerState);

const headers = { "Set-Cookie": await commitSession(session) };

posthogClient?.capture({
distinctId: ENVIRONMENT,
event: "rating given",
// eslint-disable-next-line camelcase
properties: { wasHelpful: wasHelpful[url], $current_url: url, context },
properties: { wasHelpful: userRatings[url], $current_url: url, context },
});

return clientJavaScriptAvailable
Expand Down
Loading

0 comments on commit e184e5f

Please sign in to comment.