Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
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
144 changes: 109 additions & 35 deletions apps/webapp/app/routes/_app.orgs.new/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ 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 { requireUser, requireUserId } from "~/services/session.server";
import { organizationPath, rootPath } from "~/utils/pathBuilder";
import { PlainClient, uiComponent } from "@team-plain/typescript-sdk";
import { env } from "~/env.server";

const schema = z.object({
orgName: z.string().min(3).max(50),
companySize: z.string().optional(),
whyUseUs: z.string().optional(),
});

export const loader = async ({ request }: LoaderFunctionArgs) => {
Expand All @@ -40,7 +44,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {

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

Choose a reason for hiding this comment

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

You don't need this if you have the requireUser


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

Expand All @@ -55,6 +59,67 @@ export const action: ActionFunction = async ({ request }) => {
companySize: submission.value.companySize ?? null,
});

if (env.PLAIN_API_KEY) {
const client = new PlainClient({
Copy link
Member

Choose a reason for hiding this comment

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

Create a new plain.server.ts file, maybe in the webapp utilities folder.

It would be:

export async function sendToPlain({ userId, email, orgName, title, components}: Input) {
  const client = new ......
}

Then in this particular case you would wrap this in try/catch so it doesn't stop people creating orgs if Plain is down.

apiKey: env.PLAIN_API_KEY,
});

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

const upsertCustomerRes = await client.upsertCustomer({
identifier: {
emailAddress: user.email,
},
onCreate: {
externalId: userId,
fullName: submission.value.orgName,
email: {
email: user.email,
isVerified: true,
},
},
onUpdate: {
externalId: { value: userId },
fullName: { value: submission.value.orgName },
email: {
email: user.email,
isVerified: true,
},
},
});

if (upsertCustomerRes.error) {
console.error("Failed to upsert customer in Plain", upsertCustomerRes.error);
} else if (whyUseUs) {
const createThreadRes = await client.createThread({
customerIdentifier: {
customerId: upsertCustomerRes.data.customer.id,
},
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(),
}),
],
});

if (createThreadRes.error) {
console.error("Failed to create thread in Plain", createThreadRes.error);
}
}
}

return redirect(organizationPath(organization));
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
Expand Down Expand Up @@ -97,39 +162,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>
<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>
</>
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
17 changes: 17 additions & 0 deletions apps/webapp/app/routes/storybook.textarea/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Input } from "~/components/primitives/Input";
import { TextArea } from "~/components/primitives/TextArea";

export default function Story() {
return (
<div className="flex gap-16">
<div>
<div className="m-8 flex w-64 flex-col gap-4">
<TextArea placeholder="6 rows (default)" autoFocus />
<Input placeholder="Input" />
<TextArea placeholder="3 rows" rows={3} />
<TextArea disabled placeholder="Disabled" />
</div>
</div>
</div>
);
}
12 changes: 8 additions & 4 deletions apps/webapp/app/routes/storybook/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,20 @@ const stories: Story[] = [
slug: "date-fields",
},
{
name: "Simple form",
slug: "simple-form",
name: "Input fields",
slug: "input-fields",
},
{
name: "Search fields",
slug: "search-fields",
},
{
name: "Input fields",
slug: "input-fields",
name: "Simple form",
slug: "simple-form",
},
{
name: "Textarea",
slug: "textarea",
},
{
sectionTitle: "Menus",
Expand Down