@@ -42,6 +42,7 @@ import { Card } from "./card";
4242import { StickyAlert } from "./sticky-alert" ;
4343import { Spinner } from "@/components/ui/spinner" ;
4444import { Modal } from "./modal" ;
45+ import { API_ENDPOINTS } from "@/lib/api-config" ;
4546
4647import { HudFrame } from "./hud-frame" ;
4748// PRODUCTION MODE - Debug features disabled
@@ -157,6 +158,9 @@ export function RegistrationContainer({
157158 // Errors
158159 const [ errors , setErrors ] = useState < Record < string , string > > ( { } ) ;
159160 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
161+ // True while a reCAPTCHA-guarded availability check (email / Discord) is in
162+ // flight, so the "Continue" button can show progress and block double-clicks.
163+ const [ checkingAvailability , setCheckingAvailability ] = useState ( false ) ;
160164
161165 // Google SSO. authMethod "google" means the user authenticated via the Google
162166 // popup; we then collect the remaining profile fields, lock the name, and skip
@@ -596,7 +600,25 @@ export function RegistrationContainer({
596600 return "pending" ;
597601 } ;
598602
599- const goNext = ( ) => {
603+ const isFieldTaken = async (
604+ field : "email" | "discord_username" ,
605+ value : string ,
606+ ) : Promise < boolean > => {
607+ try {
608+ const token = await executeRecaptcha ( "check_registration" ) ;
609+ const params = new URLSearchParams ( { [ field ] : value } ) ;
610+ if ( token ) params . set ( "recaptcha_token" , token ) ;
611+ const res = await fetch ( `${ API_ENDPOINTS . register } ?${ params . toString ( ) } ` ) ;
612+ if ( ! res . ok ) return false ;
613+ const data = await res . json ( ) ;
614+ return data ?. exists === true ;
615+ } catch ( error ) {
616+ console . error ( "[registration] availability check failed:" , error ) ;
617+ return false ;
618+ }
619+ } ;
620+
621+ const goNext = async ( ) => {
600622 const stepId = STEPS [ currentStepIndex ] . id ;
601623 const stepErrors = validateStep ( stepId ) ;
602624
@@ -610,6 +632,42 @@ export function RegistrationContainer({
610632 return ;
611633 }
612634
635+ const checks : Array < {
636+ field : "email" | "discord_username" ;
637+ value : string ;
638+ message : string ;
639+ } > = [ ] ;
640+ if ( stepId === "account" && authMethod === "email" ) {
641+ checks . push ( {
642+ field : "email" ,
643+ value : registerData . email . trim ( ) ,
644+ message : "This email is already registered." ,
645+ } ) ;
646+ }
647+ if ( stepId === "identity" ) {
648+ checks . push ( {
649+ field : "discord_username" ,
650+ value : registerData . discord_username . trim ( ) ,
651+ message : "This Discord username is already registered." ,
652+ } ) ;
653+ }
654+
655+ if ( checks . length > 0 ) {
656+ setCheckingAvailability ( true ) ;
657+ try {
658+ for ( const check of checks ) {
659+ if ( await isFieldTaken ( check . field , check . value ) ) {
660+ setErrors ( ( prev ) => ( { ...prev , [ check . field ] : check . message } ) ) ;
661+ setAlert ( { type : "error" , message : check . message } ) ;
662+ setTimeout ( ( ) => setAlert ( null ) , 3000 ) ;
663+ return ;
664+ }
665+ }
666+ } finally {
667+ setCheckingAvailability ( false ) ;
668+ }
669+ }
670+
613671 setAlert ( null ) ;
614672 if ( currentStepIndex < STEPS . length - 1 ) {
615673 setCurrentStepIndex ( ( i ) => i + 1 ) ;
@@ -1869,9 +1927,23 @@ export function RegistrationContainer({
18691927 { STEPS . length - currentStepIndex - 1 } step
18701928 { STEPS . length - currentStepIndex - 1 === 1 ? "" : "s" } to go
18711929 </ div >
1872- < Button type = "button" variant = "primary" onClick = { goNext } >
1873- Continue
1874- < ChevronRight className = "w-4 h-4 ml-1.5" />
1930+ < Button
1931+ type = "button"
1932+ variant = "primary"
1933+ onClick = { goNext }
1934+ disabled = { checkingAvailability }
1935+ >
1936+ { checkingAvailability ? (
1937+ < >
1938+ < Spinner size = "sm" className = "mr-2" />
1939+ Checking…
1940+ </ >
1941+ ) : (
1942+ < >
1943+ Continue
1944+ < ChevronRight className = "w-4 h-4 ml-1.5" />
1945+ </ >
1946+ ) }
18751947 </ Button >
18761948 </ div >
18771949 ) }
0 commit comments