Skip to content
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 onboarding question #1404

Merged
merged 20 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/webapp/app/components/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { EnvelopeIcon, LightBulbIcon } from "@heroicons/react/24/solid";
import { InformationCircleIcon } from "@heroicons/react/20/solid";
import { EnvelopeIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { type ReactNode, useState, useEffect } from "react";
import { type ReactNode, useEffect, useState } from "react";
import { type FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { Button } from "./primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog";
Expand All @@ -16,7 +17,6 @@ import { Label } from "./primitives/Label";
import { Paragraph } from "./primitives/Paragraph";
import { Select, SelectItem } from "./primitives/Select";
import { TextArea } from "./primitives/TextArea";
import { InformationCircleIcon } from "@heroicons/react/20/solid";
import { TextLink } from "./primitives/TextLink";

type FeedbackProps = {
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/primitives/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function TextArea({ className, rows, ...props }: TextAreaProps) {
{...props}
rows={rows ?? 6}
className={cn(
"placeholder:text-muted-foreground w-full rounded-md border border-tertiary bg-tertiary px-3 text-sm text-text-bright transition focus-custom file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
"placeholder:text-muted-foreground w-full rounded border border-charcoal-800 bg-charcoal-750 px-3 text-sm text-text-bright transition focus-custom focus-custom file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 hover:bg-charcoal-650 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
/>
Expand Down
122 changes: 83 additions & 39 deletions apps/webapp/app/routes/_app.orgs.new/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RadioGroup } from "@radix-ui/react-radio-group";
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { uiComponent } from "@team-plain/typescript-sdk";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { MainCenteredContainer } from "~/components/layout/AppLayout";
Expand All @@ -17,30 +18,33 @@ import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { RadioGroupItem } from "~/components/primitives/RadioButton";
import { TextArea } from "~/components/primitives/TextArea";
import { useFeatures } from "~/hooks/useFeatures";
import { createOrganization } from "~/models/organization.server";
import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server";
import { requireUserId } from "~/services/session.server";
import { logger } from "~/services/logger.server";
import { requireUser } from "~/services/session.server";
import { organizationPath, rootPath } from "~/utils/pathBuilder";
import { sendToPlain } from "~/utils/plain.server";

const schema = z.object({
orgName: z.string().min(3).max(50),
companySize: z.string().optional(),
whyUseUs: z.string().optional(),
matt-aitken marked this conversation as resolved.
Show resolved Hide resolved
});

export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const user = await requireUser(request);
const presenter = new NewOrganizationPresenter();
const { hasOrganizations } = await presenter.call({ userId });
const { hasOrganizations } = await presenter.call({ userId: user.id });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one could have stayed as requireUserId. The requireUser call does a database call. It's not a big deal because this is such an infrequently viewed page though.


return typedjson({
hasOrganizations,
});
};

export const action: ActionFunction = async ({ request }) => {
const userId = await requireUserId(request);

const user = await requireUser(request);
const formData = await request.formData();
const submission = parse(formData, { schema });

Expand All @@ -51,10 +55,41 @@ export const action: ActionFunction = async ({ request }) => {
try {
const organization = await createOrganization({
title: submission.value.orgName,
userId,
userId: user.id,
companySize: submission.value.companySize ?? null,
});

const whyUseUs = formData.get("whyUseUs");

if (whyUseUs) {
try {
await sendToPlain({
userId: user.id,
email: user.email,
name: user.name ?? user.displayName ?? user.email,
title: "New org feedback",
components: [
uiComponent.text({
text: `${submission.value.orgName} just created a new organization.`,
}),
uiComponent.divider({ spacingSize: "M" }),
uiComponent.text({
size: "L",
color: "NORMAL",
text: "What problem are you trying to solve?",
}),
uiComponent.text({
size: "L",
color: "NORMAL",
text: whyUseUs.toString(),
}),
],
});
} catch (error) {
logger.error("Error sending data to Plain when creating an org:", { error });
}
}

return redirect(organizationPath(organization));
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
Expand Down Expand Up @@ -97,39 +132,48 @@ export default function NewOrganizationPage() {
<FormError id={orgName.errorId}>{orgName.error}</FormError>
</InputGroup>
{isManagedCloud && (
<InputGroup>
<Label htmlFor={"companySize"}>Number of employees</Label>
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
<RadioGroupItem
id="employees-1-5"
label="1-5"
value={"1-5"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-6-49"
label="6-49"
value={"6-49"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-50-99"
label="50-99"
value={"50-99"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-100+"
label="100+"
value={"100+"}
variant="button/small"
className="grow"
/>
</RadioGroup>
</InputGroup>
<>
<InputGroup>
<Label htmlFor={"companySize"}>Number of employees</Label>
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
<RadioGroupItem
id="employees-1-5"
label="1-5"
value={"1-5"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-6-49"
label="6-49"
value={"6-49"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-50-99"
label="50-99"
value={"50-99"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-100+"
label="100+"
value={"100+"}
variant="button/small"
className="grow"
/>
</RadioGroup>
</InputGroup>
matt-aitken marked this conversation as resolved.
Show resolved Hide resolved
matt-aitken marked this conversation as resolved.
Show resolved Hide resolved
<InputGroup>
<Label htmlFor={"whyUseUs"}>What problem are you trying to solve?</Label>
<TextArea name="whyUseUs" rows={4} spellCheck={false} />
<Hint>
Your answer will help us understand your use case and provide better support.
</Hint>
</InputGroup>
matt-aitken marked this conversation as resolved.
Show resolved Hide resolved
</>
Comment on lines +135 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM with suggestion: Form updates for new feedback field

The changes to the form look good:

  • The new TextArea component for the whyUseUs field is correctly implemented.
  • The layout structure using a fragment to group the companySize and whyUseUs inputs is appropriate.
  • The hint text provides clear guidance to the user.

However, there's one improvement that can be made:

Add error handling for the whyUseUs field to be consistent with other form fields. Apply this change:

  <InputGroup>
    <Label htmlFor={"whyUseUs"}>What problem are you trying to solve?</Label>
-   <TextArea name="whyUseUs" rows={4} spellCheck={false} />
+   <TextArea {...conform.textarea(whyUseUs)} rows={4} spellCheck={false} />
    <Hint>
      Your answer will help us understand your use case and provide better support.
    </Hint>
+   <FormError id={whyUseUs.errorId}>{whyUseUs.error}</FormError>
  </InputGroup>

Also, update the form hook to include whyUseUs:

- const [form, { orgName }] = useForm({
+ const [form, { orgName, whyUseUs }] = useForm({
  // ... (rest of the code remains the same)
});

These changes will ensure that any validation errors for the whyUseUs field are properly displayed to the user, maintaining consistency with other form fields.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<>
<InputGroup>
<Label htmlFor={"companySize"}>Number of employees</Label>
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
<RadioGroupItem
id="employees-1-5"
label="1-5"
value={"1-5"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-6-49"
label="6-49"
value={"6-49"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-50-99"
label="50-99"
value={"50-99"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-100+"
label="100+"
value={"100+"}
variant="button/small"
className="grow"
/>
</RadioGroup>
</InputGroup>
<InputGroup>
<Label htmlFor={"whyUseUs"}>What problem are you trying to solve?</Label>
<TextArea name="whyUseUs" rows={4} spellCheck={false} />
<Hint>
Your answer will help us understand your use case and provide better support.
</Hint>
</InputGroup>
</>
<>
<InputGroup>
<Label htmlFor={"companySize"}>Number of employees</Label>
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
<RadioGroupItem
id="employees-1-5"
label="1-5"
value={"1-5"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-6-49"
label="6-49"
value={"6-49"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-50-99"
label="50-99"
value={"50-99"}
variant="button/small"
className="grow"
/>
<RadioGroupItem
id="employees-100+"
label="100+"
value={"100+"}
variant="button/small"
className="grow"
/>
</RadioGroup>
</InputGroup>
<InputGroup>
<Label htmlFor={"whyUseUs"}>What problem are you trying to solve?</Label>
<TextArea {...conform.textarea(whyUseUs)} rows={4} spellCheck={false} />
<Hint>
Your answer will help us understand your use case and provide better support.
</Hint>
<FormError id={whyUseUs.errorId}>{whyUseUs.error}</FormError>
</InputGroup>
</>

)}

<FormButtons
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/routes/confirm-basic-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export default function Page() {
<Label htmlFor={confirmEmail.id}>How did you hear about us?</Label>
<Input
{...conform.input(referralSource, { type: "text" })}
placeholder="Google, Twitter…?"
placeholder="Google, X (Twitter)…?"
icon="heart"
spellCheck={false}
/>
Expand Down
30 changes: 5 additions & 25 deletions apps/webapp/app/routes/resources.feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { z } from "zod";
import { env } from "~/env.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { requireUser } from "~/services/session.server";
import { sendToPlain } from "~/utils/plain.server";

let client: PlainClient | undefined;

Expand Down Expand Up @@ -63,8 +64,6 @@ export async function action({ request }: ActionFunctionArgs) {
onCreate: {
externalId: user.id,
fullName: user.name ?? "",
// TODO - Optional: set 'first name' on user
// shortName: ''
email: {
email: user.email,
isVerified: true,
Expand All @@ -73,8 +72,6 @@ export async function action({ request }: ActionFunctionArgs) {
onUpdate: {
externalId: { value: user.id },
fullName: { value: user.name ?? "" },
// TODO - see above
// shortName: { value: "" },
email: {
email: user.email,
isVerified: true,
Expand All @@ -95,10 +92,10 @@ export async function action({ request }: ActionFunctionArgs) {
}

const title = feedbackTypeLabel[submission.value.feedbackType as FeedbackType];
const createThreadRes = await client.createThread({
customerIdentifier: {
customerId: upsertCustomerRes.data.customer.id,
},
await sendToPlain({
userId: user.id,
email: user.email,
name: user.name ?? user.displayName ?? user.email,
title,
components: [
uiComponent.text({
Expand All @@ -123,25 +120,8 @@ export async function action({ request }: ActionFunctionArgs) {
text: submission.value.message,
}),
],
// TODO: Optional: set labels on threads here on creation
// labelTypeIds: [],

// TODO: Optional: set the priority (0 is urgent, 3 is low)
// priority: 0,
});

if (createThreadRes.error) {
console.error(
inspect(createThreadRes.error, {
showHidden: false,
depth: null,
colors: true,
})
);
submission.error.message = createThreadRes.error.message;
return json(submission);
}

return redirectWithSuccessMessage(
submission.value.path,
request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { ArrowDownCircleIcon } from "@heroicons/react/24/outline";
import { Form, useLocation, useNavigation } from "@remix-run/react";
import { ActionFunctionArgs } from "@remix-run/server-runtime";
import { PlainClient, uiComponent } from "@team-plain/typescript-sdk";
import { uiComponent } from "@team-plain/typescript-sdk";
import { GitHubLightIcon } from "@trigger.dev/companyicons";
import {
FreePlanDefinition,
Expand All @@ -19,7 +19,6 @@ import {
SubscriptionResult,
} from "@trigger.dev/platform/v3";
import React, { useEffect, useState } from "react";
import { inspect } from "util";
import { z } from "zod";
import { DefinitionTip } from "~/components/DefinitionTooltip";
import { Feedback } from "~/components/Feedback";
Expand All @@ -39,12 +38,12 @@ import { Spinner } from "~/components/primitives/Spinner";
import { TextArea } from "~/components/primitives/TextArea";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { redirectWithErrorMessage } from "~/models/message.server";
import { logger } from "~/services/logger.server";
import { setPlan } from "~/services/platform.v3.server";
import { requireUser } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { sendToPlain } from "~/utils/plain.server";

const Params = z.object({
organizationSlug: z.string(),
Expand Down Expand Up @@ -88,53 +87,11 @@ export async function action({ request, params }: ActionFunctionArgs) {
switch (form.type) {
case "free": {
try {
if (!env.PLAIN_API_KEY) {
throw new Error("PLAIN_API_KEY is not set");
}

const client = new PlainClient({
apiKey: env.PLAIN_API_KEY,
});

const upsertCustomerRes = await client.upsertCustomer({
identifier: {
emailAddress: user.email,
},
onCreate: {
externalId: user.id,
fullName: user.name ?? "",
email: {
email: user.email,
isVerified: true,
},
},
onUpdate: {
externalId: { value: user.id },
fullName: { value: user.name ?? "" },
email: {
email: user.email,
isVerified: true,
},
},
});

if (upsertCustomerRes.error) {
console.error(
inspect(upsertCustomerRes.error, {
showHidden: false,
depth: null,
colors: true,
})
);
throw redirectWithErrorMessage(form.callerPath, request, upsertCustomerRes.error.message);
}

// Only create a thread if there are reasons or a message
if (reasons.length > 0 || (message && message.toString().trim() !== "")) {
const createThreadRes = await client.createThread({
customerIdentifier: {
customerId: upsertCustomerRes.data.customer.id,
},
await sendToPlain({
userId: user.id,
email: user.email,
name: user.name ?? "",
title: "Plan cancelation feedback",
components: [
uiComponent.text({
Expand Down Expand Up @@ -169,17 +126,6 @@ export async function action({ request, params }: ActionFunctionArgs) {
: []),
],
});

if (createThreadRes.error) {
console.error(
inspect(createThreadRes.error, {
showHidden: false,
depth: null,
colors: true,
})
);
throw redirectWithErrorMessage(form.callerPath, request, createThreadRes.error.message);
}
}
} catch (e) {
logger.error("Failed to submit to Plain the unsubscribe reason", { error: e });
Expand Down Expand Up @@ -867,7 +813,7 @@ function TierLimit({ children, href }: { children: React.ReactNode; href?: strin
}
content={
<div className="flex items-center gap-1">
<Paragraph variant="small">View detailed compute pricing information</Paragraph>
<Paragraph variant="small">View compute pricing information</Paragraph>
<ArrowUpRightIcon className="size-4 text-text-dimmed" />
</div>
}
Expand Down
Loading