@@ -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,41 @@ const showActivityOnPublicProfileInput = qsr<HTMLInputElement>(
4248 "#editProfileModal .editProfileShowActivityOnPublicProfile" ,
4349) ;
4450
51+ bioInput . on ( "input" , ( ) => {
52+ updateSaveButtonState ( ) ;
53+ } ) ;
54+ keyboardInput . on ( "input" , ( ) => {
55+ updateSaveButtonState ( ) ;
56+ } ) ;
57+ twitterInput . on ( "input" , ( ) => {
58+ updateSaveButtonState ( ) ;
59+ } ) ;
60+ githubInput . on ( "input" , ( ) => {
61+ updateSaveButtonState ( ) ;
62+ } ) ;
63+ websiteInput . on ( "input" , ( ) => {
64+ updateSaveButtonState ( ) ;
65+ } ) ;
66+ showActivityOnPublicProfileInput . on ( "change" , ( ) => {
67+ updateSaveButtonState ( ) ;
68+ } ) ;
69+
4570const indicators = [
46- addValidation ( twitterInput , TwitterProfileSchema ) ,
47- addValidation ( githubInput , GithubProfileSchema ) ,
48- addValidation ( websiteInput , WebsiteSchema ) ,
71+ addValidation (
72+ twitterInput ,
73+ TwitterProfileSchema ,
74+ ( ) => originalState ?. twitter ?? "" ,
75+ ) ,
76+ addValidation (
77+ githubInput ,
78+ GithubProfileSchema ,
79+ ( ) => originalState ?. github ?? "" ,
80+ ) ,
81+ addValidation (
82+ websiteInput ,
83+ WebsiteSchema ,
84+ ( ) => originalState ?. website ?? "" ,
85+ ) ,
4986] ;
5087
5188let currentSelectedBadgeId = - 1 ;
@@ -100,67 +137,104 @@ function hydrateInputs(): void {
100137
101138 badgeIdsSelect ?. qsa ( ".badgeSelectionItem" ) ?. removeClass ( "selected" ) ;
102139 ( currentTarget as HTMLElement ) . classList . add ( "selected" ) ;
140+ updateSaveButtonState ( ) ;
103141 } ) ;
104142
105143 indicators . forEach ( ( it ) => it . hide ( ) ) ;
106144}
107145
146+ let characterCountersInitialized = false ;
147+
108148function initializeCharacterCounters ( ) : void {
149+ if ( characterCountersInitialized ) return ;
109150 new CharacterCounter ( bioInput , 250 ) ;
110151 new CharacterCounter ( keyboardInput , 75 ) ;
152+ characterCountersInitialized = true ;
153+ }
154+
155+ type ProfileState = {
156+ bio : string ;
157+ keyboard : string ;
158+ twitter : string ;
159+ github : string ;
160+ website : string ;
161+ badgeId : number ;
162+ showActivityOnPublicProfile : boolean ;
163+ } ;
164+
165+ function getProfileState ( ) : ProfileState {
166+ return {
167+ bio : bioInput . getValue ( ) ?? "" ,
168+ keyboard : keyboardInput . getValue ( ) ?? "" ,
169+ twitter : twitterInput . getValue ( ) ?? "" ,
170+ github : githubInput . getValue ( ) ?? "" ,
171+ website : websiteInput . getValue ( ) ?? "" ,
172+ badgeId : currentSelectedBadgeId ,
173+ showActivityOnPublicProfile :
174+ showActivityOnPublicProfileInput . isChecked ( ) ?? false ,
175+ } ;
111176}
112177
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,
178+ function buildUpdatesFromState ( state : ProfileState ) : UserProfileDetails {
179+ return {
180+ bio : state . bio ,
181+ keyboard : state . keyboard ,
125182 socialProfiles : {
126- twitter,
127- github,
128- website,
183+ twitter : state . twitter ,
184+ github : state . github ,
185+ website : state . website ,
129186 } ,
130- showActivityOnPublicProfile,
187+ showActivityOnPublicProfile : state . showActivityOnPublicProfile ,
131188 } ;
189+ }
190+
191+ let originalState : ProfileState | null = null ;
192+
193+ function hasProfileChanged (
194+ originalState : ProfileState | null ,
195+ currentState : ProfileState ,
196+ ) : boolean {
197+ if ( originalState === null ) return true ;
198+
199+ return (
200+ originalState . bio !== currentState . bio ||
201+ originalState . keyboard !== currentState . keyboard ||
202+ originalState . twitter !== currentState . twitter ||
203+ originalState . github !== currentState . github ||
204+ originalState . website !== currentState . website ||
205+ originalState . badgeId !== currentState . badgeId ||
206+ originalState . showActivityOnPublicProfile !==
207+ currentState . showActivityOnPublicProfile
208+ ) ;
209+ }
210+
211+ function updateSaveButtonState ( ) : void {
212+ const currentState = getProfileState ( ) ;
213+ const hasChanges = hasProfileChanged ( originalState , currentState ) ;
132214
133- return profileUpdates ;
215+ const hasValidationErrors = [
216+ { value : currentState . twitter , schema : TwitterProfileSchema } ,
217+ { value : currentState . github , schema : GithubProfileSchema } ,
218+ { value : currentState . website , schema : WebsiteSchema } ,
219+ ] . some (
220+ ( { value, schema } ) => value !== "" && ! schema . safeParse ( value ) . success ,
221+ ) ;
222+
223+ saveButton . native . disabled = ! hasChanges || hasValidationErrors ;
134224}
135225
136226async function updateProfile ( ) : Promise < void > {
137227 const snapshot = DB . getSnapshot ( ) ;
138228 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- }
152229
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- ) ;
230+ const currentState = getProfileState ( ) ;
231+
232+ if ( ! hasProfileChanged ( originalState , currentState ) ) {
233+ updateSaveButtonState ( ) ;
161234 return ;
162235 }
163236
237+ const updates = buildUpdatesFromState ( currentState ) ;
164238 showLoaderBar ( ) ;
165239 const response = await Ape . users . updateProfile ( {
166240 body : {
@@ -185,7 +259,7 @@ async function updateProfile(): Promise<void> {
185259 } ) ;
186260
187261 DB . setSnapshot ( snapshot ) ;
188-
262+ originalState = currentState ;
189263 showSuccessNotification ( "Profile updated" ) ;
190264
191265 hide ( ) ;
@@ -194,6 +268,7 @@ async function updateProfile(): Promise<void> {
194268function addValidation (
195269 element : ElementWithUtils < HTMLInputElement > ,
196270 schema : Zod . Schema ,
271+ getOriginalValue : ( ) => string ,
197272) : InputIndicator {
198273 const indicator = new InputIndicator ( element , {
199274 valid : {
@@ -213,10 +288,11 @@ function addValidation(
213288
214289 element . on ( "input" , ( event ) => {
215290 const value = ( event . target as HTMLInputElement ) . value ;
216- if ( value === undefined || value === "" ) {
291+ if ( value === undefined || value === "" || value === getOriginalValue ( ) ) {
217292 indicator . hide ( ) ;
218293 return ;
219294 }
295+
220296 const validationResult = schema . safeParse ( value ) ;
221297 if ( ! validationResult . success ) {
222298 indicator . show (
0 commit comments