Initial work on feedback form

This commit is contained in:
Matt Aitken
2023-06-20 22:21:36 +01:00
parent 287b6c6bc7
commit ec738515b7
7 changed files with 229 additions and 13 deletions
+125
View File
@@ -0,0 +1,125 @@
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid";
import { Button } from "./primitives/Buttons";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "./primitives/Sheet";
import { Header1 } from "./primitives/Headers";
import { NamedIconInBox } from "./primitives/NamedIcon";
import { Paragraph } from "./primitives/Paragraph";
import {
Form,
useActionData,
useLocation,
useNavigation,
} from "@remix-run/react";
import { Fieldset } from "./primitives/Fieldset";
import { InputGroup } from "./primitives/InputGroup";
import { Label } from "./primitives/Label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "./primitives/Select";
import { FormButtons } from "./primitives/FormButtons";
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { schema } from "~/routes/resources.feedback";
import { FormError } from "./primitives/FormError";
import { Input } from "./primitives/Input";
import { TextArea } from "./primitives/TextArea";
import { useState } from "react";
import { set } from "jsonpointer";
export function Feedback() {
const [open, setOpen] = useState(false);
const location = useLocation();
const lastSubmission = useActionData();
const navigation = useNavigation();
const [form, { redirectPath, feedbackType, message }] = useForm({
id: "accept-invite",
lastSubmission,
onValidate({ formData }) {
return parse(formData, { schema });
},
});
if (
open &&
navigation.formAction === "/resources/feedback" &&
form.error === undefined &&
form.errors.length === 0
) {
setOpen(false);
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild={true}>
<Button
variant="secondary/small"
LeadingIcon={ChatBubbleLeftRightIcon}
shortcut={{ key: "f" }}
onClick={() => console.log("feedback")}
>
Send us feedback
</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader className="justify-between">Give us feedback</SheetHeader>
<SheetBody>
<Paragraph variant="small" className="mb-4">
We'd love to hear your feedback, good, bad or ugly.
</Paragraph>
<Form method="post" action="/resources/feedback" {...form.props}>
<Fieldset>
<input
value={location.pathname}
{...conform.input(redirectPath, { type: "hidden" })}
/>
<InputGroup>
<Label>What kind of feedback do you have?</Label>
<SelectGroup>
<Select {...conform.input(feedbackType)} defaultValue="bug">
<SelectTrigger size="medium" width="full">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bug">Bug report</SelectItem>
<SelectItem value="feature">Feature request</SelectItem>
</SelectContent>
</Select>
</SelectGroup>
<FormError id={feedbackType.errorId}>
{feedbackType.error}
</FormError>
</InputGroup>
<InputGroup>
<Label>Message</Label>
<TextArea {...conform.textarea(message)} />
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<FormButtons
confirmButton={
<Button type="submit" variant="primary/medium">
Send
</Button>
}
/>
</Fieldset>
</Form>
</SheetBody>
</SheetContent>
</Sheet>
);
}
@@ -1,12 +1,11 @@
import { Popover, Transition } from "@headlessui/react";
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { Fragment } from "react";
import { cn } from "~/utils/cn";
import { Feedback } from "../Feedback";
import { LogoIcon } from "../LogoIcon";
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
import { Button, LinkButton } from "../primitives/Buttons";
import { LinkButton } from "../primitives/Buttons";
import { Breadcrumb } from "./Breadcrumb";
export function NavBar() {
@@ -27,15 +26,7 @@ export function NavBar() {
>
Documentation
</LinkButton>
<Button
variant="secondary/small"
data-attr="posthog-feedback-button"
LeadingIcon={ChatBubbleLeftRightIcon}
shortcut={{ key: "f" }}
onClick={() => console.log("feedback")}
>
Send us feedback
</Button>
<Feedback />
</div>
</div>
);
@@ -0,0 +1,16 @@
import { cn } from "~/utils/cn";
type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {};
export function TextArea({ className, rows, ...props }: TextAreaProps) {
return (
<textarea
{...props}
rows={rows ?? 6}
className={cn(
"w-full rounded-md border border-slate-800 bg-slate-850 px-3 text-sm text-bright ring-offset-background transition file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground hover:border-slate-750 hover:bg-slate-800 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
/>
);
}
+1
View File
@@ -35,6 +35,7 @@ const EnvironmentSchema = z.object({
REPLY_TO_EMAIL: z.string(),
RESEND_API_KEY: z.string(),
SESSION_SECRET: z.string(),
PLAIN_API_KEY: z.string().optional(),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
@@ -0,0 +1,48 @@
import { parse } from "@conform-to/zod";
import { ActionArgs, json } from "@remix-run/server-runtime";
import { PlainClient } from "@team-plain/typescript-sdk";
import { z } from "zod";
import { env } from "~/env.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { CreateEndpointError } from "~/services/endpoints/createEndpoint.server";
import { requireUserId } from "~/services/session.server";
let client: PlainClient | undefined;
const feedbackType = z.union([z.literal("bug"), z.literal("feature")], {
required_error: "Must be either 'bug' or 'feature'",
invalid_type_error: "Must be either 'bug' or 'feature'",
});
export const schema = z.object({
redirectPath: z.string(),
feedbackType,
message: z.string().min(1, "Must be at least 1 character"),
});
export async function action({ request }: ActionArgs) {
const userId = await requireUserId(request);
const formData = await request.formData();
const submission = parse(formData, { schema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
if (env.PLAIN_API_KEY) {
client = new PlainClient({
apiKey: env.PLAIN_API_KEY,
});
}
return redirectWithSuccessMessage(
submission.value.redirectPath,
request,
"Feedback submitted"
);
} catch (e) {
return json(e, { status: 400 });
}
}
+1
View File
@@ -70,6 +70,7 @@
"@remix-run/server-runtime": "1.16.1",
"@sentry/remix": "^7.53.1",
"@tanstack/react-table": "^8.0.0-alpha.87",
"@team-plain/typescript-sdk": "^2.2.0",
"@trigger.dev/companyicons": "^1.5.6",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/internal": "workspace:*",
+35 -1
View File
@@ -99,6 +99,7 @@ importers:
'@tailwindcss/forms': ^0.5.3
'@tailwindcss/typography': ^0.5.9
'@tanstack/react-table': ^8.0.0-alpha.87
'@team-plain/typescript-sdk': ^2.2.0
'@testing-library/cypress': ^8.0.3
'@testing-library/dom': ^8.18.1
'@testing-library/jest-dom': ^5.16.5
@@ -272,6 +273,7 @@ importers:
'@remix-run/server-runtime': 1.16.1
'@sentry/remix': 7.53.1_535ee7qxxe65mhmlpnplklpcuu
'@tanstack/react-table': 8.7.6_biqbaboplfbrettd7655fr4n2y
'@team-plain/typescript-sdk': 2.2.0
'@trigger.dev/companyicons': 1.5.6_biqbaboplfbrettd7655fr4n2y
'@trigger.dev/database': link:../../packages/database
'@trigger.dev/internal': link:../../packages/internal
@@ -5613,6 +5615,14 @@ packages:
resolution: {integrity: sha512-jjcWBokl9eb1gVJ85QmoaQ73CQ52xAaOCF29ukRbYNl6lY+ts0ErTaDYOBlejcbUs2OpaiqYLO5uDhyLFzWw4w==}
dev: false
/@graphql-typed-document-node/core/3.2.0_graphql@16.6.0:
resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
dependencies:
graphql: 16.6.0
dev: false
/@hapi/boom/10.0.1:
resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==}
dependencies:
@@ -9593,6 +9603,17 @@ packages:
engines: {node: '>=12'}
dev: false
/@team-plain/typescript-sdk/2.2.0:
resolution: {integrity: sha512-ZNRZJ1uhQYHqaAKNAMdcSvULxutDQldocJWEDxJ5CJX4QYyzD8adK7gB9szKQDsxHFA3sZgTjtSruFBOf+C2fw==}
dependencies:
'@graphql-typed-document-node/core': 3.2.0_graphql@16.6.0
axios: 1.4.0
graphql: 16.6.0
zod: 3.21.4
transitivePeerDependencies:
- debug
dev: false
/@testing-library/cypress/8.0.7_cypress@10.11.0:
resolution: {integrity: sha512-3HTV725rOS+YHve/gD9coZp/UcPK5xhr4H0GMnq/ni6USdtzVtSOG9WBFtd8rYnrXk8rrGD+0toRFYouJNIG0Q==}
engines: {node: '>=12', npm: '>=6'}
@@ -11384,6 +11405,16 @@ packages:
- debug
dev: false
/axios/1.4.0:
resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==}
dependencies:
follow-redirects: 1.15.2
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/axobject-query/3.1.1:
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
dependencies:
@@ -15706,7 +15737,6 @@ packages:
/graphql/16.6.0:
resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
dev: true
/gunzip-maybe/1.4.2:
resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==}
@@ -24254,6 +24284,10 @@ packages:
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}
dev: false
/zod/3.21.4:
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
dev: false
/zwitch/2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true