@@ -23,6 +23,8 @@ export function show(): void {
2323 void modal . show ( {
2424 beforeAnimation : async ( ) => {
2525 hydrateInputs ( ) ;
26+ originalState = getProfileState ( ) ;
27+ updateSaveButtonState ( ) ;
2628 initializeCharacterCounters ( ) ;
2729 } ,
2830 } ) ;
@@ -32,6 +34,10 @@ function hide(): void {
3234 void modal . hide ( ) ;
3335}
3436
37+ const saveButton = qsr < HTMLButtonElement > (
38+ "#editProfileModal .edit-profile-submit" ,
39+ ) ;
40+
3541const bioInput = qsr < HTMLTextAreaElement > ( "#editProfileModal .bio" ) ;
3642const keyboardInput = qsr < HTMLTextAreaElement > ( "#editProfileModal .keyboard" ) ;
3743const twitterInput = qsr < HTMLInputElement > ( "#editProfileModal .twitter" ) ;
@@ -42,10 +48,28 @@ const showActivityOnPublicProfileInput = qsr<HTMLInputElement>(
4248 "#editProfileModal .editProfileShowActivityOnPublicProfile" ,
4349) ;
4450
51+ const inputs = [
52+ bioInput ,
53+ keyboardInput ,
54+ twitterInput ,
55+ githubInput ,
56+ websiteInput ,
57+ ] ;
58+
59+ inputs . forEach ( ( input ) => {
60+ input . on ( "input" , updateSaveButtonState ) ;
61+ } ) ;
62+
63+ showActivityOnPublicProfileInput . on ( "change" , updateSaveButtonState ) ;
64+
4565const indicators = [
46- addValidation ( twitterInput , TwitterProfileSchema ) ,
47- addValidation ( githubInput , GithubProfileSchema ) ,
48- addValidation ( websiteInput , WebsiteSchema ) ,
66+ addValidation (
67+ twitterInput ,
68+ TwitterProfileSchema ,
69+ ( ) => originalState . twitter ,
70+ ) ,
71+ addValidation ( githubInput , GithubProfileSchema , ( ) => originalState . github ) ,
72+ addValidation ( websiteInput , WebsiteSchema , ( ) => originalState . website ) ,
4973] ;
5074
5175let currentSelectedBadgeId = - 1 ;
@@ -100,66 +124,96 @@ function hydrateInputs(): void {
100124
101125 badgeIdsSelect ?. qsa ( ".badgeSelectionItem" ) ?. removeClass ( "selected" ) ;
102126 ( currentTarget as HTMLElement ) . classList . add ( "selected" ) ;
127+ updateSaveButtonState ( ) ;
103128 } ) ;
104129
105130 indicators . forEach ( ( it ) => it . hide ( ) ) ;
106131}
107132
133+ let characterCountersInitialized = false ;
134+
108135function initializeCharacterCounters ( ) : void {
136+ if ( characterCountersInitialized ) return ;
109137 new CharacterCounter ( bioInput , 250 ) ;
110138 new CharacterCounter ( keyboardInput , 75 ) ;
139+ characterCountersInitialized = true ;
140+ }
141+
142+ type ProfileState = {
143+ bio : string ;
144+ keyboard : string ;
145+ twitter : string ;
146+ github : string ;
147+ website : string ;
148+ badgeId : number ;
149+ showActivityOnPublicProfile : boolean ;
150+ } ;
151+
152+ function getProfileState ( ) : ProfileState {
153+ return {
154+ bio : bioInput . getValue ( ) ?? "" ,
155+ keyboard : keyboardInput . getValue ( ) ?? "" ,
156+ twitter : twitterInput . getValue ( ) ?? "" ,
157+ github : githubInput . getValue ( ) ?? "" ,
158+ website : websiteInput . getValue ( ) ?? "" ,
159+ badgeId : currentSelectedBadgeId ,
160+ showActivityOnPublicProfile :
161+ showActivityOnPublicProfileInput . isChecked ( ) ?? false ,
162+ } ;
111163}
112164
113- function buildUpdatesFromInputs ( ) : UserProfileDetails {
114- const bio = bioInput . getValue ( ) ?? "" ;
115- const keyboard = keyboardInput . getValue ( ) ?? "" ;
116- const twitter = twitterInput . getValue ( ) ?? "" ;
117- const github = githubInput . getValue ( ) ?? "" ;
118- const website = websiteInput . getValue ( ) ?? "" ;
119- const showActivityOnPublicProfile =
120- showActivityOnPublicProfileInput . isChecked ( ) ?? false ;
121-
122- const profileUpdates : UserProfileDetails = {
123- bio,
124- keyboard,
165+ function buildUpdatesFromState ( state : ProfileState ) : UserProfileDetails {
166+ return {
167+ bio : state . bio ,
168+ keyboard : state . keyboard ,
125169 socialProfiles : {
126- twitter,
127- github,
128- website,
170+ twitter : state . twitter ,
171+ github : state . github ,
172+ website : state . website ,
129173 } ,
130- showActivityOnPublicProfile,
174+ showActivityOnPublicProfile : state . showActivityOnPublicProfile ,
131175 } ;
176+ }
132177
133- return profileUpdates ;
178+ let originalState : ProfileState ;
179+
180+ function hasProfileChanged (
181+ originalProfile : ProfileState ,
182+ currentProfile : ProfileState ,
183+ ) : boolean {
184+ return (
185+ originalProfile . bio !== currentProfile . bio ||
186+ originalProfile . keyboard !== currentProfile . keyboard ||
187+ originalProfile . twitter !== currentProfile . twitter ||
188+ originalProfile . github !== currentProfile . github ||
189+ originalProfile . website !== currentProfile . website ||
190+ originalProfile . badgeId !== currentProfile . badgeId ||
191+ originalProfile . showActivityOnPublicProfile !==
192+ currentProfile . showActivityOnPublicProfile
193+ ) ;
194+ }
195+
196+ function updateSaveButtonState ( ) : void {
197+ const currentState = getProfileState ( ) ;
198+ const hasChanges = hasProfileChanged ( originalState , currentState ) ;
199+
200+ const hasValidationErrors = [
201+ { value : currentState . twitter , schema : TwitterProfileSchema } ,
202+ { value : currentState . github , schema : GithubProfileSchema } ,
203+ { value : currentState . website , schema : WebsiteSchema } ,
204+ ] . some (
205+ ( { value, schema } ) => value !== "" && ! schema . safeParse ( value ) . success ,
206+ ) ;
207+
208+ saveButton . native . disabled = ! hasChanges || hasValidationErrors ;
134209}
135210
136211async function updateProfile ( ) : Promise < void > {
137212 const snapshot = DB . getSnapshot ( ) ;
138213 if ( ! snapshot ) return ;
139- const updates = buildUpdatesFromInputs ( ) ;
140-
141- // check for length resctrictions before sending server requests
142- const githubLengthLimit = 39 ;
143- if (
144- updates . socialProfiles ?. github !== undefined &&
145- updates . socialProfiles ?. github . length > githubLengthLimit
146- ) {
147- showErrorNotification (
148- `GitHub username exceeds maximum allowed length (${ githubLengthLimit } characters).` ,
149- ) ;
150- return ;
151- }
152214
153- const twitterLengthLimit = 20 ;
154- if (
155- updates . socialProfiles ?. twitter !== undefined &&
156- updates . socialProfiles ?. twitter . length > twitterLengthLimit
157- ) {
158- showErrorNotification (
159- `Twitter username exceeds maximum allowed length (${ twitterLengthLimit } characters).` ,
160- ) ;
161- return ;
162- }
215+ const currentState = getProfileState ( ) ;
216+ const updates = buildUpdatesFromState ( currentState ) ;
163217
164218 showLoaderBar ( ) ;
165219 const response = await Ape . users . updateProfile ( {
@@ -185,7 +239,7 @@ async function updateProfile(): Promise<void> {
185239 } ) ;
186240
187241 DB . setSnapshot ( snapshot ) ;
188-
242+ originalState = currentState ;
189243 showSuccessNotification ( "Profile updated" ) ;
190244
191245 hide ( ) ;
@@ -194,6 +248,7 @@ async function updateProfile(): Promise<void> {
194248function addValidation (
195249 element : ElementWithUtils < HTMLInputElement > ,
196250 schema : Zod . Schema ,
251+ getOriginalValue : ( ) => string ,
197252) : InputIndicator {
198253 const indicator = new InputIndicator ( element , {
199254 valid : {
@@ -213,7 +268,7 @@ function addValidation(
213268
214269 element . on ( "input" , ( event ) => {
215270 const value = ( event . target as HTMLInputElement ) . value ;
216- if ( value === undefined || value === "" ) {
271+ if ( value === undefined || value === "" || value === getOriginalValue ( ) ) {
217272 indicator . hide ( ) ;
218273 return ;
219274 }
0 commit comments