@@ -108,6 +108,79 @@ function formatStepNameForError(stepKey) {
108108 . trim ( ) ; // Remove any leading/trailing spaces
109109}
110110
111+ /**
112+ * Debounces a function call
113+ * @param {Function } func - The function to debounce
114+ * @param {number } delay - Delay in milliseconds (default: 800)
115+ * @returns {Function } Debounced function
116+ */
117+ function debounce ( func , delay = 800 ) {
118+ let timeout ;
119+ return ( ...args ) => {
120+ clearTimeout ( timeout ) ;
121+ timeout = setTimeout ( ( ) => func ( ...args ) , delay ) ;
122+ } ;
123+ }
124+
125+ /**
126+ * Validates a GitHub username by checking if it exists
127+ * @param {string } username - The GitHub username to validate
128+ * @returns {Promise<{valid: boolean|null, message: string}> } Validation result
129+ */
130+ async function validateGitHubId ( username ) {
131+ if ( ! username . trim ( ) ) {
132+ return { valid : true , message : '' } ;
133+ }
134+
135+ try {
136+ const response = await fetch ( `https://api.github.com/users/${ encodeURIComponent ( username ) } ` ) ;
137+
138+ if ( response . status === 200 ) {
139+ return { valid : true , message : 'GitHub user found' } ;
140+ }
141+ if ( response . status === 404 ) {
142+ return { valid : false , message : 'GitHub user not found' } ;
143+ }
144+ if ( response . status === 403 || response . status === 429 ) {
145+ return { valid : null , message : 'Rate limit reached. Validation skipped.' } ;
146+ }
147+ return { valid : null , message : 'Unable to verify' } ;
148+ } catch ( error ) {
149+ return { valid : null , message : 'Unable to verify' } ;
150+ }
151+ }
152+
153+ /**
154+ * Updates the GitHub ID field UI based on validation state
155+ * @param {HTMLInputElement } input - The input element
156+ * @param {string } state - Validation state: clear, validating, valid, invalid, warning
157+ * @param {string } message - The message to display
158+ */
159+ function updateGitHubValidationUI ( input , state , message ) {
160+ const fieldWrapper = input . closest ( '.form-field' ) ;
161+ const existingMsg = fieldWrapper . querySelector ( '.validation-message' ) ;
162+
163+ fieldWrapper . classList . remove ( 'validating' , 'valid' , 'invalid' ) ;
164+ if ( existingMsg ) existingMsg . remove ( ) ;
165+
166+ if ( ! state || state === 'clear' ) return ;
167+
168+ if ( state === 'validating' ) {
169+ fieldWrapper . classList . add ( 'validating' ) ;
170+ } else if ( state === 'valid' && message ) {
171+ fieldWrapper . classList . add ( 'valid' ) ;
172+ const msg = createTag ( 'div' , { class : 'validation-message validation-success' } , message ) ;
173+ input . after ( msg ) ;
174+ } else if ( state === 'invalid' ) {
175+ fieldWrapper . classList . add ( 'invalid' ) ;
176+ const msg = createTag ( 'div' , { class : 'validation-message validation-error' } , message ) ;
177+ input . after ( msg ) ;
178+ } else if ( state === 'warning' ) {
179+ const msg = createTag ( 'div' , { class : 'validation-message validation-warning' } , message ) ;
180+ input . after ( msg ) ;
181+ }
182+ }
183+
111184/**
112185 * Creates and shows a custom modal dialog
113186 * @param {string } message - The error message to display
@@ -571,6 +644,31 @@ async function buildForm(block) {
571644 } ) ;
572645 const githubHelpText = createTag ( 'p' , { class : 'help-text' } , 'If you provide your GitHub ID we will also set up a GitHub repo with project files so you can do code and style changes.' ) ;
573646 githubField . append ( githubLabel , githubInput , githubHelpText ) ;
647+
648+ // Add GitHub ID validation
649+ const debouncedValidate = debounce ( async ( ) => {
650+ const username = githubInput . value . trim ( ) ;
651+
652+ if ( ! username ) {
653+ updateGitHubValidationUI ( githubInput , 'clear' , '' ) ;
654+ return ;
655+ }
656+
657+ updateGitHubValidationUI ( githubInput , 'validating' , '' ) ;
658+ const result = await validateGitHubId ( username ) ;
659+
660+ if ( result . valid === true && result . message ) {
661+ updateGitHubValidationUI ( githubInput , 'valid' , result . message ) ;
662+ } else if ( result . valid === false ) {
663+ updateGitHubValidationUI ( githubInput , 'invalid' , result . message ) ;
664+ } else if ( result . valid === null ) {
665+ updateGitHubValidationUI ( githubInput , 'warning' , result . message ) ;
666+ } else {
667+ updateGitHubValidationUI ( githubInput , 'clear' , '' ) ;
668+ }
669+ } , 800 ) ;
670+
671+ githubInput . addEventListener ( 'input' , debouncedValidate ) ;
574672 }
575673
576674 // Terms and Conditions - extract from block content
@@ -662,6 +760,16 @@ async function buildForm(block) {
662760 form . addEventListener ( 'submit' , async ( e ) => {
663761 e . preventDefault ( ) ;
664762
763+ // Check if GitHub ID field is in invalid state
764+ const githubFieldEl = form . querySelector ( '#github-field' ) ;
765+ if ( githubFieldEl && githubFieldEl . classList . contains ( 'invalid' ) ) {
766+ const githubInputEl = githubFieldEl . querySelector ( 'input' ) ;
767+ if ( githubInputEl ) {
768+ githubInputEl . focus ( ) ;
769+ }
770+ return ;
771+ }
772+
665773 // Disable submit button to prevent multiple submissions
666774 const submitButtonSubmit = form . querySelector ( 'button[type="submit"]' ) ;
667775 submitButtonSubmit . disabled = true ;
0 commit comments