@@ -5,6 +5,9 @@ import { useRouter } from 'next/navigation';
55import Link from 'next/link' ;
66import { createClient } from '@/lib/supabase/client' ;
77import { toast } from 'sonner' ;
8+ import CodeuniaLogo from '@/components/codeunia-logo' ;
9+ import { InputValidator } from '@/lib/security/input-validation' ;
10+ import { CheckCircle , XCircle , AlertCircle , Loader2 , Sparkles } from 'lucide-react' ;
811
912interface User {
1013 id : string ;
@@ -25,6 +28,7 @@ export default function CompleteProfile() {
2528 const [ isLoading , setIsLoading ] = useState ( false ) ;
2629 const [ isCheckingUsername , setIsCheckingUsername ] = useState ( false ) ;
2730 const [ usernameAvailable , setUsernameAvailable ] = useState < boolean | null > ( null ) ;
31+ const [ usernameError , setUsernameError ] = useState < string > ( '' ) ;
2832 const [ user , setUser ] = useState < User | null > ( null ) ;
2933 const [ isValidating , setIsValidating ] = useState ( true ) ;
3034 const router = useRouter ( ) ;
@@ -101,31 +105,53 @@ export default function CompleteProfile() {
101105 } , [ ] ) ;
102106
103107 const checkUsernameAvailability = async ( usernameToCheck : string ) => {
104- if ( ! usernameToCheck || usernameToCheck . length < 3 ) {
108+ const clean = usernameToCheck . trim ( ) ;
109+
110+ // First validate the username format
111+ const validation = InputValidator . validateUsername ( clean ) ;
112+ if ( ! validation . isValid ) {
113+ setUsernameError ( validation . error || 'Invalid username' ) ;
114+ setUsernameAvailable ( false ) ;
115+ return ;
116+ }
117+
118+ setUsernameError ( '' ) ;
119+
120+ if ( ! clean || clean . length < 3 ) {
105121 setUsernameAvailable ( null ) ;
106122 return ;
107123 }
108124
109125 setIsCheckingUsername ( true ) ;
110126 try {
111- const clean = usernameToCheck . trim ( ) ;
112- const { data : isAvailable } = await getSupabaseClient ( ) . rpc ( 'check_username_availability' , {
113- username_param : clean
114- } ) ;
115- setUsernameAvailable ( isAvailable ) ;
127+ // Use direct query instead of RPC function (same approach as UsernameField component)
128+ const { data, error } = await getSupabaseClient ( )
129+ . from ( 'profiles' )
130+ . select ( 'username' )
131+ . eq ( 'username' , clean )
132+ . single ( ) ;
133+
134+ if ( error && error . code !== 'PGRST116' ) {
135+ throw error ;
136+ }
137+
138+ // If no data found, username is available
139+ setUsernameAvailable ( ! data ) ;
116140 } catch ( error ) {
117141 console . error ( 'Error checking username:' , error ) ;
118142 setUsernameAvailable ( null ) ;
143+ setUsernameError ( 'Unable to check username availability' ) ;
119144 } finally {
120145 setIsCheckingUsername ( false ) ;
121146 }
122147 } ;
123148
124149 const handleUsernameChange = ( value : string ) => {
125150 setUsername ( value ) ;
151+ setUsernameError ( '' ) ; // Clear previous errors
126152 if ( usernameCheckTimeout . current ) clearTimeout ( usernameCheckTimeout . current ) ;
127153 usernameCheckTimeout . current = setTimeout ( ( ) => {
128- checkUsernameAvailability ( value . trim ( ) ) ;
154+ checkUsernameAvailability ( value ) ;
129155 } , 500 ) ;
130156 } ;
131157
@@ -199,11 +225,17 @@ export default function CompleteProfile() {
199225
200226 const generateRandomUsername = async ( ) => {
201227 try {
202- const { data : randomUsername } = await getSupabaseClient ( ) . rpc ( 'generate_safe_username' ) ;
203- if ( randomUsername ) {
204- setUsername ( randomUsername ) ;
205- checkUsernameAvailability ( randomUsername ) ;
206- }
228+ // Generate a simple random username since RPC might not be available
229+ const adjectives = [ 'cool' , 'smart' , 'bright' , 'quick' , 'bold' , 'wise' , 'keen' , 'sharp' ] ;
230+ const nouns = [ 'coder' , 'dev' , 'builder' , 'creator' , 'maker' , 'hacker' , 'ninja' , 'wizard' ] ;
231+ const numbers = Math . floor ( Math . random ( ) * 9999 ) ;
232+
233+ const adjective = adjectives [ Math . floor ( Math . random ( ) * adjectives . length ) ] ;
234+ const noun = nouns [ Math . floor ( Math . random ( ) * nouns . length ) ] ;
235+ const randomUsername = `${ adjective } ${ noun } ${ numbers } ` ;
236+
237+ setUsername ( randomUsername ) ;
238+ checkUsernameAvailability ( randomUsername ) ;
207239 } catch ( error ) {
208240 console . error ( 'Error generating username:' , error ) ;
209241 toast . error ( 'Error generating username' ) ;
@@ -223,137 +255,189 @@ export default function CompleteProfile() {
223255 }
224256
225257 return (
226- < div className = "min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4" >
227- < div className = "max-w-md w-full bg-white rounded-2xl shadow-xl p-8" >
258+ < div className = "min-h-screen bg-gradient-to-br from-slate-50 via- blue-50 to-indigo-100 flex items-center justify-center p-4" >
259+ < div className = "max-w-md w-full bg-white/80 backdrop-blur-sm rounded-3xl shadow-2xl border border-white/20 p-8" >
228260 { /* Header */ }
229261 < div className = "text-center mb-8" >
230- < div className = "w-16 h-16 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4 " >
231- < span className = "text-white text-2xl font-bold" > CU </ span >
262+ < div className = "flex justify-center mb-6 " >
263+ < CodeuniaLogo size = "lg" showText = { true } noLink = { true } instanceId = "complete-profile" / >
232264 </ div >
233- < h1 className = "text-2xl font-bold text-gray-900 mb-2 " >
234- Welcome! Let's set up your CodeUnia profile.
265+ < h1 className = "text-2xl font-bold text-gray-900 mb-3 " >
266+ Welcome! Let's set up your profile
235267 </ h1 >
236- < p className = "text-gray-600" >
268+ < p className = "text-gray-600 leading-relaxed " >
237269 Complete your profile to get started with CodeUnia. This will only take a moment.
238270 </ p >
239271 </ div >
240272
241273 { /* Setup Form */ }
242274 < form onSubmit = { handleSubmit } className = "space-y-6" >
243275 { /* First Name */ }
244- < div >
245- < label className = "block text-sm font-medium text-gray-700 mb-2 " >
276+ < div className = "space-y-2" >
277+ < label className = "block text-sm font-semibold text-gray-700" >
246278 First Name *
247279 </ label >
248280 < input
249281 type = "text"
250282 value = { firstName }
251283 onChange = { ( e ) => setFirstName ( e . target . value ) }
252- className = "w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent "
284+ className = "w-full border border-gray-200 rounded-xl px-4 py-3 text-sm bg-white/50 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all duration-200 placeholder:text-gray-400 "
253285 placeholder = "Enter your first name"
254286 required
255287 />
256288 </ div >
257289
258290 { /* Last Name */ }
259- < div >
260- < label className = "block text-sm font-medium text-gray-700 mb-2 " >
291+ < div className = "space-y-2" >
292+ < label className = "block text-sm font-semibold text-gray-700" >
261293 Last Name *
262294 </ label >
263295 < input
264296 type = "text"
265297 value = { lastName }
266298 onChange = { ( e ) => setLastName ( e . target . value ) }
267- className = "w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent "
299+ className = "w-full border border-gray-200 rounded-xl px-4 py-3 text-sm bg-white/50 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all duration-200 placeholder:text-gray-400 "
268300 placeholder = "Enter your last name"
269301 required
270302 />
271303 </ div >
272304
273305 { /* Username Input */ }
274- < div >
275- < label className = "block text-sm font-medium text-gray-700 mb-2 " >
306+ < div className = "space-y-3" >
307+ < label className = "block text-sm font-semibold text-gray-700" >
276308 Choose Your Username *
277309 </ label >
278310 < div className = "relative" >
279- < input
280- type = "text"
281- value = { username }
282- onChange = { ( e ) => handleUsernameChange ( e . target . value ) }
283- className = { `w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
284- usernameAvailable === true
285- ? 'border-green-300 bg-green-50'
286- : usernameAvailable === false
287- ? 'border-red-300 bg-red-50'
288- : 'border-gray-300'
289- } `}
290- placeholder = "Enter your username"
291- minLength = { 3 }
292- maxLength = { 20 }
293- pattern = "[a-zA-Z0-9_-]+"
294- title = "Username can only contain letters, numbers, hyphens, and underscores"
295- required
296- />
297- < button
298- type = "button"
299- onClick = { generateRandomUsername }
300- className = "absolute right-2 top-1/2 transform -translate-y-1/2 text-sm text-blue-600 hover:text-blue-700"
301- title = "Generate random username"
302- >
303- 🎲
304- </ button >
305- </ div >
306-
307- { /* Username Status */ }
308- { isCheckingUsername && (
309- < p className = "text-xs text-gray-500 mt-1" > Checking availability...</ p >
310- ) }
311- { usernameAvailable === true && (
312- < p className = "text-xs text-green-600 mt-1" > ✅ Username is available!</ p >
313- ) }
314- { usernameAvailable === false && (
315- < p className = "text-xs text-red-600 mt-1" > ❌ Username is already taken</ p >
316- ) }
317-
318- { /* Username Requirements */ }
319- < div className = "mt-2 text-xs text-gray-500" >
320- < p > • 3-20 characters long</ p >
321- < p > • Letters, numbers, hyphens, and underscores only</ p >
322- < p > • Must be unique across all users</ p >
311+ < div className = "relative" >
312+ < input
313+ type = "text"
314+ value = { username }
315+ onChange = { ( e ) => handleUsernameChange ( e . target . value ) }
316+ className = { `w-full border rounded-xl px-4 py-3 pr-20 text-sm bg-white/50 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all duration-200 placeholder:text-gray-400 ${
317+ usernameAvailable === true
318+ ? 'border-green-300 bg-green-50/50'
319+ : usernameAvailable === false || usernameError
320+ ? 'border-red-300 bg-red-50/50'
321+ : 'border-gray-200'
322+ } `}
323+ placeholder = "Enter your username"
324+ minLength = { 3 }
325+ maxLength = { 30 }
326+ title = "Username can only contain letters, numbers, hyphens, and underscores"
327+ required
328+ />
329+ < div className = "absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center space-x-2" >
330+ { isCheckingUsername && (
331+ < Loader2 className = "h-4 w-4 animate-spin text-blue-500" />
332+ ) }
333+ { ! isCheckingUsername && usernameAvailable === true && (
334+ < CheckCircle className = "h-4 w-4 text-green-500" />
335+ ) }
336+ { ! isCheckingUsername && ( usernameAvailable === false || usernameError ) && (
337+ < XCircle className = "h-4 w-4 text-red-500" />
338+ ) }
339+ < button
340+ type = "button"
341+ onClick = { generateRandomUsername }
342+ className = "p-1 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-md transition-colors"
343+ title = "Generate random username"
344+ >
345+ < Sparkles className = "h-4 w-4" />
346+ </ button >
347+ </ div >
348+ </ div >
349+
350+ { /* Username Preview */ }
351+ { username && (
352+ < div className = "mt-2 flex items-center space-x-2" >
353+ < span className = "text-xs text-gray-500" > Preview:</ span >
354+ < span className = "text-xs font-mono bg-gray-100 px-2 py-1 rounded-md" >
355+ @{ username . toLowerCase ( ) }
356+ </ span >
357+ </ div >
358+ ) }
359+
360+ { /* Username Status Messages */ }
361+ { isCheckingUsername && (
362+ < div className = "flex items-center space-x-2 mt-2" >
363+ < Loader2 className = "h-3 w-3 animate-spin text-blue-500" />
364+ < p className = "text-xs text-blue-600" > Checking availability...</ p >
365+ </ div >
366+ ) }
367+
368+ { ! isCheckingUsername && usernameAvailable === true && (
369+ < div className = "flex items-center space-x-2 mt-2" >
370+ < CheckCircle className = "h-3 w-3 text-green-500" />
371+ < p className = "text-xs text-green-600" > Username is available!</ p >
372+ </ div >
373+ ) }
374+
375+ { ! isCheckingUsername && usernameAvailable === false && ! usernameError && (
376+ < div className = "flex items-center space-x-2 mt-2" >
377+ < XCircle className = "h-3 w-3 text-red-500" />
378+ < p className = "text-xs text-red-600" > Username is already taken</ p >
379+ </ div >
380+ ) }
381+
382+ { usernameError && (
383+ < div className = "flex items-center space-x-2 mt-2" >
384+ < AlertCircle className = "h-3 w-3 text-red-500" />
385+ < p className = "text-xs text-red-600" > { usernameError } </ p >
386+ </ div >
387+ ) }
388+
389+ { /* Username Requirements */ }
390+ < div className = "mt-3 p-3 bg-gray-50/50 rounded-lg border border-gray-100" >
391+ < p className = "text-xs font-medium text-gray-700 mb-2" > Username Requirements:</ p >
392+ < ul className = "text-xs text-gray-600 space-y-1" >
393+ < li className = "flex items-center space-x-2" >
394+ < div className = { `w-1.5 h-1.5 rounded-full ${ username . length >= 3 && username . length <= 30 ? 'bg-green-500' : 'bg-gray-300' } ` } > </ div >
395+ < span > 3-30 characters long</ span >
396+ </ li >
397+ < li className = "flex items-center space-x-2" >
398+ < div className = { `w-1.5 h-1.5 rounded-full ${ / ^ [ a - z A - Z 0 - 9 _ - ] + $ / . test ( username ) || ! username ? 'bg-green-500' : 'bg-gray-300' } ` } > </ div >
399+ < span > Letters, numbers, hyphens, and underscores only</ span >
400+ </ li >
401+ < li className = "flex items-center space-x-2" >
402+ < div className = { `w-1.5 h-1.5 rounded-full ${ usernameAvailable === true ? 'bg-green-500' : usernameAvailable === false ? 'bg-red-500' : 'bg-gray-300' } ` } > </ div >
403+ < span > Must be unique across all users</ span >
404+ </ li >
405+ </ ul >
406+ </ div >
323407 </ div >
324408 </ div >
325409
326410 { /* Submit Button */ }
327411 < button
328412 type = "submit"
329- disabled = { isLoading || ! firstName . trim ( ) || ! lastName . trim ( ) || ! username || ! usernameAvailable }
330- className = { `w-full py-3 px-4 rounded-md font-medium transition-colors ${
331- isLoading || ! firstName . trim ( ) || ! lastName . trim ( ) || ! username || ! usernameAvailable
332- ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
333- : 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700'
413+ disabled = { isLoading || ! firstName . trim ( ) || ! lastName . trim ( ) || ! username || ! usernameAvailable || ! ! usernameError }
414+ className = { `w-full py-4 px-6 rounded-xl font-semibold text-sm transition-all duration-200 ${
415+ isLoading || ! firstName . trim ( ) || ! lastName . trim ( ) || ! username || ! usernameAvailable || ! ! usernameError
416+ ? 'bg-gray-200 text-gray-400 cursor-not-allowed'
417+ : 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 hover:shadow-lg hover:scale-[1.02] active:scale-[0.98] '
334418 } `}
335419 >
336420 { isLoading ? (
337421 < span className = "flex items-center justify-center" >
338- < svg className = "animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" >
339- < circle className = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "4" > </ circle >
340- < path className = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > </ path >
341- </ svg >
422+ < Loader2 className = "animate-spin -ml-1 mr-3 h-5 w-5" />
342423 Completing profile...
343424 </ span >
344425 ) : (
345- 'Complete Profile & Continue'
426+ < span className = "flex items-center justify-center" >
427+ < CheckCircle className = "mr-2 h-4 w-4" />
428+ Complete Profile & Continue
429+ </ span >
346430 ) }
347431 </ button >
348432 </ form >
349433
350434 { /* Footer */ }
351- < div className = "mt-8 text-center " >
352- < p className = "text-xs text-gray-500" >
435+ < div className = "mt-8 pt-6 border-t border-gray-100 " >
436+ < p className = "text-xs text-gray-500 text-center leading-relaxed " >
353437 By continuing, you agree to CodeUnia's{ ' ' }
354- < Link href = "/terms" className = "text-blue-600 hover:underline" > Terms of Service</ Link >
438+ < Link href = "/terms" className = "text-blue-600 hover:text-blue-700 hover: underline transition-colors " > Terms of Service</ Link >
355439 { ' ' } and{ ' ' }
356- < Link href = "/privacy" className = "text-blue-600 hover:underline" > Privacy Policy</ Link >
440+ < Link href = "/privacy" className = "text-blue-600 hover:text-blue-700 hover: underline transition-colors " > Privacy Policy</ Link >
357441 </ p >
358442 </ div >
359443 </ div >
0 commit comments