Interactive Profile Setup Form Component
A comprehensive form for users to complete their profile after registration. It features a clean layout, an interactive avatar uploader with a live preview, and sections for personal, professional, and social information. The form includes real-time validation on blur, a character counter for the bio, and a smooth transition to a success state upon completion.
LTR
RTL
<section class="relative overflow-hidden min-h-screen bg-white dark:bg-gray-900 flex items-center justify-center py-12">
<!-- Background elements -->
<div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-1/2 -right-1/2 bg-blue-400/20 dark:bg-blue-600/20 blur-3xl w-96 h-96 rounded-full"></div>
<div class="absolute -bottom-1/2 -left-1/2 bg-purple-400/20 dark:bg-purple-600/20 blur-3xl w-96 h-96 rounded-full"></div>
</div>
<div class="relative z-10 mx-auto px-4 w-full max-w-2xl">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 leading-tight sm:text-4xl dark:text-white">Complete Your Profile</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Tell us more about yourself to personalize your experience</p>
</div>
<!-- Profile Setup Form Container -->
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-gray-200/50 dark:border-gray-700/50"
x-data="{
submitting: false,
showSuccess: false,
formData: {
avatar: '',
firstName: '',
lastName: '',
bio: '',
location: '',
website: '',
jobTitle: '',
company: '',
facebook: '',
linkedin: ''
},
errors: {},
validateField(fieldName, value) {
this.errors[fieldName] = '';
if (fieldName === 'firstName') {
if (!value.trim()) return 'First name is required';
}
if (fieldName === 'lastName') {
if (!value.trim()) return 'Last name is required';
}
if (fieldName === 'bio') {
if (value.length > 200) return 'Bio must be less than 200 characters';
}
if (fieldName === 'website') {
if (value && !/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/.test(value)) {
return 'Please enter a valid website URL';
}
}
if (fieldName === 'facebook') {
if (value && !/^(https?:\/\/)?(www\.)?facebook\.com\/[a-zA-Z0-9(\.\?)?]/.test(value)) {
return 'Please enter a valid Facebook URL';
}
}
return '';
},
validateForm() {
this.errors = {};
let isValid = true;
// Validate required fields
Object.keys(this.formData).forEach(field => {
if (field === 'firstName' || field === 'lastName') {
const error = this.validateField(field, this.formData[field]);
if (error) {
this.errors[field] = error;
isValid = false;
}
}
});
// Validate optional fields if they have values
['bio', 'website', 'facebook'].forEach(field => {
if (this.formData[field]) {
const error = this.validateField(field, this.formData[field]);
if (error) {
this.errors[field] = error;
isValid = false;
}
}
});
return isValid;
},
async submitForm() {
if (!this.validateForm()) {
// Scroll to first error
const firstError = document.querySelector('.error-input');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstError.focus();
}
return;
}
this.submitting = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
// Show success message
this.showSuccess = true;
this.submitting = false;
},
handleAvatarUpload(event) {
const file = event.target.files[0];
if (file) {
if (file.size > 2 * 1024 * 1024) { // 2MB limit
alert('File size must be less than 2MB');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this.formData.avatar = e.target.result;
};
reader.readAsDataURL(file);
}
}
}">
<!-- Profile Setup Form -->
<form @submit.prevent="submitForm" x-show="!showSuccess" x-transition>
<div class="space-y-8">
<!-- Avatar Section -->
<div class="text-center">
<div class="avatar-upload inline-block mb-4 relative">
<div class="relative w-32 h-32 mx-auto rounded-full border-2 border-dashed border-gray-300 dark:border-gray-600 flex items-center justify-center overflow-hidden">
<!-- Profile Image -->
<template x-if="formData.avatar">
<img :src="formData.avatar" alt="Profile preview" class="w-full h-full object-cover rounded-full">
</template>
<!-- Placeholder Icon -->
<template x-if="!formData.avatar">
<div class="flex items-center justify-center w-full h-full text-gray-400">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
</template>
<!-- Hover Overlay -->
<div class="absolute inset-0 bg-black bg-opacity-30 rounded-full flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-200 cursor-pointer">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</div>
<!-- Hidden File Input -->
<input type="file" accept="image/*" @change="handleAvatarUpload" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer rounded-full">
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Click on the avatar to upload a profile picture</p>
</div>
<!-- Personal Information -->
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Personal Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="firstName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">First Name</label>
<input type="text" id="firstName" x-model="formData.firstName"
@blur="errors.firstName = validateField('firstName', formData.firstName)"
:class="errors.firstName ? 'error-input' : ''"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="John">
<div x-show="errors.firstName" x-text="errors.firstName" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div>
<label for="lastName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Last Name</label>
<input type="text" id="lastName" x-model="formData.lastName"
@blur="errors.lastName = validateField('lastName', formData.lastName)"
:class="errors.lastName ? 'error-input' : ''"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="Doe">
<div x-show="errors.lastName" x-text="errors.lastName" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
</div>
</div>
<!-- Bio -->
<div>
<label for="bio" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Bio</label>
<textarea id="bio" x-model="formData.bio" rows="3"
@blur="errors.bio = validateField('bio', formData.bio)"
:class="errors.bio ? 'error-input' : ''"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="Tell us a bit about yourself..."></textarea>
<div class="flex justify-between mt-1">
<div x-show="errors.bio" x-text="errors.bio" class="text-sm text-red-600 dark:text-red-400"></div>
<div class="text-sm text-gray-500 dark:text-gray-400" x-text="`${200 - formData.bio.length} characters remaining`"></div>
</div>
</div>
<!-- Professional Information -->
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Professional Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="jobTitle" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Job Title</label>
<input type="text" id="jobTitle" x-model="formData.jobTitle"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="Software Engineer">
</div>
<div>
<label for="company" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Company</label>
<input type="text" id="company" x-model="formData.company"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="Acme Inc.">
</div>
</div>
</div>
<!-- Location & Website -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Location</label>
<input type="text" id="location" x-model="formData.location"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="New York, NY">
</div>
<div>
<label for="website" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Website</label>
<input type="text" id="website" x-model="formData.website"
@blur="errors.website = validateField('website', formData.website)"
:class="errors.website ? 'error-input' : ''"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="https://example.com">
<div x-show="errors.website" x-text="errors.website" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
</div>
<!-- Social Links -->
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Social Links</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="facebook" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 flex items-center">
<svg class="w-4 h-4 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
Facebook
</label>
<input type="text" id="facebook" x-model="formData.facebook"
@blur="errors.facebook = validateField('facebook', formData.facebook)"
:class="errors.facebook ? 'error-input' : ''"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="https://facebook.com/username">
<div x-show="errors.facebook" x-text="errors.facebook" class="mt-1 text-sm text-red-600 dark:text-red-400"></div>
</div>
<div>
<label for="linkedin" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 flex items-center">
<svg class="w-4 h-4 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
</svg>
LinkedIn
</label>
<input type="text" id="linkedin" x-model="formData.linkedin"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none transition-all duration-200"
placeholder="linkedin.com/in/username">
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="pt-4">
<button type="submit" :disabled="submitting" class="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-70 disabled:cursor-not-allowed transition-colors duration-200 font-medium flex items-center justify-center">
<span x-show="!submitting">Complete Profile</span>
<span x-show="submitting" class="flex items-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="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>
</svg>
Saving Profile...
</span>
</button>
</div>
</div>
</form>
<!-- Success Message -->
<div x-show="showSuccess" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
class="text-center py-8">
<div class="mb-4 flex justify-center">
<div class="w-16 h-16 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center success-checkmark">
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Profile Updated Successfully!</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">Your profile has been updated with the new information.</p>
<div class="flex space-x-4">
<a href="#" class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200 font-medium text-center">
View Profile
</a>
<a href="#" class="flex-1 px-4 py-2 bg-blue-600 text-white text-center rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium">Go to Dashboard</a>
</div>
</div>
</div>
</div>
</section>