// lib/screens/contributor_profile_screen.dart // CTR-004 — Public contributor profile page. // Shows avatar, tier ring, EP stats, and submission grid for any contributor. import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/services/gamification_service.dart'; import '../widgets/tier_avatar_ring.dart'; class ContributorProfileScreen extends StatefulWidget { final String contributorId; final String contributorName; const ContributorProfileScreen({ super.key, required this.contributorId, required this.contributorName, }); @override State createState() => _ContributorProfileScreenState(); } class _ContributorProfileScreenState extends State { DashboardResponse? _data; bool _loading = true; String? _error; @override void initState() { super.initState(); _load(); } Future _load() async { try { final data = await GamificationService().getDashboardForUser(widget.contributorId); if (mounted) { setState(() { _data = data; _loading = false; }); } } catch (_) { if (mounted) { setState(() { _error = 'Could not load profile'; _loading = false; }); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF0F172A), appBar: AppBar( backgroundColor: const Color(0xFF0F172A), foregroundColor: Colors.white, title: Text( widget.contributorName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), elevation: 0, ), body: _loading ? const Center( child: CircularProgressIndicator(color: Color(0xFF3B82F6)), ) : _error != null ? Center( child: Text( _error!, style: const TextStyle(color: Colors.white54), ), ) : _buildContent(), ); } Widget _buildContent() { final profile = _data!.profile; final submissions = _data!.submissions; final tierStr = tierLabel(profile.tier); return CustomScrollView( slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ // Avatar with tier ring TierAvatarRing( username: widget.contributorName, tier: tierStr, size: 88, ), const SizedBox(height: 12), Text( widget.contributorName, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white, ), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), decoration: BoxDecoration( color: const Color(0xFF1E3A8A), borderRadius: BorderRadius.circular(12), ), child: Text( tierStr, style: const TextStyle(fontSize: 12, color: Color(0xFF93C5FD)), ), ), const SizedBox(height: 16), // Stats row Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _statCard('EP', '${profile.currentEp}'), _statCard('Events', '${submissions.length}'), _statCard( 'Approved', '${submissions.where((s) => s.status.toUpperCase() == 'APPROVED').length}', ), ], ), ], ), ), ), if (submissions.isNotEmpty) SliverPadding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 24), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( (context, i) => _buildSubmissionTile(submissions[i]), childCount: submissions.length, ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 1.1, ), ), ) else const SliverToBoxAdapter( child: Center( child: Padding( padding: EdgeInsets.all(32), child: Text( 'No submissions yet', style: TextStyle(color: Colors.white38), ), ), ), ), ], ); } Widget _statCard(String label, String value) { return Column( children: [ Text( value, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w800, color: Colors.white, ), ), const SizedBox(height: 2), Text( label, style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8)), ), ], ); } Widget _buildSubmissionTile(SubmissionModel s) { final Color statusColor; switch (s.status.toUpperCase()) { case 'APPROVED': statusColor = const Color(0xFF22C55E); break; case 'REJECTED': statusColor = const Color(0xFFEF4444); break; default: statusColor = const Color(0xFFFBBF24); // PENDING } // SubmissionModel.images is List; use first image if present. final String? firstImage = s.images.isNotEmpty ? s.images.first : null; return Container( decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(10), ), child: Stack( children: [ if (firstImage != null && firstImage.isNotEmpty) ClipRRect( borderRadius: BorderRadius.circular(10), child: SizedBox.expand( child: CachedNetworkImage( imageUrl: firstImage, fit: BoxFit.cover, memCacheWidth: 400, memCacheHeight: 300, maxWidthDiskCache: 800, maxHeightDiskCache: 600, placeholder: (_, __) => Container(color: const Color(0xFF1E293B)), errorWidget: (_, __, ___) => Container(color: const Color(0xFF334155)), ), ), ), Positioned( top: 6, right: 6, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: statusColor.withOpacity(0.9), borderRadius: BorderRadius.circular(6), ), child: Text( s.status, style: const TextStyle( fontSize: 9, color: Colors.white, fontWeight: FontWeight.w700, ), ), ), ), if (s.eventName.isNotEmpty) Positioned( bottom: 0, left: 0, right: 0, child: Container( padding: const EdgeInsets.all(6), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black87, Colors.transparent], ), borderRadius: BorderRadius.vertical(bottom: Radius.circular(10)), ), child: Text( s.eventName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 10, color: Colors.white), ), ), ), ], ), ); } }