feat: enhance profile card with animations matching React component

- Add animated EXP progress bar (0% → 65% with ease-out on gray track)
- Add animated stat counters (count up from 0 to 72.9K/828/342.9K)
- Expand rainbow gradient to 6 colors (purple→pink→orange→yellow→green→blue)
- Add Follow/Following toggle button on cover banner
- Add title/bio text between name and email
- Add top/bottom borders to stats section
- Add _formatNumber() helper for K/M formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 16:46:38 +05:30
parent 3816c2c844
commit 89e12a707b

View File

@@ -19,7 +19,8 @@ class ProfileScreen extends StatefulWidget {
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
class _ProfileScreenState extends State<ProfileScreen>
with SingleTickerProviderStateMixin {
String _username = '';
String _email = 'not provided';
String _profileImage = '';
@@ -33,15 +34,96 @@ class _ProfileScreenState extends State<ProfileScreen> {
bool _loadingEvents = true;
// Gradient used for EXP bar and rainbow bar
// Gradient used for EXP bar and rainbow bar (6-color rainbow)
static const LinearGradient _expGradient = LinearGradient(
colors: [Color(0xFFA855F7), Color(0xFFEAB308), Color(0xFF3B82F6)],
colors: [
Color(0xFFA855F7), // purple-500
Color(0xFFEC4899), // pink-500
Color(0xFFF97316), // orange-500
Color(0xFFEAB308), // yellow-500
Color(0xFF22C55E), // green-500
Color(0xFF3B82F6), // blue-500
],
);
// Animation state
late AnimationController _animController;
double _expProgress = 0.0;
int _animatedLikes = 0;
int _animatedPosts = 0;
int _animatedViews = 0;
bool _isFollowing = false;
String _title = 'Product Designer who focuses on simplicity & usability.';
// Target stat values
static const int _targetLikes = 72900;
static const int _targetPosts = 828;
static const int _targetViews = 342900;
@override
void initState() {
super.initState();
// Animation controller for EXP bar + stat counters
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
_loadProfile();
_startAnimations();
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
void _startAnimations() {
// Delay to match React's setTimeout(500ms)
Future.delayed(const Duration(milliseconds: 500), () {
if (!mounted) return;
// Animate EXP bar: 0 → 0.65 with ease-out over 1.3s
final expTween = Tween<double>(begin: 0.0, end: 0.65);
final expAnim = CurvedAnimation(
parent: _animController,
curve: const Interval(0.0, 0.65, curve: Curves.easeOut),
);
expAnim.addListener(() {
if (mounted) {
setState(() {
_expProgress = expTween.evaluate(expAnim);
});
}
});
// Animate stat counters: 0 → target over full 2s
_animController.addListener(() {
if (!mounted) return;
final t = _animController.value;
setState(() {
_animatedLikes = (t * _targetLikes).round();
_animatedPosts = (t * _targetPosts).round();
_animatedViews = (t * _targetViews).round();
});
});
_animController.forward();
});
}
/// Format large numbers: ≥1M → "X.XM", ≥1K → "X.XK", else raw
String _formatNumber(int n) {
if (n >= 1000000) {
final val = n / 1000000;
return '${val.toStringAsFixed(1)}M';
} else if (n >= 1000) {
final val = n / 1000;
return '${val.toStringAsFixed(1)}K';
}
return n.toString();
}
// ───────── Data Loading (unchanged) ─────────
@@ -585,20 +667,36 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
),
),
// Edit pencil button (top-right)
// Follow / Following toggle button (top-right)
Positioned(
top: 8,
right: 8,
child: GestureDetector(
onTap: () => _openEditDialog(),
child: Container(
width: 36,
height: 36,
onTap: () => setState(() => _isFollowing = !_isFollowing),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
color: _isFollowing ? Colors.white : Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Text(
_isFollowing ? 'Following \u2713' : 'Follow +',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: _isFollowing
? AppConstants.textSecondary
: AppConstants.textPrimary,
),
),
child: const Icon(Icons.edit, color: Colors.white, size: 18),
),
),
),
@@ -641,7 +739,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
// ───────── EXP Progress Bar ─────────
// ───────── Animated EXP Progress Bar ─────────
Widget _buildExpBar() {
return Padding(
@@ -658,12 +756,29 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const SizedBox(width: 8),
Expanded(
child: Container(
height: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
gradient: _expGradient,
),
child: LayoutBuilder(
builder: (context, constraints) {
final fullWidth = constraints.maxWidth;
final filledWidth = fullWidth * _expProgress;
return Container(
height: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.grey.shade200, // gray track
),
child: Align(
alignment: Alignment.centerLeft,
child: Container(
width: filledWidth,
height: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
gradient: _expGradient,
),
),
),
);
},
),
),
],
@@ -671,7 +786,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
// ───────── Stats Row ─────────
// ───────── Stats Row (animated counters + top/bottom borders) ─────────
Widget _buildStatsRow() {
Widget statColumn(String value, String label) {
@@ -701,16 +816,23 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey.shade200, width: 1),
bottom: BorderSide(color: Colors.grey.shade200, width: 1),
),
),
child: IntrinsicHeight(
child: Row(
children: [
statColumn('1.2K', 'Likes'),
statColumn(_formatNumber(_animatedLikes), 'Likes'),
VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1),
statColumn('45', 'Posts'),
statColumn(_formatNumber(_animatedPosts), 'Posts'),
VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1),
statColumn('3.4K', 'Views'),
statColumn(_formatNumber(_animatedViews), 'Views'),
],
),
),
@@ -795,6 +917,21 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
),
),
const SizedBox(height: 6),
// Title / Bio
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppConstants.textSecondary,
height: 1.5,
),
),
),
const SizedBox(height: 4),
// Email
@@ -802,10 +939,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_email,
style: const TextStyle(
fontSize: 14,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w300,
color: AppConstants.textSecondary,
color: Colors.grey.shade400,
),
),
),