11import { conform , useForm } from "@conform-to/react" ;
22import { parse } from "@conform-to/zod" ;
3- import { BuildingOffice2Icon } from "@heroicons/react/20/solid" ;
3+ import { BuildingOffice2Icon , GlobeAltIcon } from "@heroicons/react/20/solid" ;
44import { RadioGroup } from "@radix-ui/react-radio-group" ;
55import { json , redirect , type ActionFunction , type LoaderFunctionArgs } from "@remix-run/node" ;
66import { Form , useActionData , useNavigation } from "@remix-run/react" ;
7+ import { useCallback , useEffect , useRef , useState } from "react" ;
78import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
89import { z } from "zod" ;
910import { BackgroundWrapper } from "~/components/BackgroundWrapper" ;
@@ -27,8 +28,45 @@ import { organizationPath, rootPath } from "~/utils/pathBuilder";
2728const 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+
3270export 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