-
-
Notifications
You must be signed in to change notification settings - Fork 627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New Help & Feedback menu and tab-selected styles #1374
Changes from 21 commits
9e7b07d
f54ae15
7a3f948
a0f92db
af6185a
01ab899
596ceb8
e77805f
7e71d86
1ece140
67107d0
691ef3e
10070a4
2d53f6d
3a63585
cc41085
fa5a8f8
fab1e0d
b95fb8a
a066e04
d4ee186
dfce7d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,23 @@ | ||
import { conform, useForm } from "@conform-to/react"; | ||
import { parse } from "@conform-to/zod"; | ||
import { BookOpenIcon } from "@heroicons/react/20/solid"; | ||
import { | ||
CalendarDaysIcon, | ||
ChevronRightIcon, | ||
EnvelopeIcon, | ||
LifebuoyIcon, | ||
LightBulbIcon, | ||
} from "@heroicons/react/24/solid"; | ||
import { EnvelopeIcon, LightBulbIcon } from "@heroicons/react/24/solid"; | ||
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react"; | ||
import { DiscordIcon } from "@trigger.dev/companyicons"; | ||
import { ActivityIcon } from "lucide-react"; | ||
import { type ReactNode, useState } from "react"; | ||
import { type ReactNode, useState, useEffect } from "react"; | ||
import { type FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback"; | ||
import { cn } from "~/utils/cn"; | ||
import { docsTroubleshootingPath } from "~/utils/pathBuilder"; | ||
import { Button, LinkButton } from "./primitives/Buttons"; | ||
import { Button } from "./primitives/Buttons"; | ||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog"; | ||
import { Fieldset } from "./primitives/Fieldset"; | ||
import { FormButtons } from "./primitives/FormButtons"; | ||
import { FormError } from "./primitives/FormError"; | ||
import { Header1 } from "./primitives/Headers"; | ||
import { Icon } from "./primitives/Icon"; | ||
import { InfoPanel } from "./primitives/InfoPanel"; | ||
import { InputGroup } from "./primitives/InputGroup"; | ||
import { Label } from "./primitives/Label"; | ||
import { Paragraph } from "./primitives/Paragraph"; | ||
import { Select, SelectItem } from "./primitives/Select"; | ||
import { Sheet, SheetBody, SheetContent, SheetTrigger } from "./primitives/Sheet"; | ||
import { TextArea } from "./primitives/TextArea"; | ||
import { InformationCircleIcon } from "@heroicons/react/20/solid"; | ||
import { TextLink } from "./primitives/TextLink"; | ||
|
||
type FeedbackProps = { | ||
button: ReactNode; | ||
|
@@ -37,161 +29,111 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) { | |
const location = useLocation(); | ||
const lastSubmission = useActionData(); | ||
const navigation = useNavigation(); | ||
const [type, setType] = useState<FeedbackType>(defaultValue); | ||
|
||
const [form, { path, feedbackType, message }] = useForm({ | ||
id: "accept-invite", | ||
// TODO: type this | ||
lastSubmission: lastSubmission as any, | ||
onValidate({ formData }) { | ||
return parse(formData, { schema }); | ||
}, | ||
shouldRevalidate: "onInput", | ||
}); | ||
|
||
if ( | ||
open && | ||
navigation.formAction === "/resources/feedback" && | ||
form.error === undefined && | ||
form.errors.length === 0 | ||
) { | ||
setOpen(false); | ||
} | ||
useEffect(() => { | ||
if ( | ||
navigation.formAction === "/resources/feedback" && | ||
navigation.state === "loading" && | ||
form.error === undefined && | ||
form.errors.length === 0 | ||
) { | ||
setOpen(false); | ||
} | ||
}, [navigation, form]); | ||
|
||
return ( | ||
<Sheet open={open} onOpenChange={setOpen}> | ||
<SheetTrigger asChild={true}>{button}</SheetTrigger> | ||
<SheetContent className="@container"> | ||
<SheetBody className="flex h-full flex-col justify-between"> | ||
<LinkBanner | ||
title="Join our Discord community" | ||
icon={<DiscordIcon className="size-9" />} | ||
to="https://trigger.dev/discord" | ||
className="hover:border-text-link" | ||
> | ||
<Paragraph>The quickest way to get answers from the Trigger.dev community.</Paragraph> | ||
</LinkBanner> | ||
<LinkBanner | ||
title="Book a 15 min chat with the founders" | ||
icon={<CalendarDaysIcon className="size-9 text-green-500" />} | ||
to="https://cal.com/team/triggerdotdev/founders-call" | ||
className="hover:border-green-500" | ||
> | ||
<Paragraph>Have a question or want to chat? Book a time to talk with us.</Paragraph> | ||
</LinkBanner> | ||
<LinkBanner | ||
title="Suggest a feature" | ||
icon={<LightBulbIcon className="size-9 text-sun-500" />} | ||
to="https://feedback.trigger.dev/" | ||
className="hover:border-sun-400" | ||
> | ||
<Paragraph>Have an idea for a new feature or improvement? Let us know!</Paragraph> | ||
</LinkBanner> | ||
<LinkBanner | ||
title="Troubleshooting" | ||
icon={<LifebuoyIcon className="size-9 text-rose-500" />} | ||
> | ||
<Paragraph> | ||
If you're having trouble, check out our troubleshooting guide or the Trigger.dev | ||
Status page. | ||
<Dialog open={open} onOpenChange={setOpen}> | ||
<DialogTrigger asChild>{button}</DialogTrigger> | ||
<DialogContent> | ||
<DialogHeader>Contact us</DialogHeader> | ||
<div className="mt-2 flex flex-col gap-4"> | ||
<div className="flex items-center gap-4"> | ||
<Icon icon={EnvelopeIcon} className="size-10 min-w-[2.5rem] text-blue-500" /> | ||
<Paragraph variant="base/bright"> | ||
How can we help? We read every message and will respond as quickly as we can. | ||
</Paragraph> | ||
<div className="flex flex-wrap gap-2"> | ||
<LinkButton | ||
to={docsTroubleshootingPath("")} | ||
variant="tertiary/medium" | ||
LeadingIcon={BookOpenIcon} | ||
> | ||
Troubleshooting Docs | ||
</LinkButton> | ||
<LinkButton | ||
to={"https://status.trigger.dev/"} | ||
variant="tertiary/medium" | ||
LeadingIcon={ActivityIcon} | ||
> | ||
Trigger.dev Status | ||
</LinkButton> | ||
</div> | ||
</LinkBanner> | ||
<LinkBanner | ||
title="Send us an email" | ||
icon={<EnvelopeIcon className="size-9 text-blue-500" />} | ||
> | ||
<Paragraph>We read every message and respond quickly.</Paragraph> | ||
<Form method="post" action="/resources/feedback" {...form.props} className="w-full"> | ||
<Fieldset className="max-w-full gap-y-3"> | ||
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} /> | ||
<InputGroup className="max-w-full"> | ||
<Select | ||
{...conform.select(feedbackType)} | ||
variant="tertiary/medium" | ||
defaultValue={defaultValue} | ||
placeholder="Select type" | ||
text={(value) => feedbackTypeLabel[value]} | ||
dropdownIcon | ||
</div> | ||
<hr className="border-charcoal-800" /> | ||
<Form method="post" action="/resources/feedback" {...form.props} className="w-full"> | ||
<Fieldset className="max-w-full gap-y-3"> | ||
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} /> | ||
<InputGroup className="max-w-full"> | ||
{type === "feature" && ( | ||
<InfoPanel | ||
icon={InformationCircleIcon} | ||
iconClassName="text-blue-500" | ||
panelClassName="w-full mb-2" | ||
> | ||
{Object.entries(feedbackTypeLabel).map(([name, title]) => ( | ||
<SelectItem key={name} value={name}> | ||
{title} | ||
</SelectItem> | ||
))} | ||
</Select> | ||
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError> | ||
</InputGroup> | ||
<InputGroup className="max-w-full"> | ||
<Label>Message</Label> | ||
<TextArea {...conform.textarea(message)} /> | ||
<FormError id={message.errorId}>{message.error}</FormError> | ||
</InputGroup> | ||
<FormError>{form.error}</FormError> | ||
<div className="flex w-full justify-end"> | ||
<FormButtons | ||
className="m-0 w-max" | ||
confirmButton={ | ||
<Button type="submit" variant="tertiary/medium"> | ||
Send message | ||
</Button> | ||
} | ||
/> | ||
</div> | ||
</Fieldset> | ||
</Form> | ||
</LinkBanner> | ||
</SheetBody> | ||
</SheetContent> | ||
</Sheet> | ||
); | ||
} | ||
|
||
function LinkBanner({ | ||
className, | ||
icon, | ||
title, | ||
children, | ||
to, | ||
}: { | ||
className?: string; | ||
icon?: ReactNode; | ||
title?: string; | ||
children?: ReactNode; | ||
to?: string; | ||
}) { | ||
return ( | ||
<a | ||
href={to} | ||
target="_blank" | ||
className={cn( | ||
"group/banner mb-4 flex w-full items-center justify-between rounded-md border border-grid-bright bg-charcoal-750 p-4 transition", | ||
className | ||
)} | ||
> | ||
<div className="flex w-full items-start gap-4"> | ||
<span>{icon}</span> | ||
<div className="flex w-full flex-col gap-2"> | ||
<Header1 className="text-2xl font-semibold text-text-bright">{title}</Header1> | ||
{children} | ||
<Paragraph variant="small"> | ||
All our feature requests are public and voted on by the community. The best | ||
way to submit your feature request is to{" "} | ||
<TextLink to="https://feedback.trigger.dev"> | ||
post it to our feedback forum | ||
</TextLink> | ||
. | ||
</Paragraph> | ||
</InfoPanel> | ||
)} | ||
Comment on lines
+71
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Refactor InfoPanel rendering to avoid code duplication The conditional rendering blocks for Here's how you could refactor: const infoPanelContent = {
feature: {
message: (
<Paragraph variant="small">
All our feature requests are public and voted on by the community. The best way to submit your feature request is to{" "}
<TextLink to="https://feedback.trigger.dev">post it to our feedback forum</TextLink>.
</Paragraph>
),
},
help: {
message: (
<Paragraph variant="small">
The quickest way to get answers from the Trigger.dev team and community is to{" "}
<TextLink to="https://trigger.dev/discord">ask in our Discord</TextLink>.
</Paragraph>
),
},
};
{["feature", "help"].includes(type) && (
<InfoPanel
icon={InformationCircleIcon}
iconClassName="text-blue-500"
panelClassName="w-full mb-2"
>
{infoPanelContent[type].message}
</InfoPanel>
)} This approach makes it cleaner and easier to add more types in the future. Also applies to: 87-98 |
||
{type === "help" && ( | ||
<InfoPanel | ||
icon={InformationCircleIcon} | ||
iconClassName="text-blue-500" | ||
panelClassName="w-full mb-2" | ||
> | ||
<Paragraph variant="small"> | ||
The quickest way to get answers from the Trigger.dev team and community is to{" "} | ||
<TextLink to="https://trigger.dev/discord">ask in our Discord</TextLink>. | ||
</Paragraph> | ||
</InfoPanel> | ||
)} | ||
<Select | ||
{...conform.select(feedbackType)} | ||
variant="tertiary/medium" | ||
value={type} | ||
defaultValue={type} | ||
setValue={(v) => setType(v as FeedbackType)} | ||
placeholder="Select type" | ||
text={(value) => feedbackTypeLabel[value as FeedbackType]} | ||
dropdownIcon | ||
> | ||
{Object.entries(feedbackTypeLabel).map(([name, title]) => ( | ||
<SelectItem key={name} value={name}> | ||
{title} | ||
</SelectItem> | ||
))} | ||
</Select> | ||
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError> | ||
</InputGroup> | ||
<InputGroup className="max-w-full"> | ||
<Label>Message</Label> | ||
<TextArea {...conform.textarea(message)} /> | ||
<FormError id={message.errorId}>{message.error}</FormError> | ||
</InputGroup> | ||
<FormError>{form.error}</FormError> | ||
<div className="flex w-full justify-end"> | ||
<FormButtons | ||
className="m-0 w-max" | ||
confirmButton={ | ||
<Button type="submit" variant="tertiary/medium"> | ||
Send message | ||
</Button> | ||
} | ||
/> | ||
</div> | ||
</Fieldset> | ||
</Form> | ||
</div> | ||
</div> | ||
{to && ( | ||
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover/banner:text-text-bright" /> | ||
)} | ||
</a> | ||
</DialogContent> | ||
</Dialog> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure the
useEffect
dependencies are completeIn your
useEffect
hook, you're relying onnavigation.formAction
,navigation.state
,form.error
, andform.errors.length
, but onlynavigation
andform
are specified in the dependency array. To prevent potential stale closures or missed updates, consider including all specific dependencies used within the effect.Update the dependency array as follows:
📝 Committable suggestion
🛠️ Refactor suggestion
Consider improving dialog close logic after form submission
Currently, the
useEffect
hook closes the dialog when the form is successfully submitted by checking ifnavigation.state === "loading"
. This might close the dialog prematurely or not at the intended time. Consider refining the condition to ensure the dialog closes only after the submission is successfully completed.You could modify the
useEffect
hook as follows:This ensures that the dialog closes only when the navigation state is idle and there is a last submission, indicating the form was submitted successfully.