@@ -17,10 +17,14 @@ import { Button } from "@comp/ui/button"
1717import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@comp/ui/select"
1818import { updateTrustPortalFrameworks } from "../actions/update-trust-portal-frameworks"
1919import { SOC2 , ISO27001 , GDPR } from "./logos"
20+ import { isFriendlyAvailable } from "../actions/is-friendly-available"
21+ import { useDebounce } from "@/hooks/useDebounce"
22+ import { useState , useEffect , useRef , useCallback } from "react"
2023
2124const trustPortalSwitchSchema = z . object ( {
2225 enabled : z . boolean ( ) ,
2326 contactEmail : z . string ( ) . email ( ) . or ( z . literal ( "" ) ) . optional ( ) ,
27+ friendlyUrl : z . string ( ) . optional ( ) ,
2428 soc2 : z . boolean ( ) ,
2529 iso27001 : z . boolean ( ) ,
2630 gdpr : z . boolean ( ) ,
@@ -42,6 +46,7 @@ export function TrustPortalSwitch({
4246 soc2Status,
4347 iso27001Status,
4448 gdprStatus,
49+ friendlyUrl,
4550} : {
4651 enabled : boolean
4752 slug : string
@@ -55,6 +60,7 @@ export function TrustPortalSwitch({
5560 soc2Status : "started" | "in_progress" | "compliant"
5661 iso27001Status : "started" | "in_progress" | "compliant"
5762 gdprStatus : "started" | "in_progress" | "compliant"
63+ friendlyUrl : string | null
5864} ) {
5965 const t = useI18n ( )
6066
@@ -67,6 +73,8 @@ export function TrustPortalSwitch({
6773 } ,
6874 } )
6975
76+ const checkFriendlyUrl = useAction ( isFriendlyAvailable )
77+
7078 const form = useForm < z . infer < typeof trustPortalSwitchSchema > > ( {
7179 resolver : zodResolver ( trustPortalSwitchSchema ) ,
7280 defaultValues : {
@@ -78,18 +86,113 @@ export function TrustPortalSwitch({
7886 soc2Status : soc2Status ?? "started" ,
7987 iso27001Status : iso27001Status ?? "started" ,
8088 gdprStatus : gdprStatus ?? "started" ,
89+ friendlyUrl : friendlyUrl ?? undefined ,
8190 } ,
8291 } )
8392
8493 const onSubmit = async ( data : z . infer < typeof trustPortalSwitchSchema > ) => {
8594 await trustPortalSwitch . execute ( data )
8695 }
8796
88- const portalUrl = domainVerified ? `https://${ domain } ` : `https://trust.trycomp.ai/${ slug } `
97+ const portalUrl = domainVerified ? `https://${ domain } ` : `https://trust.inc/${ slug } `
98+
99+ // --- Auto-save helpers ---
100+ const lastSaved = useRef < { [ key : string ] : string | boolean } > ( {
101+ contactEmail : contactEmail ?? "" ,
102+ friendlyUrl : friendlyUrl ?? "" ,
103+ enabled : enabled ,
104+ } )
105+
106+ // Save handler
107+ const autoSave = useCallback (
108+ async ( field : string , value : any ) => {
109+ const current = form . getValues ( )
110+ if ( lastSaved . current [ field ] !== value ) {
111+ const data = { ...current , [ field ] : value }
112+ await onSubmit ( data )
113+ lastSaved . current [ field ] = value
114+ }
115+ } ,
116+ [ form , onSubmit ]
117+ )
118+
119+ // --- Field Handlers ---
120+ // Contact Email
121+ const [ contactEmailValue , setContactEmailValue ] = useState ( form . getValues ( "contactEmail" ) || "" )
122+ const debouncedContactEmail = useDebounce ( contactEmailValue , 500 )
123+ // Debounced auto-save
124+ useEffect ( ( ) => {
125+ if (
126+ debouncedContactEmail !== undefined &&
127+ debouncedContactEmail !== lastSaved . current . contactEmail
128+ ) {
129+ form . setValue ( "contactEmail" , debouncedContactEmail )
130+ autoSave ( "contactEmail" , debouncedContactEmail )
131+ }
132+ // eslint-disable-next-line react-hooks/exhaustive-deps
133+ } , [ debouncedContactEmail ] )
134+ // On blur immediate save
135+ const handleContactEmailBlur = useCallback (
136+ ( e : React . FocusEvent < HTMLInputElement > ) => {
137+ const value = e . target . value
138+ form . setValue ( "contactEmail" , value )
139+ autoSave ( "contactEmail" , value )
140+ } ,
141+ [ form , autoSave ]
142+ )
143+
144+ // Friendly URL
145+ const [ friendlyUrlValue , setFriendlyUrlValue ] = useState ( form . getValues ( "friendlyUrl" ) || "" )
146+ const debouncedFriendlyUrl = useDebounce ( friendlyUrlValue , 500 )
147+ const [ friendlyUrlStatus , setFriendlyUrlStatus ] = useState < "idle" | "checking" | "available" | "unavailable" > ( "idle" )
148+ // Check availability on debounce
149+ useEffect ( ( ) => {
150+ if ( ! debouncedFriendlyUrl || debouncedFriendlyUrl === ( friendlyUrl ?? "" ) ) {
151+ setFriendlyUrlStatus ( "idle" )
152+ return
153+ }
154+ setFriendlyUrlStatus ( "checking" )
155+ checkFriendlyUrl . execute ( { friendlyUrl : debouncedFriendlyUrl , orgId } )
156+ // eslint-disable-next-line react-hooks/exhaustive-deps
157+ } , [ debouncedFriendlyUrl , orgId , friendlyUrl ] )
158+ useEffect ( ( ) => {
159+ if ( checkFriendlyUrl . status === "executing" ) return
160+ if ( checkFriendlyUrl . result ?. data ?. isAvailable === true ) {
161+ setFriendlyUrlStatus ( "available" )
162+ // Auto-save if available and changed
163+ if ( debouncedFriendlyUrl !== lastSaved . current . friendlyUrl ) {
164+ form . setValue ( "friendlyUrl" , debouncedFriendlyUrl )
165+ autoSave ( "friendlyUrl" , debouncedFriendlyUrl )
166+ }
167+ } else if ( checkFriendlyUrl . result ?. data ?. isAvailable === false ) {
168+ setFriendlyUrlStatus ( "unavailable" )
169+ }
170+ // eslint-disable-next-line react-hooks/exhaustive-deps
171+ } , [ checkFriendlyUrl . status , checkFriendlyUrl . result ] )
172+ // On blur immediate save if available
173+ const handleFriendlyUrlBlur = useCallback (
174+ ( e : React . FocusEvent < HTMLInputElement > ) => {
175+ const value = e . target . value
176+ if ( friendlyUrlStatus === "available" && value !== lastSaved . current . friendlyUrl ) {
177+ form . setValue ( "friendlyUrl" , value )
178+ autoSave ( "friendlyUrl" , value )
179+ }
180+ } ,
181+ [ form , autoSave , friendlyUrlStatus ]
182+ )
183+
184+ // Enabled switch immediate save
185+ const handleEnabledChange = useCallback (
186+ ( val : boolean ) => {
187+ form . setValue ( "enabled" , val )
188+ autoSave ( "enabled" , val )
189+ } ,
190+ [ form , autoSave ]
191+ )
89192
90193 return (
91194 < Form { ...form } >
92- < form onSubmit = { form . handleSubmit ( onSubmit ) } className = "space-y-4" >
195+ < form className = "space-y-4" >
93196 < Card className = "overflow-hidden" >
94197 < CardHeader className = "pb-4" >
95198 < div className = "flex items-center justify-between" >
@@ -114,7 +217,7 @@ export function TrustPortalSwitch({
114217 < FormControl >
115218 < Switch
116219 checked = { field . value }
117- onCheckedChange = { field . onChange }
220+ onCheckedChange = { handleEnabledChange }
118221 disabled = { trustPortalSwitch . status === "executing" }
119222 />
120223 </ FormControl >
@@ -126,19 +229,64 @@ export function TrustPortalSwitch({
126229 < CardContent className = "space-y-6 pt-0" >
127230 { form . watch ( "enabled" ) && (
128231 < div className = "pt-2" >
129- < h3 className = "text-sm font-medium mb-4" > Information Requests</ h3 >
130- < div className = "rounded-md border p-4" >
232+ < h3 className = "text-sm font-medium mb-4" > Trust Portal Settings</ h3 >
233+ < div className = "grid grid-cols-1 lg:grid-cols-2 gap-x-6 border rounded-md p-4" >
234+ < FormField
235+ control = { form . control }
236+ name = "friendlyUrl"
237+ render = { ( { field } ) => (
238+ < FormItem className = "w-full" >
239+ < FormLabel >
240+ Custom URL
241+ </ FormLabel >
242+ < FormControl >
243+ < div >
244+ < div className = "relative flex items-center w-full" >
245+ < Input
246+ { ...field }
247+ value = { friendlyUrlValue }
248+ onChange = { e => {
249+ field . onChange ( e )
250+ setFriendlyUrlValue ( e . target . value )
251+ } }
252+ onBlur = { handleFriendlyUrlBlur }
253+ placeholder = "my-org"
254+ autoComplete = "off"
255+ autoCapitalize = "none"
256+ autoCorrect = "off"
257+ spellCheck = "false"
258+ prefix = "trust.inc/"
259+ />
260+ </ div >
261+ { friendlyUrlValue && (
262+ < div className = "text-xs mt-1 min-h-[18px]" >
263+ { friendlyUrlStatus === "checking" && t ( "settings.trust_portal.friendly_url.checking" ) }
264+ { friendlyUrlStatus === "available" && < span className = "text-green-600" > { t ( "settings.trust_portal.friendly_url.available" ) } </ span > }
265+ { friendlyUrlStatus === "unavailable" && < span className = "text-red-600" > { t ( "settings.trust_portal.friendly_url.unavailable" ) } </ span > }
266+ </ div >
267+ ) }
268+ </ div >
269+ </ FormControl >
270+ </ FormItem >
271+ ) }
272+ />
131273 < FormField
132274 control = { form . control }
133275 name = "contactEmail"
134276 render = { ( { field } ) => (
135- < FormItem className = "flex items-center justify-between " >
277+ < FormItem className = "w-full " >
136278 < FormLabel >
137279 Contact Email
138280 </ FormLabel >
139281 < FormControl >
140282 < Input
141283 { ...field }
284+ value = { contactEmailValue }
285+ onChange = { e => {
286+ field . onChange ( e )
287+ setContactEmailValue ( e . target . value )
288+ } }
289+ onBlur = { handleContactEmailBlur }
142290143291 className = "w-auto"
144292 autoComplete = "off"
@@ -259,10 +407,6 @@ export function TrustPortalSwitch({
259407 ) : (
260408 < p className = "text-xs text-muted-foreground" > Trust portal is currently disabled.</ p >
261409 ) }
262- < Button type = "submit" disabled = { trustPortalSwitch . status === "executing" } >
263- { trustPortalSwitch . status === "executing" ? < Loader2 className = "h-4 w-4 animate-spin mr-1" /> : null }
264- { t ( "common.actions.save" ) }
265- </ Button >
266410 </ CardFooter >
267411 </ Card >
268412 </ form >
0 commit comments