Skip to content

Commit 77cd35a

Browse files
author
Deepak Pandey
committed
fix: Improve UI and fix username validation issues
- Replace 'CU' logo with proper CodeUnia logo component - Fix username validation to use direct database queries instead of RPC - Remove problematic regex pattern attribute causing console errors - Implement client-side random username generation - Add comprehensive username validation with visual feedback - Improve form styling with modern glassmorphism design - Add username preview and real-time validation indicators - Fix TypeScript errors and improve error handling Resolves console errors and improves user experience
1 parent a9fed5c commit 77cd35a

File tree

1 file changed

+169
-85
lines changed

1 file changed

+169
-85
lines changed

app/complete-profile/page.tsx

Lines changed: 169 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { useRouter } from 'next/navigation';
55
import Link from 'next/link';
66
import { createClient } from '@/lib/supabase/client';
77
import { 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

912
interface 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&apos;s set up your CodeUnia profile.
265+
<h1 className="text-2xl font-bold text-gray-900 mb-3">
266+
Welcome! Let&apos;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-zA-Z0-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&apos;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

Comments
 (0)