11'use client'
22
3- import { useCallback , useEffect , useState } from 'react'
3+ import { useEffect , useState } from 'react'
44import { createLogger } from '@sim/logger'
55import { toError } from '@sim/utils/errors'
66import { Loader2 , X } from 'lucide-react'
77import Image from 'next/image'
88import { useParams } from 'next/navigation'
9- import { Button , Input , Label , toast } from '@/components/emcn'
9+ import { Button , Input , Label , Skeleton , toast } from '@/components/emcn'
1010import { useSession } from '@/lib/auth/auth-client'
1111import { getSubscriptionAccessState } from '@/lib/billing/client/utils'
1212import { HEX_COLOR_REGEX } from '@/lib/branding'
1313import { isBillingEnabled } from '@/lib/core/config/feature-flags'
1414import { cn } from '@/lib/core/utils/cn'
1515import { getUserRole } from '@/lib/workspaces/organization/utils'
1616import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload'
17+ import { SettingRow } from '@/ee/components/setting-row'
1718import {
1819 useUpdateWhitelabelSettings ,
1920 useWhitelabelSettings ,
@@ -33,33 +34,24 @@ interface DropZoneProps {
3334function DropZone ( { onDrop, children, className } : DropZoneProps ) {
3435 const [ isDragging , setIsDragging ] = useState ( false )
3536
36- const handleDragOver = useCallback ( ( e : React . DragEvent ) => {
37- if ( e . dataTransfer . types . includes ( 'Files' ) ) {
38- e . preventDefault ( )
39- setIsDragging ( true )
40- }
41- } , [ ] )
42-
43- const handleDragLeave = useCallback ( ( e : React . DragEvent ) => {
44- if ( ! e . currentTarget . contains ( e . relatedTarget as Node ) ) {
45- setIsDragging ( false )
46- }
47- } , [ ] )
48-
49- const handleDrop = useCallback (
50- ( e : React . DragEvent ) => {
51- setIsDragging ( false )
52- onDrop ( e )
53- } ,
54- [ onDrop ]
55- )
56-
5737 return (
5838 < div
5939 className = { cn ( 'relative' , className ) }
60- onDragOver = { handleDragOver }
61- onDragLeave = { handleDragLeave }
62- onDrop = { handleDrop }
40+ onDragOver = { ( e ) => {
41+ if ( e . dataTransfer . types . includes ( 'Files' ) ) {
42+ e . preventDefault ( )
43+ setIsDragging ( true )
44+ }
45+ } }
46+ onDragLeave = { ( e ) => {
47+ if ( ! e . currentTarget . contains ( e . relatedTarget as Node ) ) {
48+ setIsDragging ( false )
49+ }
50+ } }
51+ onDrop = { ( e ) => {
52+ setIsDragging ( false )
53+ onDrop ( e )
54+ } }
6355 >
6456 { children }
6557 { isDragging && (
@@ -81,22 +73,6 @@ interface ColorInputProps {
8173function ColorInput ( { label, value, onChange, placeholder = '#000000' } : ColorInputProps ) {
8274 const isValidHex = ! value || HEX_COLOR_REGEX . test ( value )
8375
84- const handleChange = useCallback (
85- ( e : React . ChangeEvent < HTMLInputElement > ) => {
86- let v = e . target . value . trim ( )
87- if ( v && ! v . startsWith ( '#' ) ) {
88- v = `#${ v } `
89- }
90- v = v . slice ( 0 , 1 ) + v . slice ( 1 ) . replace ( / [ ^ 0 - 9 a - f A - F ] / g, '' )
91- onChange ( v . slice ( 0 , 7 ) )
92- } ,
93- [ onChange ]
94- )
95-
96- const handleFocus = useCallback ( ( e : React . FocusEvent < HTMLInputElement > ) => {
97- e . target . select ( )
98- } , [ ] )
99-
10076 return (
10177 < div className = 'flex flex-col gap-1.5' >
10278 < Label className = 'text-[13px] text-[var(--text-primary)]' > { label } </ Label >
@@ -110,39 +86,32 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn
11086 </ div >
11187 < Input
11288 value = { value }
113- onChange = { handleChange }
114- onFocus = { handleFocus }
89+ onChange = { ( e ) => {
90+ let v = e . target . value . trim ( )
91+ if ( v && ! v . startsWith ( '#' ) ) {
92+ v = `#${ v } `
93+ }
94+ v = v . slice ( 0 , 1 ) + v . slice ( 1 ) . replace ( / [ ^ 0 - 9 a - f A - F ] / g, '' )
95+ onChange ( v . slice ( 0 , 7 ) )
96+ } }
97+ onFocus = { ( e ) => e . target . select ( ) }
11598 placeholder = { placeholder }
11699 className = { cn (
117100 'h-[36px] font-mono text-[13px]' ,
118- ! isValidHex && 'border-red-500 focus-visible:ring-red-500 '
101+ ! isValidHex && 'border-[var(--text-error)] focus-visible:ring-[var(--text-error)] '
119102 ) }
120103 maxLength = { 7 }
121104 />
122105 </ div >
123106 { ! isValidHex && (
124- < p className = 'text-[12px] text-red-500' > Must be a valid hex color (e.g. #701ffc)</ p >
107+ < p className = 'text-[12px] text-[var(--text-error)]' >
108+ Must be a valid hex color (e.g. #701ffc)
109+ </ p >
125110 ) }
126111 </ div >
127112 )
128113}
129114
130- interface SettingRowProps {
131- label : string
132- description ?: string
133- children : React . ReactNode
134- }
135-
136- function SettingRow ( { label, description, children } : SettingRowProps ) {
137- return (
138- < div className = 'flex flex-col gap-1.5' >
139- < Label className = 'text-[13px] text-[var(--text-primary)]' > { label } </ Label >
140- { description && < p className = 'text-[12px] text-[var(--text-muted)]' > { description } </ p > }
141- { children }
142- </ div >
143- )
144- }
145-
146115function SectionTitle ( { children } : { children : React . ReactNode } ) {
147116 return < h3 className = 'mb-4 font-medium text-[15px] text-[var(--text-primary)]' > { children } </ h3 >
148117}
@@ -177,20 +146,53 @@ export function WhitelabelingSettings() {
177146 const [ logoUrl , setLogoUrl ] = useState < string | null > ( null )
178147 const [ wordmarkUrl , setWordmarkUrl ] = useState < string | null > ( null )
179148 const [ formInitialized , setFormInitialized ] = useState ( false )
149+ const [ savedBrandName , setSavedBrandName ] = useState ( '' )
150+ const [ savedPrimaryColor , setSavedPrimaryColor ] = useState ( '' )
151+ const [ savedPrimaryHoverColor , setSavedPrimaryHoverColor ] = useState ( '' )
152+ const [ savedAccentColor , setSavedAccentColor ] = useState ( '' )
153+ const [ savedAccentHoverColor , setSavedAccentHoverColor ] = useState ( '' )
154+ const [ savedSupportEmail , setSavedSupportEmail ] = useState ( '' )
155+ const [ savedDocumentationUrl , setSavedDocumentationUrl ] = useState ( '' )
156+ const [ savedTermsUrl , setSavedTermsUrl ] = useState ( '' )
157+ const [ savedPrivacyUrl , setSavedPrivacyUrl ] = useState ( '' )
158+ const [ savedLogoUrl , setSavedLogoUrl ] = useState < string | null > ( null )
159+ const [ savedWordmarkUrl , setSavedWordmarkUrl ] = useState < string | null > ( null )
180160
181161 useEffect ( ( ) => {
182162 if ( ! savedSettings || formInitialized ) return
183- setBrandName ( savedSettings . brandName ?? '' )
184- setPrimaryColor ( savedSettings . primaryColor ?? '' )
185- setPrimaryHoverColor ( savedSettings . primaryHoverColor ?? '' )
186- setAccentColor ( savedSettings . accentColor ?? '' )
187- setAccentHoverColor ( savedSettings . accentHoverColor ?? '' )
188- setSupportEmail ( savedSettings . supportEmail ?? '' )
189- setDocumentationUrl ( savedSettings . documentationUrl ?? '' )
190- setTermsUrl ( savedSettings . termsUrl ?? '' )
191- setPrivacyUrl ( savedSettings . privacyUrl ?? '' )
192- setLogoUrl ( savedSettings . logoUrl ?? null )
193- setWordmarkUrl ( savedSettings . wordmarkUrl ?? null )
163+ const brand = savedSettings . brandName ?? ''
164+ const primary = savedSettings . primaryColor ?? ''
165+ const primaryHover = savedSettings . primaryHoverColor ?? ''
166+ const accent = savedSettings . accentColor ?? ''
167+ const accentHover = savedSettings . accentHoverColor ?? ''
168+ const support = savedSettings . supportEmail ?? ''
169+ const docs = savedSettings . documentationUrl ?? ''
170+ const terms = savedSettings . termsUrl ?? ''
171+ const privacy = savedSettings . privacyUrl ?? ''
172+ const logo = savedSettings . logoUrl ?? null
173+ const wordmark = savedSettings . wordmarkUrl ?? null
174+ setBrandName ( brand )
175+ setPrimaryColor ( primary )
176+ setPrimaryHoverColor ( primaryHover )
177+ setAccentColor ( accent )
178+ setAccentHoverColor ( accentHover )
179+ setSupportEmail ( support )
180+ setDocumentationUrl ( docs )
181+ setTermsUrl ( terms )
182+ setPrivacyUrl ( privacy )
183+ setLogoUrl ( logo )
184+ setWordmarkUrl ( wordmark )
185+ setSavedBrandName ( brand )
186+ setSavedPrimaryColor ( primary )
187+ setSavedPrimaryHoverColor ( primaryHover )
188+ setSavedAccentColor ( accent )
189+ setSavedAccentHoverColor ( accentHover )
190+ setSavedSupportEmail ( support )
191+ setSavedDocumentationUrl ( docs )
192+ setSavedTermsUrl ( terms )
193+ setSavedPrivacyUrl ( privacy )
194+ setSavedLogoUrl ( logo )
195+ setSavedWordmarkUrl ( wordmark )
194196 setFormInitialized ( true )
195197 } , [ savedSettings , formInitialized ] )
196198
@@ -212,18 +214,17 @@ export function WhitelabelingSettings() {
212214
213215 const hasChanges =
214216 formInitialized &&
215- ! ! savedSettings &&
216- ( brandName !== ( savedSettings . brandName ?? '' ) ||
217- primaryColor !== ( savedSettings . primaryColor ?? '' ) ||
218- primaryHoverColor !== ( savedSettings . primaryHoverColor ?? '' ) ||
219- accentColor !== ( savedSettings . accentColor ?? '' ) ||
220- accentHoverColor !== ( savedSettings . accentHoverColor ?? '' ) ||
221- supportEmail !== ( savedSettings . supportEmail ?? '' ) ||
222- documentationUrl !== ( savedSettings . documentationUrl ?? '' ) ||
223- termsUrl !== ( savedSettings . termsUrl ?? '' ) ||
224- privacyUrl !== ( savedSettings . privacyUrl ?? '' ) ||
225- ( logoUpload . previewUrl || null ) !== savedSettings . logoUrl ||
226- ( wordmarkUpload . previewUrl || null ) !== savedSettings . wordmarkUrl )
217+ ( brandName !== savedBrandName ||
218+ primaryColor !== savedPrimaryColor ||
219+ primaryHoverColor !== savedPrimaryHoverColor ||
220+ accentColor !== savedAccentColor ||
221+ accentHoverColor !== savedAccentHoverColor ||
222+ supportEmail !== savedSupportEmail ||
223+ documentationUrl !== savedDocumentationUrl ||
224+ termsUrl !== savedTermsUrl ||
225+ privacyUrl !== savedPrivacyUrl ||
226+ ( logoUpload . previewUrl || null ) !== savedLogoUrl ||
227+ ( wordmarkUpload . previewUrl || null ) !== savedWordmarkUrl )
227228
228229 async function handleSave ( ) {
229230 if ( ! orgId ) return
@@ -258,7 +259,17 @@ export function WhitelabelingSettings() {
258259
259260 try {
260261 await updateSettings . mutateAsync ( { orgId, settings } )
261- setFormInitialized ( false )
262+ setSavedBrandName ( brandName )
263+ setSavedPrimaryColor ( primaryColor )
264+ setSavedPrimaryHoverColor ( primaryHoverColor )
265+ setSavedAccentColor ( accentColor )
266+ setSavedAccentHoverColor ( accentHoverColor )
267+ setSavedSupportEmail ( supportEmail )
268+ setSavedDocumentationUrl ( documentationUrl )
269+ setSavedTermsUrl ( termsUrl )
270+ setSavedPrivacyUrl ( privacyUrl )
271+ setSavedLogoUrl ( logoUpload . previewUrl || null )
272+ setSavedWordmarkUrl ( wordmarkUpload . previewUrl || null )
262273 toast . success ( 'Whitelabeling settings saved.' )
263274 } catch ( error ) {
264275 logger . error ( 'Failed to save whitelabel settings' , { error } )
@@ -295,10 +306,10 @@ export function WhitelabelingSettings() {
295306 if ( isLoading ) {
296307 return (
297308 < div className = 'flex flex-col gap-8' >
298- { [ ... Array ( 3 ) ] . map ( ( _ , i ) => (
309+ { Array . from ( { length : 3 } ) . map ( ( _ , i ) => (
299310 < div key = { i } className = 'flex flex-col gap-3' >
300- < div className = 'h-4 w-32 animate-pulse rounded bg-[var(--surface-3) ]' />
301- < div className = 'h-9 w-full animate-pulse rounded-lg bg-[var(--surface-3)] ' />
311+ < Skeleton className = 'h-[16px] w-[128px ]' />
312+ < Skeleton className = 'h-[36px] w-full rounded-lg' />
302313 </ div >
303314 ) ) }
304315 </ div >
0 commit comments