Skip to content

Commit 795f741

Browse files
committed
Add a new field to capture org URL and show a favicon
1 parent 95ee2db commit 795f741

File tree

1 file changed

+135
-38
lines changed
  • apps/webapp/app/routes/_app.orgs.new

1 file changed

+135
-38
lines changed

apps/webapp/app/routes/_app.orgs.new/route.tsx

Lines changed: 135 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { conform, useForm } from "@conform-to/react";
22
import { parse } from "@conform-to/zod";
3-
import { BuildingOffice2Icon } from "@heroicons/react/20/solid";
3+
import { BuildingOffice2Icon, GlobeAltIcon } from "@heroicons/react/20/solid";
44
import { RadioGroup } from "@radix-ui/react-radio-group";
55
import { json, redirect, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/node";
66
import { Form, useActionData, useNavigation } from "@remix-run/react";
7+
import { useCallback, useEffect, useRef, useState } from "react";
78
import { typedjson, useTypedLoaderData } from "remix-typedjson";
89
import { z } from "zod";
910
import { BackgroundWrapper } from "~/components/BackgroundWrapper";
@@ -27,8 +28,45 @@ import { organizationPath, rootPath } from "~/utils/pathBuilder";
2728
const schema = z.object({
2829
orgName: z.string().min(3).max(50),
2930
companySize: z.string().optional(),
31+
companyUrl: z.string().optional(),
3032
});
3133

34+
function extractDomain(input: string): string | null {
35+
try {
36+
const withProtocol = input.includes("://") ? input : `https://${input}`;
37+
const url = new URL(withProtocol);
38+
return url.hostname;
39+
} catch {
40+
return null;
41+
}
42+
}
43+
44+
function useFaviconUrl(urlInput: string) {
45+
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
46+
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
47+
48+
const update = useCallback((value: string) => {
49+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
50+
timeoutRef.current = setTimeout(() => {
51+
const domain = extractDomain(value);
52+
if (domain && domain.includes(".")) {
53+
setFaviconUrl(`https://www.google.com/s2/favicons?domain=${domain}&sz=64`);
54+
} else {
55+
setFaviconUrl(null);
56+
}
57+
}, 400);
58+
}, []);
59+
60+
useEffect(() => {
61+
update(urlInput);
62+
return () => {
63+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
64+
};
65+
}, [urlInput, update]);
66+
67+
return faviconUrl;
68+
}
69+
3270
export const loader = async ({ request }: LoaderFunctionArgs) => {
3371
const userId = await requireUserId(request);
3472
const presenter = new NewOrganizationPresenter();
@@ -51,10 +89,31 @@ export const action: ActionFunction = async ({ request }) => {
5189
try {
5290
const companySize = submission.value.companySize ?? null;
5391

92+
const onboardingData: Record<string, string> = {};
93+
if (submission.value.companyUrl) {
94+
onboardingData.companyUrl = submission.value.companyUrl;
95+
}
96+
if (submission.value.companySize) {
97+
onboardingData.companySize = submission.value.companySize;
98+
}
99+
100+
let avatar: { type: "image"; url: string } | undefined;
101+
if (submission.value.companyUrl) {
102+
const domain = extractDomain(submission.value.companyUrl);
103+
if (domain) {
104+
avatar = {
105+
type: "image",
106+
url: `https://www.google.com/s2/favicons?domain=${domain}&sz=128`,
107+
};
108+
}
109+
}
110+
54111
const organization = await createOrganization({
55112
title: submission.value.orgName,
56113
userId: user.id,
57114
companySize,
115+
onboardingData: Object.keys(onboardingData).length > 0 ? onboardingData : undefined,
116+
avatar,
58117
});
59118

60119
const url = new URL(request.url);
@@ -87,6 +146,9 @@ export default function NewOrganizationPage() {
87146
const lastSubmission = useActionData();
88147
const { isManagedCloud } = useFeatures();
89148
const navigation = useNavigation();
149+
const [companyUrl, setCompanyUrl] = useState("");
150+
const faviconUrl = useFaviconUrl(companyUrl);
151+
const [faviconError, setFaviconError] = useState(false);
90152

91153
const [form, { orgName }] = useForm({
92154
id: "create-organization",
@@ -100,6 +162,21 @@ export default function NewOrganizationPage() {
100162

101163
const isLoading = navigation.state === "submitting" || navigation.state === "loading";
102164

165+
const urlIcon =
166+
faviconUrl && !faviconError ? (
167+
<img
168+
src={faviconUrl}
169+
alt=""
170+
width={16}
171+
height={16}
172+
className="ml-0.5 shrink-0 rounded-sm"
173+
onError={() => setFaviconError(true)}
174+
onLoad={() => setFaviconError(false)}
175+
/>
176+
) : (
177+
GlobeAltIcon
178+
);
179+
103180
return (
104181
<AppContainer className="bg-charcoal-900">
105182
<BackgroundWrapper>
@@ -111,53 +188,73 @@ export default function NewOrganizationPage() {
111188
<Form method="post" {...form.props}>
112189
<Fieldset>
113190
<InputGroup>
114-
<Label htmlFor={orgName.id}>Organization name</Label>
191+
<Label htmlFor={orgName.id}>
192+
Organization name <span className="text-text-dimmed">*</span>
193+
</Label>
115194
<Input
116195
{...conform.input(orgName, { type: "text" })}
117196
placeholder="Your Organization name"
118197
icon={BuildingOffice2Icon}
119198
autoFocus
120199
/>
121-
<Hint>E.g. your company name or your workspace name.</Hint>
200+
<Hint>Normally your company name.</Hint>
122201
<FormError id={orgName.errorId}>{orgName.error}</FormError>
123202
</InputGroup>
124203
{isManagedCloud && (
125-
<InputGroup>
126-
<Label htmlFor={"companySize"}>Number of employees</Label>
127-
<RadioGroup
128-
name="companySize"
129-
className="flex items-center justify-between gap-2"
130-
>
131-
<RadioGroupItem
132-
id="employees-1-5"
133-
label="1-5"
134-
value={"1-5"}
135-
variant="button/small"
136-
className="grow"
137-
/>
138-
<RadioGroupItem
139-
id="employees-6-49"
140-
label="6-49"
141-
value={"6-49"}
142-
variant="button/small"
143-
className="grow"
144-
/>
145-
<RadioGroupItem
146-
id="employees-50-99"
147-
label="50-99"
148-
value={"50-99"}
149-
variant="button/small"
150-
className="grow"
151-
/>
152-
<RadioGroupItem
153-
id="employees-100+"
154-
label="100+"
155-
value={"100+"}
156-
variant="button/small"
157-
className="grow"
204+
<>
205+
<InputGroup>
206+
<Label htmlFor="companyUrl">URL</Label>
207+
<Input
208+
id="companyUrl"
209+
name="companyUrl"
210+
type="url"
211+
placeholder="Your Organization URL"
212+
icon={urlIcon}
213+
value={companyUrl}
214+
onChange={(e) => {
215+
setCompanyUrl(e.target.value);
216+
setFaviconError(false);
217+
}}
158218
/>
159-
</RadioGroup>
160-
</InputGroup>
219+
<Hint>Add your company URL and we'll use it as your organization's logo.</Hint>
220+
</InputGroup>
221+
<InputGroup>
222+
<Label htmlFor="companySize">Number of employees</Label>
223+
<RadioGroup
224+
name="companySize"
225+
className="flex items-center justify-between gap-2"
226+
>
227+
<RadioGroupItem
228+
id="employees-1-5"
229+
label="1-5"
230+
value={"1-5"}
231+
variant="button/small"
232+
className="grow"
233+
/>
234+
<RadioGroupItem
235+
id="employees-6-49"
236+
label="6-49"
237+
value={"6-49"}
238+
variant="button/small"
239+
className="grow"
240+
/>
241+
<RadioGroupItem
242+
id="employees-50-99"
243+
label="50-99"
244+
value={"50-99"}
245+
variant="button/small"
246+
className="grow"
247+
/>
248+
<RadioGroupItem
249+
id="employees-100+"
250+
label="100+"
251+
value={"100+"}
252+
variant="button/small"
253+
className="grow"
254+
/>
255+
</RadioGroup>
256+
</InputGroup>
257+
</>
161258
)}
162259

163260
<FormButtons

0 commit comments

Comments
 (0)