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(); State<ProfileScreen> createState() => _ProfileScreenState();
} }
class _ProfileScreenState extends State<ProfileScreen> { class _ProfileScreenState extends State<ProfileScreen>
with SingleTickerProviderStateMixin {
String _username = ''; String _username = '';
String _email = 'not provided'; String _email = 'not provided';
String _profileImage = ''; String _profileImage = '';
@@ -33,15 +34,96 @@ class _ProfileScreenState extends State<ProfileScreen> {
bool _loadingEvents = true; 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( 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 @override
void initState() { void initState() {
super.initState(); super.initState();
// Animation controller for EXP bar + stat counters
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
_loadProfile(); _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) ───────── // ───────── Data Loading (unchanged) ─────────
@@ -585,20 +667,36 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
), ),
), ),
// Edit pencil button (top-right) // Follow / Following toggle button (top-right)
Positioned( Positioned(
top: 8, top: 8,
right: 8, right: 8,
child: GestureDetector( child: GestureDetector(
onTap: () => _openEditDialog(), onTap: () => setState(() => _isFollowing = !_isFollowing),
child: Container( child: AnimatedContainer(
width: 36, duration: const Duration(milliseconds: 200),
height: 36, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15), color: _isFollowing ? Colors.white : Colors.white,
borderRadius: BorderRadius.circular(8), 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() { Widget _buildExpBar() {
return Padding( return Padding(
@@ -658,12 +756,29 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Container( child: LayoutBuilder(
height: 8, builder: (context, constraints) {
decoration: BoxDecoration( final fullWidth = constraints.maxWidth;
borderRadius: BorderRadius.circular(4), final filledWidth = fullWidth * _expProgress;
gradient: _expGradient, 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 _buildStatsRow() {
Widget statColumn(String value, String label) { Widget statColumn(String value, String label) {
@@ -701,16 +816,23 @@ class _ProfileScreenState extends State<ProfileScreen> {
); );
} }
return Padding( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8), 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: IntrinsicHeight(
child: Row( child: Row(
children: [ children: [
statColumn('1.2K', 'Likes'), statColumn(_formatNumber(_animatedLikes), 'Likes'),
VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1), 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), 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), const SizedBox(height: 4),
// Email // Email
@@ -802,10 +939,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text( child: Text(
_email, _email,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 13,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
color: AppConstants.textSecondary, color: Colors.grey.shade400,
), ),
), ),
), ),